dev ce8f5005e483 cached
72 files
265.5 KB
68.8k tokens
354 symbols
1 requests
Download .txt
Showing preview only (284K chars total). Download the full file or copy to clipboard to get everything.
Repository: home-assistant/home-assistant-cli
Branch: dev
Commit: ce8f5005e483
Files: 72
Total size: 265.5 KB

Directory structure:
gitextract_uoxhyuya/

├── .coveragerc
├── .dockerignore
├── .github/
│   ├── release-drafter.yml
│   └── workflows/
│       ├── publish-to-pypi.yml
│       └── testing.yml
├── .gitignore
├── .pre-commit-config.yaml
├── Dockerfile
├── Dockerfile.armhf
├── LICENSE.md
├── MANIFEST.in
├── README.rst
├── SECURITY.md
├── docker-hass-cli
├── homeassistant_cli/
│   ├── __init__.py
│   ├── autocompletion.py
│   ├── cli.py
│   ├── config.py
│   ├── const.py
│   ├── exceptions.py
│   ├── hassconst.py
│   ├── helper.py
│   ├── plugins/
│   │   ├── area.py
│   │   ├── completion.py
│   │   ├── config.py
│   │   ├── device.py
│   │   ├── discover.py
│   │   ├── entity.py
│   │   ├── event.py
│   │   ├── ha.py
│   │   ├── info.py
│   │   ├── integration.py
│   │   ├── map.py
│   │   ├── raw.py
│   │   ├── service.py
│   │   ├── state.py
│   │   ├── system.py
│   │   └── template.py
│   ├── remote.py
│   └── yaml.py
├── mypy.ini
├── mypyrc
├── pylintrc
├── pyproject.toml
└── tests/
    ├── __init__.py
    ├── bandit.yaml
    ├── conftest.py
    ├── fixtures/
    │   ├── basic_entities.json
    │   ├── basic_entities_table.txt
    │   ├── basic_entities_table_columns.txt
    │   ├── basic_entities_table_format.txt
    │   ├── basic_entities_table_no_header.txt
    │   ├── basic_entities_table_sorted.txt
    │   ├── default_areas.json
    │   ├── default_devices.json
    │   ├── default_entities.json
    │   ├── default_events.json
    │   └── default_services.json
    ├── test_area.py
    ├── test_completion.py
    ├── test_defaults.py
    ├── test_device.py
    ├── test_ha.py
    ├── test_helper.py
    ├── test_info.py
    ├── test_integration.py
    ├── test_map.py
    ├── test_plugins.py
    ├── test_raw.py
    ├── test_service.py
    ├── test_state.py
    └── test_template.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
source = homeassistant_cli

[report]
# Regexes for lines to exclude from consideration
exclude_lines =
    # Have to re-enable the standard pragma
    pragma: no cover

    # Don't complain about missing debug-only code:
    def __repr__

    # Don't complain if tests don't hit defensive assertion code:
    raise AssertionError
    raise NotImplementedError


================================================
FILE: .dockerignore
================================================
# General files
.git
.github
config

# Test related files
.tox

# Other virtualization methods
venv
.vagrant

# Temporary files
**/__pycache__
.#*


================================================
FILE: .github/release-drafter.yml
================================================
template: |
  ## What's Changed

  $CHANGES


================================================
FILE: .github/workflows/publish-to-pypi.yml
================================================
name: Publish to PyPI

on:
  release:
    types: [published]

permissions:
  contents: read

jobs:
  deploy:

    runs-on: ubuntu-latest

    environment: release
    permissions:
      id-token: write

    steps:
    - uses: actions/checkout@v6

    - name: Set up Python
      uses: actions/setup-python@v6
      with:
        python-version: '3.13'
        cache: 'pip'

    - name: Install Poetry
      uses: snok/install-poetry@v1
      with:
        virtualenvs-create: true
        virtualenvs-in-project: true
        installer-parallel: true

    - name: Load cached venv
      id: cached-poetry-dependencies
      uses: actions/cache@v5
      with:
        path: .venv
        key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}

    - name: Install dependencies
      if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
      run: poetry install --no-interaction --no-root

    - name: Install library
      run: poetry install --no-interaction

    - name: Run tests
      run: |
        source .venv/bin/activate
        pytest tests/

    - name: Build package
      run: poetry build

    - name: Publish package distributions to PyPI
      uses: pypa/gh-action-pypi-publish@release/v1


================================================
FILE: .github/workflows/testing.yml
================================================
name: Testing package

on:
  push:
    branches: [ master, dev ]
  pull_request:
    branches: [ master, dev ]

jobs:
  build:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [ "3.13", "3.14" ]

    steps:
    - name: Checkout
      uses: actions/checkout@v6

    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v6
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install Poetry
      uses: snok/install-poetry@v1
      with:
        virtualenvs-create: true
        virtualenvs-in-project: true
        installer-parallel: true

    - name: Load cached venv
      id: cached-poetry-dependencies
      uses: actions/cache@v5
      with:
        path: .venv
        key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}

    - name: Install dependencies
      if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
      run: poetry install --no-interaction --no-root

    - name: Install library
      run: poetry install --no-interaction

    - name: Run tests
      run: |
        source .venv/bin/activate
        pytest tests/

    - name: Lint with ruff
      run: |
        pip install ruff
        ruff check .


================================================
FILE: .gitignore
================================================
# Hide sublime text stuff
*.sublime-project
*.sublime-workspace

# Hide vscode
.vscode
*.code-workspace

# Hide some OS X stuff
.DS_Store
.AppleDouble
.LSOverride
Icon

# Thumbnails
._*

.idea

# pytest
.cache
htmlcov

# GitHub Proposed Python stuff:
*.py[cod]

# C extensions
*.so

# Packages
*.egg
*.egg-info
dist
build
eggs
.eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox
htmlcov
nosetests.xml

# Translations
*.mo

# Mr Developer
.mr.developer.cfg
.project
.pydevproject

.python-version

# emacs auto backups
*~
*#
*.orig

# venv stuff
pyvenv.cfg
pip-selfcheck.json
venv
.venv
.Python
include

# vimmy stuff
*.swp
*.swo

ctags.tmp

# vagrant stuff
virtualization/vagrant/setup_done
virtualization/vagrant/.vagrant
virtualization/vagrant/config

# pytest
.pytest_cache

# share/man ignore
share

# ignored to make check_dirty not fail
travis_wait*
.mypy_cache

README.html
pip-wheel-metadata
.xprocess
results.xml


================================================
FILE: .pre-commit-config.yaml
================================================
repos:
  - repo: https://github.com/asottile/pyupgrade
    rev: v3.21.2
    hooks:
      - id: pyupgrade
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.10
    hooks:
      - id: ruff-format
        files: ^((homeassistant_cli|script|tests)/.+)?[^/]+\.py$
      - id: ruff
        args: [--fix]
        files: ^((homeassistant_cli|script|tests)/.+)?[^/]+\.py$
  - repo: https://github.com/codespell-project/codespell
    rev: v2.4.2
    hooks:
      - id: codespell
        args:
          - --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
          - --skip="./.*,*.json"
          - --quiet-level=2
        exclude_types: [json]
  - repo: https://github.com/PyCQA/bandit
    rev: 1.9.4
    hooks:
      - id: bandit
        args:
          - --quiet
          - --format=custom
          - --configfile=tests/bandit.yaml
        files: ^(homeassistant_cli|tests)/.+\.py$
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-executables-have-shebangs
        stages: [manual]
      - id: check-json
      - id: no-commit-to-branch
        args:
          #- --branch=dev
          - --branch=master
          - --branch=rc
      - id: trailing-whitespace
      - id: end-of-file-fixer


================================================
FILE: Dockerfile
================================================
FROM python:3.13-alpine
LABEL maintainer="Fabian Affolter <fabian@affolter-engineering.ch>"

WORKDIR /usr/src/app

COPY . .

RUN apk add --no-cache --virtual build-dependencies gcc musl-dev\
    &&  rm -rf /var/cache/apk/*

RUN pip3 install --upgrade pip; pip3 install --no-cache-dir -e .

ENTRYPOINT ["hass-cli"]


================================================
FILE: Dockerfile.armhf
================================================
# Python 3.11 with Alpine
FROM balenalib/armv7hf-alpine-python:3.11-3.15
LABEL maintainer="Fabian Affolter <fabian@affolter-engineering.ch>"

RUN [ "cross-build-start" ]

RUN apk add --no-cache --virtual build-dependencies gcc musl-dev\
    &&  rm -rf /var/cache/apk/*

WORKDIR /usr/src/app
COPY . .
RUN pip3 install --upgrade pip; pip3 install --no-cache-dir -e .

RUN [ "cross-build-end" ]

ENTRYPOINT ["hass-cli"]


================================================
FILE: LICENSE.md
================================================
Apache License
==============

_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_

### Terms and Conditions for use, reproduction, and distribution

#### 1. Definitions

“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.

“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.

“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.

“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.

“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.

“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.

“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).

“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.

“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”

“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.

#### 2. Grant of Copyright License

Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.

#### 3. Grant of Patent License

Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.

#### 4. Redistribution

You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:

* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.

You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.

#### 5. Submission of Contributions

Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.

#### 6. Trademarks

This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.

#### 7. Disclaimer of Warranty

Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.

#### 8. Limitation of Liability

In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.

#### 9. Accepting Warranty or Additional Liability

While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.

_END OF TERMS AND CONDITIONS_

### APPENDIX: How to apply the Apache License to your work

To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.

    Copyright [yyyy] [name of copyright owner]

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.


================================================
FILE: MANIFEST.in
================================================
include README.rst
include LICENSE.md
graft homeassistant_cli
graft tests
recursive-exclude * *.py[co]


================================================
FILE: README.rst
================================================
Home Assistant Command-line Interface (``hass-cli``)
====================================================

|Coverage| |License| |PyPI|

The Home Assistant Command-line interface (``hass-cli``) allows one to
work with a local or a remote `Home Assistant <https://home-assistant.io>`_
instance directly from the command-line.

.. image:: https://asciinema.org/a/216235.png
      :alt: hass-cli screencast
      :target: https://asciinema.org/a/216235?autoplay=1&speed=1


Installation
============

To use latest release:

.. code:: bash

    $ pip install homeassistant-cli

To use latest pre-release from ``dev`` branch:

.. code:: bash

   $ pip install git+https://github.com/home-assistant-ecosystem/home-assistant-cli@dev

The 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:

.. code:: bash

   $ sudo  dnf -y install home-assistant-cli

The community is providing support for macOS through `homebew <https://formulae.brew.sh/formula/homeassistant-cli#default>`_.

.. code:: bash

   $ brew install homeassistant-cli

Keep in mind that the available releases in the distribution could be out-dated.

``home-assistant-cli`` is also available for NixOS.

To use the tool on NixOS. Keep in mind that the latest release could only
be available in the ``unstable`` channel.

.. code:: bash

   $ nix-env -iA nixos.home-assistant-cli

Docker
-------

If you do not have a Python setup you can try use ``hass-cli`` via a container
using Docker.

.. code:: bash

   $ docker run homeassistant/home-assistant-cli


To make auto-completion and access environment work like other scripts you'll
need to create a script file to execute.

.. code:: bash

   $ curl https://raw.githubusercontent.com/home-assistant/home-assistant-cli/master/docker-hass-cli > hass-cli
   $ chmod +x hass-cli


Now put the ``hass-cli`` script into your path and you can use it like if you
had installed it via command line as long as you don't need file system access
(like for ``hass-cli template``).

Setup
======

To get started you'll need to have or generate a long lasting token format
on your Home Assistant profile page (e. g., http://homeassistant.local:8123/profile
then scroll down to "Long-Lived Access Tokens").

Then you can use ``--server`` and ``--token`` parameter on each call or as is
recommended setup ``HASS_SERVER`` and ``HASS_TOKEN`` environment variables.

.. code:: bash

    $ export HASS_SERVER=http://homeassistant.local:8123
    $ export HASS_TOKEN=<secret>


Remote API access
-----------------

For Home Assistant Operating System users, the `Remote API proxy <https://developers.home-assistant.io/docs/supervisor/development/#supervisor-api-access>`
add-on is needed. Keep in mind that this is not a feature for regular users as it allows access to
the Supervisor API. This is relevant for the ``ha`` commands in ``hass-cli``.

Install the add-on and start it. Once started you can get the Supervisor API key for the add-on via
the logs:

.. code:: text

    s6-rc: info: service legacy-services: starting
    s6-rc: info: service legacy-services successfully started
    Your API key is: 89400091.....d0897


Use ``--supervisor-token`` or the ``HASS_SUPERVISOR_TOKEN`` environment variable.

.. code:: bash

    $ export HASS_SUPERVISOR_TOKEN=<supervisor_secret>


Automatic completion
--------------------

Once that is enabled, run one of the following commands to enable
autocompletion for ``hass-cli`` commands.

.. code:: bash

  $ source <(_HASS_CLI_COMPLETE=bash_source hass-cli) # for bash
  $ source <(_HASS_CLI_COMPLETE=zsh_source hass-cli)  # for zsh
  $ eval (_HASS_CLI_COMPLETE=fish_source hass-cli)    # for fish


Usage
=====

Basic info
----------

Note: Below is listed **some** of the features, make sure to use ``--help`` and
autocompletion to learn more of the features as they become available.

Most commands returns a table version of what the Home Assistant API returns.
For example to get basic info about your Home Assistant server you use ``system``:

.. code:: bash

   $ hass-cli config release
   VERSION
   2026.4.1


If you prefer yaml you can use ``--output=yaml``:

.. code:: bash

    $ hass-cli --output=yaml config release
      -  2026.4.1


Backup
------

Backup can be created with command:

.. code:: bash

    $ hass-cli service list | grep backup
    $ hass-cli service call backup.create

States
------

To get list of states you use `state list`:

.. code:: bash

    $ hass-cli state list
    ENTITY                                                     DESCRIPTION                                     STATE
    zone.school                                                School                                          zoning
    zone.home                                                  Andersens                                       zoning
    sun.sun                                                    Sun                                             below_horizon
    camera.babymonitor                                         babymonitor                                     idle
    timer.timer_office_lights                                                                                  idle
    timer.timer_small_bathroom                                                                                 idle
    [...]


You can use ``--no-headers`` to suppress the header.

``--table-format`` let you select which table format you want. Default is
``simple`` but you can use any of the formats supported by https://pypi.org/project/tabulate/:
``plain``, ``simple``, ``github``, ``grid``, ``fancy_grid``, ``pipe``,
``orgtbl``, ``rst``, ``mediawiki``, ``html``, ``latex``, ``latex_raw``,
``latex_booktabs`` or ``tsv``

Finally, you can also via ``--columns`` control which data you want shown.
Each column has a name and a jsonpath. The default setup for entities are:

``--columns=ENTITY=entity_id,DESCRIPTION=attributes.friendly_name,STATE=state,CHANGED=last_changed``

If you for example just wanted the name and all attributes you could do:

.. code:: bash

   $ hass-cli --columns=ENTITY="entity_id,ATTRIBUTES=attributes[*]" state list zone
   ENTITY             ATTRIBUTES
   zone.school        {'friendly_name': 'School', 'hidden': True, 'icon': 'mdi:school', 'latitude': 7.011023, 'longitude': 16.858151, 'radius': 50.0}
   zone.unnamed_zone  {'friendly_name': 'Unnamed zone', 'hidden': True, 'icon': 'mdi:home', 'latitude': 37.006476, 'longitude': 2.861699, 'radius': 50.0}
   zone.home          {'friendly_name': 'Andersens', 'hidden': True, 'icon': 'mdi:home', 'latitude': 27.006476, 'longitude': 7.861699, 'radius': 100}

You can get more details about a state by using ``yaml`` or ``json`` output
format. In this example we use the shorthand of output: ``-o``:

.. code:: bash

    $ hass-cli -o yaml state get light.guestroom_light                                                                                                                                                                       ◼
    attributes:
      friendly_name: Guestroom Light
      supported_features: 61
    context:
      id: 84d52fe306ec4895948b546b492702a4
      user_id: null
    entity_id: light.guestroom_light
    last_changed: '2018-12-10T18:33:51.883238+00:00'
    last_updated: '2018-12-10T18:33:51.883238+00:00'
    state: 'off'

You can edit state via an editor:

.. code:: bash

    $ hass-cli state edit light.guestroom_light

This will open the current state in your favorite editor and any changes you
save will be used for an update.

You can also explicitly create/edit via the ``--json`` flag:

.. code:: bash

  $ hass-cli state edit sensor.test --json='{ "state":"off"}'

List possible services with or without a regular expression filter:

Services
--------

.. code:: bash

    $ hass-cli service list 'home.*toggle'
      DOMAIN         SERVICE    DESCRIPTION
      homeassistant  toggle     Generic service to toggle devices on/off...

For more details the YAML format is useful:

.. code:: bash

    $ hass-cli -o yaml service list homeassistant.toggle
    homeassistant:
      services:
        toggle:
          description: Generic service to toggle devices on/off under any domain. Same
            usage as the light.turn_on, switch.turn_on, etc. services.
          fields:
            entity_id:
              description: The entity_id of the device to toggle on/off.
              example: light.living_room

You can get history about one or more entities, here getting state changes for the last
50 minutes:

.. code:: bash

   $ hass-cli state history --since 50m light.kitchen_light_1 binary_sensor.presence_kitchen
     ENTITY                          DESCRIPTION      STATE    CHANGED
     binary_sensor.presence_kitchen  Kitchen Motion   off      2019-01-27T23:19:55.322474+00:00
     binary_sensor.presence_kitchen  Kitchen Motion   on       2019-01-27T23:21:44.015071+00:00
     binary_sensor.presence_kitchen  Kitchen Motion   off      2019-01-27T23:22:02.330566+00:00
     light.kitchen_light_1           Kitchen Light 1  on       2019-01-27T23:19:55.322474+00:00
     light.kitchen_light_1           Kitchen Light 1  off      2019-01-27T23:36:45.254266+00:00

The data is sorted by default as Home Assistant returns it, thus for history it is useful
to sort by a property:

.. code:: bash

   $ hass-cli --sort-by last_changed state history --since 50m  light.kitchen_light_1 binary_sensor.presence_kitchen
   ENTITY                          DESCRIPTION      STATE    CHANGED
   binary_sensor.presence_kitchen  Kitchen Motion   off      2019-01-27T23:18:00.717611+00:00
   light.kitchen_light_1           Kitchen Light 1  on       2019-01-27T23:18:00.717611+00:00
   binary_sensor.presence_kitchen  Kitchen Motion   on       2019-01-27T23:18:12.135015+00:00
   binary_sensor.presence_kitchen  Kitchen Motion   off      2019-01-27T23:18:30.417064+00:00
   light.kitchen_light_1           Kitchen Light 1  off      2019-01-27T23:36:45.254266+00:00

Note: the `--sort-by` argument is referring to the attribute in the underlying
``json``/``yaml`` NOT the column name. The advantage for this is that it can
be used for sorting on any property even if not included in the default output.

Areas and Device Registry
-------------------------

Since v0.87 of Home Assistant there is a notion of Areas in the Device registry. ``hass-cli`` lets
you list devices and areas and assign areas to devices.

Listing devices and areas works similar to list Entities.

.. code:: bash

   $ hass-cli device list
   ID                                NAME                           MODEL                            MANUFACTURER        AREA
   a3852c3c3ebd47d3acac195478ca6f8b  Basement stairs motion         SML001                           Philips             c6c962b892064a218e968fcaee7950c8
   880a944e74db4bb48ea3db6dd24af357  Basement Light 2               TRADFRI bulb GU10 WS 400lm       IKEA of Sweden      c6c962b892064a218e968fcaee7950c8
   657c3cc908594479aab819ff80d0c710  Office                         Hue white lamp                   Philips             None
   [...]

   $ hass-cli area list
   ID                                NAME
   295afc88012341ecb897cd12d3fbc6b4  Bathroom
   9e08d89203804d5db995c3d0d5dbd91b  Winter Garden
   8816ee92b7b84f54bbb30a68b877e739  Office
   [...]


You can create and delete areas:

.. code:: bash

   $ hass-cli area delete "Old Shed"
   -  id: 1
      type: result
      success: true
      result: success

   $ hass-cli area create "New Shed"
   -  id: 1
      type: result
      success: true
      result:
          area_id: cdd09a80f03a4cc59d2943053c0414c0
          name: New Shed

You can assign area to a specific device. Here the Kitchen
area gets assigned to device named "Cupboard Light".

.. code:: bash

   $ hass-cli device assign Kitchen "Cupboard Light"

Besides assigning individual devices you can assign in bulk:

.. code:: bash

   $ hass-cli device assign Kitchen --match "Kitchen Light"

The above line will assign Kitchen area to all devices with substring "Kitchen Light".

You can also combine individual and matched devices in one line:

.. code:: bash

   $ hass-cli device assign Kitchen --match "Kitchen Light" eab9930f8652408882cc8cb604651c60 Cupboard

Above will assign area named "Kitchen" to all devices having substring "Kitchen Light" and to
specific area with id "eab9930..." or named "Cupboard".

Events
------

You can subscribe and watch all or a specific event type using ``event watch``.

.. code:: bash

   $ hass-cli event watch

This will watch for all event types, you can limit to a specific event type
by specifying it as an argument:

.. code:: bash

   $ hass-cli event watch deconz_event


Home Assistant Operating System
-------------------------------

If you are using Home Assistant Operating System there are commands available
for you to interact with Home Assistant services/systems. This includes the
underlying services like the supervisor.

Check the Supervisor release you are running:

.. code:: bash

   $ hass-cli ha supervisor info
     result: ok
     data:
       version: 2026.03.3
        version_latest: 2026.03.3
       update_available: false
       channel: stable
    [...]

Check the Core release you are using at the moment:

.. code:: bash

   $ hass-cli ha core info
   result: ok
   data:
       version: 2026.4.1
       version_latest: 2026.4.1
       update_available: false
       machine: generic-x86-64
       [...]

Update Core to the latest available release:

.. code:: bash

   $ hass-cli ha core update


Other
-----

You can call services:

.. code:: bash

    $ hass-cli service call deconz.device_refresh

With arguments:

.. code:: bash

    $ hass-cli service call homeassistant.toggle --arguments entity_id=light.office_light


Open a map for your Home Assistant location:

.. code:: bash

    $ hass-cli map

Render templates server side:

.. code:: bash

    $ hass-cli template motionlight.yaml.j2 motiondata.yaml

Render templates client (local) side:

.. code:: bash

    $ hass-cli template --local lovelace-template.yaml


Auto-completion
###############

As described above you can use ``source <(hass-cli completion zsh)`` to
quickly and easy enable auto completion. If you do it from your ``.bashrc``
or ``.zshrc`` it's recommend to use the form below as that does not trigger
a run of ``hass-cli`` itself.

For zsh:

.. code:: bash

  eval "$(_HASS_CLI_COMPLETE=source_zsh hass-cli)"


For bash:

.. code:: bash

  eval "$(_HASS_CLI_COMPLETE=source hass-cli)"


Once enabled there is autocompletion for commands and for certain attributes like entities:

.. code:: bash

  $ hass-cli state get light.<TAB>                                                                                                                                                                    ⏎ ✱ ◼
  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
  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
  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
  [...]


Note: For this to work you'll need to have setup the following environment
variables if your Home Assistant installation is secured and not running on
localhost:8123:

.. code:: bash

   export HASS_SERVER=http://homeassistant.local:8123
   export HASS_TOKEN=eyJ0eXAiO-----------------------ed8mj0NP8


Help
####

.. code:: bash

   $ hass-cli --help
   Usage: hass-cli [OPTIONS] COMMAND [ARGS]...

   Command line interface for Home Assistant.

   Options:
   -l, --loglevel LVL              Either CRITICAL, ERROR, WARNING, INFO or
                                    DEBUG
   --version                       Show the version and exit.
   -s, --server TEXT               The server URL or `auto` for automatic
                                    detection. Can also be set with the
                                    environment variable HASS_SERVER.  [default:
                                    auto]
   --token TEXT                    The Bearer token for Home Assistant
                                    instance. Can also be set with the
                                    environment variable HASS_TOKEN.
   --supervisor-token TEXT         The Bearer token for Home Assistant
                                    supervisor. Can also be set with the
                                    environment variable HASS_SUPERVISOR_TOKEN.
   --password TEXT                 The API password for Home Assistant
                                    instance. Can also be set with the
                                    environment variable HASS_PASSWORD.
   --timeout INTEGER               Timeout for network operations.  [default:
                                    5]
   -o, --output [json|yaml|table|auto|ndjson]
                                    Output format.  [default: auto]
   -v, --verbose                   Enables verbose mode.
   -x                              Print backtraces when exception occurs.
   --cert TEXT                     Path to client certificate file (.pem) to
                                    use when connecting.
   --insecure                      Ignore SSL Certificates. Allow to connect to
                                    servers with self-signed certificates. Be
                                    careful!
   --debug                         Enables debug mode.
   --columns TEXT                  Custom columns key=value list. Example:
                                    ENTITY=entity_id,
                                    NAME=attributes.friendly_name
   --no-headers                    When printing tables don't use headers
                                    (default: print headers)
   --table-format TEXT             Which table format to use.
   --sort-by TEXT                  Sort table by the jsonpath expression.
                                    Example: last_changed
   --help                          Show this message and exit.

   Commands:
   area         Get info and operate on areas from Home Assistant...
   config       Get configuration from a Home Assistant instance.
   device       Get info and operate on devices from Home Assistant.
   discover     Discovery for the local network.
   entity       Get info on entities from Home Assistant.
   event        Interact with events.
   ha           Home Assistant Operating System commands.
   info         Show information about Home Assistant CLI.
   integration  Get info and operate on integrations (config entries) from...
   map          Show the location of the config or an entity on a map.
   raw          Call the raw API (advanced).
   service      Call and work with services.
   state        Get info on entity state from Home Assistant.
   system       System details and operations for Home Assistant.
   template     Render templates on server or locally.


Clone the git repository and

.. code:: bash

    $ pip3 install --editable .



Development
###########

Developing is (re)using as much as possible from
`Home Assistant development setup <https://developers.home-assistant.io/docs/en/development_environment.html>`_.

Recommended way to develop is to use virtual environment to ensure isolation
from rest of your system using the following steps:

Clone the git repository and do the following:

.. code:: bash

    $ python3 -m venv .
    $ source bin/activate
    $ script/setup


after this you should be able to edit the source code and running ``hass-cli``
directly:

.. code:: bash

    $ hass-cli

.. |License| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg
   :target: https://github.com/home-assistant/home-assistant-cli/blob/master/LICENSE
   :alt: License
.. |PyPI| image:: https://img.shields.io/pypi/v/homeassistant_cli.svg
   :target: https://pypi.org/project/homeassistant_cli/
   :alt: PyPI release
.. |Coverage| image:: https://coveralls.io/repos/github/home-assistant/home-assistant-cli/badge.svg?branch=dev
    :target: https://coveralls.io/github/home-assistant/home-assistant-cli?branch=dev
    :alt: Coveralls
.. |Docker| image:: https://img.shields.io/docker/pulls/homeassistant/home-assistant-cli.svg?style=flat
    :target: https://hub.docker.com/r/homeassistant/home-assistant-cli
    :alt: Docker


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Reporting a Vulnerability

If 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). 


================================================
FILE: docker-hass-cli
================================================

## Use the tag that best fits you
## dev contains last build from dev branch.
#TAG=dev
## latest is latest released build
TAG=latest
## You can also use a specific tag, see https://hub.docker.com/r/homeassistant/home-assistant-cli/tags
## for available ones.
##TAG=0.6.0

IMAGE=homeassistant/home-assistant-cli:$TAG

## The -e arguments passes in the environment variables needed for basic hass-cli setup (HASS_SERVER and HASS_TOKEN)
## and what's needed for auto completion (_HASS_CLI_COMPLETE, COMP_WORDS, COMP_CWORD)
## Be aware that while hass-cli runs via docker these variables values will be visible using `docker inspect`.
docker run -e HASS_TOKEN -e HASS_SERVER -e _HASS_CLI_COMPLETE -e COMP_WORDS -e COMP_CWORD -e _HASS_CLI_COMPLETE $IMAGE $*


================================================
FILE: homeassistant_cli/__init__.py
================================================
"""Init file for Home Assistant CLI (hass-cli)."""


================================================
FILE: homeassistant_cli/autocompletion.py
================================================
"""Details for the auto-completion."""

import os

from requests.exceptions import HTTPError

import homeassistant_cli.remote as api
from homeassistant_cli import const, hassconst
from homeassistant_cli.config import Configuration, resolve_server


def _init_ctx(ctx: Configuration) -> None:
    """Initialize ctx."""
    # ctx is incomplete thus need to 'hack' around it
    # see bug https://github.com/pallets/click/issues/942
    if not hasattr(ctx, "server"):
        ctx.server = os.environ.get("HASS_SERVER", const.AUTO_SERVER)

    if not hasattr(ctx, "token"):
        ctx.token = os.environ.get("HASS_TOKEN", os.environ.get("HASSIO_TOKEN", None))

    if not hasattr(ctx, "password"):
        ctx.password = os.environ.get("HASS_PASSWORD", None)

    if not hasattr(ctx, "timeout"):
        ctx.timeout = int(os.environ.get("HASS_TIMEOUT", str(const.DEFAULT_TIMEOUT)))

    if not hasattr(ctx, "insecure"):
        ctx.insecure = False

    if not hasattr(ctx, "session"):
        ctx.session = None

    if not hasattr(ctx, "cert"):
        ctx.cert = None

    if not hasattr(ctx, "resolved_server"):
        ctx.resolved_server = resolve_server(ctx)


def services(ctx: Configuration, args: list, incomplete: str) -> list[tuple[str, str]]:
    """Services."""
    _init_ctx(ctx)
    try:
        response = api.get_services(ctx)
    except HTTPError:
        response = []

    completions = []  # type: List[Tuple[str, str]]
    if response:
        for domain in response:
            domain_name = domain["domain"]
            servicesdict = domain["services"]

            for service in servicesdict:
                completions.append(
                    (
                        f"{domain_name}.{service}",
                        servicesdict[service]["description"],
                    )
                )

        completions.sort()

        return [c for c in completions if incomplete in c[0]]

    return completions


def entities(ctx: Configuration, args: list, incomplete: str) -> list[tuple[str, str]]:
    """Entities."""
    _init_ctx(ctx)
    try:
        response = api.get_states(ctx)
    except HTTPError:
        response = []

    completions = []  # type List[Tuple[str, str]]

    if response:
        for entity in response:
            friendly_name = entity["attributes"].get("friendly_name", "")
            completions.append((entity["entity_id"], friendly_name))

        completions.sort()

        return [c for c in completions if incomplete in c[0]]

    return completions


def events(ctx: Configuration, args: list, incomplete: str) -> list[tuple[str, str]]:
    """Events."""
    _init_ctx(ctx)
    try:
        response = api.get_events(ctx)
    except HTTPError:
        response = {}

    completions = []

    if response:
        for entity in response:
            completions.append((entity["event"], ""))  # type: ignore

        completions.sort()

        return [c for c in completions if incomplete in c[0]]

    return completions


def table_formats(
    ctx: Configuration, args: list, incomplete: str
) -> list[tuple[str, str]]:
    """Table Formats."""
    _init_ctx(ctx)

    completions = [
        ("plain", "Plain tables, no pseudo-graphics to draw lines"),
        ("simple", "Simple table with --- as header/footer (default)"),
        ("github", "Github flavored Markdown table"),
        ("grid", "Formatted as Emacs 'table.el' package"),
        ("fancy_grid", "Draws a fancy grid using box-drawing characters"),
        ("pipe", "PHP Markdown Extra"),
        ("orgtbl", "org-mode table"),
        ("jira", "Atlassian Jira Markup"),
        ("presto", "Formatted as PrestoDB CLI"),
        ("psql", "Formatted as Postgres psql CLI"),
        ("rst", "reStructuredText"),
        ("mediawiki", "Media Wiki as used in Wikipedia"),
        ("moinmoin", "MoinMoin Wiki"),
        ("youtrack", "Youtrack format"),
        ("html", "HTML Markup"),
        ("latex", "LaTeX markup, replacing special characters"),
        ("latex_raw", "LaTeX markup, no replacing of special characters"),
        (
            "latex_booktabs",
            "LaTex markup using spacing and style from `booktabs",
        ),
        ("textile", "Textile"),
        ("tsv", "Tab Separated Values"),
    ]

    completions.sort()

    return [c for c in completions if incomplete in c[0]]


def api_methods(
    ctx: Configuration, args: list, incomplete: str
) -> list[tuple[str, str]]:
    """Auto completion for methods."""
    _init_ctx(ctx)

    from inspect import getmembers

    completions = []
    for name, value in getmembers(hassconst):
        if name.startswith("URL_API_"):
            completions.append((value, name[len("URL_API_") :]))

    completions.sort()

    return [c for c in completions if incomplete in c[0]]


def wsapi_methods(
    ctx: Configuration, args: list, incomplete: str
) -> list[tuple[str, str]]:
    """Auto completion for websocket methods."""
    _init_ctx(ctx)

    from inspect import getmembers

    completions = []
    for name, value in getmembers(hassconst):
        if name.startswith("WS_TYPE_"):
            completions.append((value, name[len("WS_TYPE_") :]))

    completions.sort()

    return [c for c in completions if incomplete in c[0]]


def _quote_if_needed(value: str) -> str:
    """Add quotes if needed."""
    if value and " " in value:
        return f'"{value}"'
    return value


def areas(ctx: Configuration, args: list, incomplete: str) -> list[tuple[str, str]]:
    """Areas."""
    _init_ctx(ctx)
    all_areas = api.get_areas(ctx)

    completions = []  # type: List[Tuple[str, str]]

    if all_areas:
        for area in all_areas:
            completions.append((_quote_if_needed(area["name"]), area["area_id"]))

        completions.sort()

        return [c for c in completions if incomplete in c[0]]

    return completions


================================================
FILE: homeassistant_cli/cli.py
================================================
"""Home Assistant CLI (hass-cli)."""

import logging
import os
import sys
from typing import cast

import click
import click_log
from click.core import Command, Context, Group

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.const as const
from homeassistant_cli.config import Configuration
from homeassistant_cli.helper import debug_requests_on, to_tuples

click_log.basic_config()

_LOGGER = logging.getLogger(__name__)

CONTEXT_SETTINGS = dict(auto_envvar_prefix="HOMEASSISTANT")

pass_context = click.make_pass_decorator(  # pylint: disable=invalid-name
    Configuration, ensure=True
)


def run() -> None:
    """Run entry point.

    Wraps click for full control over exception handling in Click.
    """
    # A hack to see if exception details should be printed.
    exceptionflags = ["-x"]
    verbose = [c for c in exceptionflags if c in sys.argv]

    try:
        # Could use cli.invoke here to use the just created context
        # but then shell completion will not work. Thus calling
        # standalone mode to keep that working.
        result = cli.main(standalone_mode=False)
        if isinstance(result, int):
            sys.exit(result)

    # Exception handling below is done to use logger
    # and mimic as close as possible what click would
    # do normally in its main()
    except click.ClickException as ex:
        ex.show()  # let Click handle its own errors
        sys.exit(ex.exit_code)
    except click.Abort:
        _LOGGER.critical("Aborted!")
        sys.exit(1)
    except Exception as ex:  # pylint: disable=broad-except
        if verbose:
            _LOGGER.exception(ex)
        else:
            _LOGGER.error("%s: %s", type(ex).__name__, ex)
            _LOGGER.info(
                "Run with %s to see full exception information",
                " or ".join(exceptionflags),
            )
        sys.exit(1)


class HomeAssistantCli(click.MultiCommand):
    """The Home Assistant Command-line."""

    def list_commands(self, ctx: Context) -> list[str]:
        """List all command available as plugin."""
        cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "plugins"))

        commands = []
        for filename in os.listdir(cmd_folder):
            if filename.endswith(".py") and not filename.startswith("__"):
                commands.append(filename[:-3])
        commands.sort()

        return commands

    def get_command(self, ctx: Context, cmd_name: str) -> Group | Command | None:
        """Import the commands of the plugins."""
        try:
            mod = __import__(
                f"{const.PACKAGE_NAME}.plugins.{cmd_name}",
                {},
                {},
                ["cli"],
            )
        except ImportError:
            # todo: print out issue of loading plugins?
            return None
        return cast(Group | Command, mod.cli)  # type: ignore


def _default_token() -> str | None:
    """Handle the token provided as env variable."""
    return os.environ.get("HASS_TOKEN", os.environ.get("HASSIO_TOKEN", None))


@click.command(cls=HomeAssistantCli, context_settings=CONTEXT_SETTINGS)
@click_log.simple_verbosity_option(logging.getLogger(), "--loglevel", "-l")
@click.version_option(const.__version__)
@click.option(
    "--server",
    "-s",
    help=(
        "The server URL or `auto` for automatic detection. Can also be set "
        "with the environment variable HASS_SERVER."
    ),
    default="auto",
    show_default=True,
    envvar="HASS_SERVER",
)
@click.option(
    "--token",
    default=_default_token,
    help=(
        "The Bearer token for Home Assistant instance. Can also be set with "
        "the environment variable HASS_TOKEN."
    ),
    envvar="HASS_TOKEN",
)
@click.option(
    "--supervisor-token",
    default=_default_token,
    help=(
        "The Bearer token for Home Assistant supervisor. Can also be set with "
        "the environment variable HASS_SUPERVISOR_TOKEN."
    ),
    envvar="HASS_SUPERVISOR_TOKEN",
)
@click.option(
    "--password",
    default=None,
    help=(
        "The API password for Home Assistant instance. Can also be set with "
        "the environment variable HASS_PASSWORD."
    ),
    envvar="HASS_PASSWORD",
)
@click.option(
    "--timeout",
    help="Timeout for network operations.",
    default=const.DEFAULT_TIMEOUT,
    show_default=True,
)
@click.option(
    "--output",
    "-o",
    help="Output format.",
    type=click.Choice(["json", "yaml", "table", "auto", "ndjson"]),
    default="auto",
    show_default=True,
)
@click.option(
    "-v",
    "--verbose",
    is_flag=True,
    default=False,
    help="Enables verbose mode.",
)
@click.option(
    "-x",
    "showexceptions",
    default=False,
    is_flag=True,
    help="Print backtraces when exception occurs.",
)
@click.option(
    "--cert",
    default=None,
    envvar="HASS_CERT",
    help="Path to client certificate file (.pem) to use when connecting.",
)
@click.option(
    "--insecure",
    is_flag=True,
    default=False,
    help=(
        "Ignore SSL Certificates."
        " Allow to connect to servers with self-signed certificates."
        " Be careful!"
    ),
)
@click.option("--debug", is_flag=True, default=False, help="Enables debug mode.")
@click.option(
    "--columns",
    default=None,
    help=(
        "Custom columns key=value list."
        " Example: ENTITY=entity_id, NAME=attributes.friendly_name"
    ),
)
@click.option(
    "--no-headers",
    default=False,
    is_flag=True,
    help="When printing tables don't use headers (default: print headers)",
)
@click.option(
    "--table-format",
    default="plain",
    help="Which table format to use.",
    shell_complete=autocompletion.table_formats,
)
@click.option(
    "--sort-by",
    default=None,
    help="Sort table by the jsonpath expression. Example: last_changed",
)
@pass_context
def cli(
    ctx: Configuration,
    verbose: bool,
    server: str,
    token: str | None,
    supervisor_token: str | None,
    password: str | None,
    output: str,
    timeout: int,
    debug: bool,
    insecure: bool,
    showexceptions: bool,
    cert: str,
    columns: str,
    no_headers: bool,
    table_format: str,
    sort_by: str | None,
) -> None:
    """Command line interface for Home Assistant."""
    ctx.verbose = verbose
    ctx.server = server
    ctx.token = token
    ctx.supervisor_token = supervisor_token
    ctx.password = password
    ctx.timeout = timeout
    ctx.output = output
    ctx.debug = debug
    ctx.insecure = insecure
    ctx.showexceptions = showexceptions
    ctx.cert = cert
    ctx.columns = to_tuples(columns)
    ctx.no_headers = no_headers
    ctx.table_format = table_format
    ctx.sort_by = sort_by  # type: ignore

    _LOGGER.debug("Using settings: %s", ctx)

    if debug:
        debug_requests_on()


================================================
FILE: homeassistant_cli/config.py
================================================
"""Configuration for Home Assistant CLI (hass-cli)."""

import logging
import os
import sys
from typing import Any, Dict, List, Optional, Tuple, cast

import click
import zeroconf
from ruamel.yaml import YAML

import homeassistant_cli.const as const
import homeassistant_cli.yaml as yaml

_LOGGING = logging.getLogger(__name__)


class _ZeroconfListener:
    """Representation of the Zeroconf listener."""

    def __init__(self) -> None:
        """Initialize the listener."""
        self.services = {}  # type: Dict[str, zeroconf.ServiceInfo]

    def remove_service(
        self, _zeroconf: zeroconf.Zeroconf, _type: str, name: str
    ) -> None:
        """Remove service."""
        self.services[name] = None

    def add_service(self, _zeroconf: zeroconf.Zeroconf, _type: str, name: str) -> None:
        """Add service."""
        self.services[name] = _zeroconf.get_service_info(_type, name)

    def update_service(
        self, _zeroconf: zeroconf.Zeroconf, _type: str, name: str
    ) -> None:
        """Update service."""
        self.services[name] = _zeroconf.get_service_info(_type, name)


def _locate_ha() -> str | None:
    """Locate the Home Assistant instance."""
    _zeroconf = zeroconf.Zeroconf(interfaces=zeroconf.InterfaceChoice.Default)
    listener = _ZeroconfListener()
    zeroconf.ServiceBrowser(_zeroconf, "_home-assistant._tcp.local.", listener)
    try:
        import time

        retries = 0
        while not listener.services and retries < 5:
            _LOGGING.info("Trying to locate Home Assistant on local network...")
            time.sleep(0.5)
            retries = retries + 1
    finally:
        _zeroconf.close()

    if listener.services:
        if len(listener.services) > 1:
            _LOGGING.warning(
                f"Found multiple Home Assistant instances at "
                f"{', '.join(listener.services)}"
            )
            _LOGGING.warning("Use --server to explicitly specify one.")
            return None

        _, service = listener.services.popitem()
        base_url = service.properties[b"base_url"].decode("utf-8")
        _LOGGING.info(f"Found and using {base_url} as server")
        return cast(str, base_url)

    _LOGGING.warning("Found no Home Assistant on local network. Using defaults")
    return None


def resolve_server(ctx: Any) -> str:
    """Resolve server if not already done.

    if server is `auto` try and resolve it
    """
    # Work-around for bug in click that hands out non-Configuration context objects
    if not hasattr(ctx, "resolved_server"):
        ctx.resolved_server = None

    if not ctx.resolved_server:
        if ctx.server == "auto":
            if "HASSIO_TOKEN" in os.environ and "HASS_TOKEN" not in os.environ:
                ctx.resolved_server = const.DEFAULT_SERVER_MDNS
            else:
                if not ctx.resolved_server and "pytest" in sys.modules:
                    ctx.resolved_server = const.DEFAULT_SERVER
                else:
                    ctx.resolved_server = _locate_ha()
                    if not ctx.resolved_server:
                        sys.exit(3)
        else:
            ctx.resolved_server = ctx.server

        if not ctx.resolved_server:
            ctx.resolved_server = const.DEFAULT_SERVER

    return cast(str, ctx.resolved_server)


def set_supervisor_server(ctx: Any) -> str:
    """Derive the supervisor server URL from the main server URL."""
    if not hasattr(ctx, "supervisor_server"):
        ctx.supervisor_server = None

    if not ctx.supervisor_server:
        if ctx.server and ctx.server != "auto":
            ctx.supervisor_server = ctx.server.rsplit(":", 1)[0]
        else:
            # Ensure resolved_server is set first
            resolved = resolve_server(ctx)
            ctx.supervisor_server = resolved.rsplit(":", 1)[0]

    return cast(str, ctx.supervisor_server)


class Configuration:
    """The configuration context for the Home Assistant CLI."""

    def __init__(self) -> None:
        """Initialize the configuration."""
        self.verbose = False  # type: bool
        self.server = const.AUTO_SERVER  # type: str
        self.resolved_server = None  # type: Optional[str]
        self.supervisor_server = None  # type: Optional[str]
        self.output = const.DEFAULT_OUTPUT  # type: str
        self.token = None  # type: Optional[str]
        self.supervisor_token = None  # type: Optional[str]
        self.password = None  # type: Optional[str]
        self.insecure = False  # type: bool
        self.timeout = const.DEFAULT_TIMEOUT  # type: int
        self.debug = False  # type: bool
        self.showexceptions = False  # type: bool
        self.session = None  # type: Optional[Session]
        self.cert = None  # type: Optional[str]
        self.columns = None  # type: Optional[List[Tuple[str, str]]]
        self.no_headers = False
        self.table_format = "plain"
        self.sort_by = None

    def echo(self, msg: str, *args: Any | None) -> None:
        """Put content message to stdout."""
        self.log(msg, *args)

    def log(  # pylint: disable=no-self-use
        self, msg: str, *args: str | None
    ) -> None:  # pylint: disable=no-self-use
        """Log a message to stdout."""
        if args:
            msg %= args
        click.echo(msg, file=sys.stdout)

    def vlog(self, msg: str, *args: str | None) -> None:
        """Log a message only if verbose is enabled."""
        if self.verbose:
            self.log(msg, *args)

    def __repr__(self) -> str:
        """Return the representation of the Configuration."""
        view = {
            "server": self.server,
            "access-token": "yes" if self.token is not None else "no",
            "api-password": "yes" if self.password is not None else "no",
            "insecure": self.insecure,
            "output": self.output,
            "verbose": self.verbose,
        }

        print("-------------------------------")
        print(view)

        return f"<Configuration({view})"

    def resolve_server(self) -> str:
        """Return resolved server (after resolving if needed)."""
        return resolve_server(self)

    def set_supervisor_server(self) -> str:
        """Return supervisor server."""
        return set_supervisor_server(self)

    def auto_output(self, auto_output: str) -> str:
        """Configure output format."""
        if self.output == "auto":
            if auto_output == "data":
                auto_output = const.DEFAULT_DATAOUTPUT
            _LOGGING.debug("Setting auto-output to: %s", auto_output)
            self.output = auto_output
        return self.output

    def yaml(self) -> YAML:
        """Create default yaml parser."""
        if self:
            yaml.yaml()
        return yaml.yaml()

    def yamlload(self, source: str) -> Any:
        """Load YAML from source."""
        return self.yaml().load(source)

    def yamldump(self, source: Any) -> str:
        """Dump dictionary to YAML string."""
        return cast(str, yaml.dumpyaml(self.yaml(), source))


================================================
FILE: homeassistant_cli/const.py
================================================
"""Constants used by Home Assistant CLI (hass-cli)."""

PACKAGE_NAME = "homeassistant_cli"

__version__ = "1.0.1"

AUTO_SERVER = "auto"
DEFAULT_SERVER = "http://localhost:8123"
DEFAULT_SERVER_MDNS = "http://homeassistant.local:8123"
DEFAULT_TIMEOUT = 5
DEFAULT_OUTPUT = "json"  # TODO: Have default be human table relevant output

DEFAULT_DATAOUTPUT = "yaml"

COLUMNS_DEFAULT = [("ALL", "$")]
COLUMNS_ENTITIES = [
    ("ENTITY", "entity_id"),
    ("DESCRIPTION", "attributes.friendly_name"),
    ("STATE", "state"),
    ("CHANGED", "last_changed"),
]
COLUMNS_SERVICES = [("DOMAIN", "domain"), ("SERVICE", "domain.services[*]")]


================================================
FILE: homeassistant_cli/exceptions.py
================================================
"""The exceptions used by Home Assistant CLI."""


class HomeAssistantCliError(Exception):
    """General Home Assistant CLI exception occurred."""


class UnsafeTemplateError(HomeAssistantCliError):
    """Template contains unsafe operations."""


================================================
FILE: homeassistant_cli/hassconst.py
================================================
"""Constants used by Home Assistant components.

Copy of recent homeassistant.const to make hass-cli run
without installing Home Assistant itself.
"""
# Home Assistant WS constants

# Websocket API
WS_TYPE_DEVICE_REGISTRY_LIST = "config/device_registry/list"
WS_TYPE_AREA_REGISTRY_LIST = "config/area_registry/list"
WS_TYPE_AREA_REGISTRY_CREATE = "config/area_registry/create"
WS_TYPE_AREA_REGISTRY_DELETE = "config/area_registry/delete"
WS_TYPE_AREA_REGISTRY_UPDATE = "config/area_registry/update"
WS_TYPE_DEVICE_REGISTRY_UPDATE = "config/device_registry/update"
WS_TYPE_ENTITY_REGISTRY_LIST = "config/entity_registry/list"
WS_TYPE_ENTITY_REGISTRY_GET = "config/entity_registry/get"
WS_TYPE_ENTITY_REGISTRY_UPDATE = "config/entity_registry/update"
WS_TYPE_ENTITY_REGISTRY_REMOVE = "config/entity_registry/remove"

# Config entries (integrations)
WS_TYPE_CONFIG_ENTRIES_GET = "config_entries/get"
WS_TYPE_CONFIG_ENTRIES_GET_SINGLE = "config_entries/get_single"
WS_TYPE_CONFIG_ENTRIES_UPDATE = "config_entries/update"
WS_TYPE_CONFIG_ENTRIES_DISABLE = "config_entries/disable"

###############################################################################
# Home Assistant constants

# Format for platform files
PLATFORM_FORMAT = "{platform}.{domain}"

# Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = "*"

# Entity target all constant
ENTITY_MATCH_NONE = "none"
ENTITY_MATCH_ALL = "all"

# If no name is specified
DEVICE_DEFAULT_NAME = "Unnamed Device"

# Sun events
SUN_EVENT_SUNSET = "sunset"
SUN_EVENT_SUNRISE = "sunrise"

# #### CONFIG ####
CONF_ABOVE = "above"
CONF_ACCESS_TOKEN = "access_token"
CONF_ADDRESS = "address"
CONF_AFTER = "after"
CONF_ALIAS = "alias"
CONF_ALLOWLIST_EXTERNAL_URLS = "allowlist_external_urls"
CONF_API_KEY = "api_key"
CONF_API_TOKEN = "api_token"
CONF_API_VERSION = "api_version"
CONF_ARMING_TIME = "arming_time"
CONF_AT = "at"
CONF_ATTRIBUTE = "attribute"
CONF_AUTH_MFA_MODULES = "auth_mfa_modules"
CONF_AUTH_PROVIDERS = "auth_providers"
CONF_AUTHENTICATION = "authentication"
CONF_BASE = "base"
CONF_BEFORE = "before"
CONF_BELOW = "below"
CONF_BINARY_SENSORS = "binary_sensors"
CONF_BRIGHTNESS = "brightness"
CONF_BROADCAST_ADDRESS = "broadcast_address"
CONF_BROADCAST_PORT = "broadcast_port"
CONF_CHOOSE = "choose"
CONF_CLIENT_ID = "client_id"
CONF_CLIENT_SECRET = "client_secret"
CONF_CODE = "code"
CONF_COLOR_TEMP = "color_temp"
CONF_COMMAND = "command"
CONF_COMMAND_CLOSE = "command_close"
CONF_COMMAND_OFF = "command_off"
CONF_COMMAND_ON = "command_on"
CONF_COMMAND_OPEN = "command_open"
CONF_COMMAND_STATE = "command_state"
CONF_COMMAND_STOP = "command_stop"
CONF_CONDITION = "condition"
CONF_CONDITIONS = "conditions"
CONF_CONTINUE_ON_TIMEOUT = "continue_on_timeout"
CONF_COUNT = "count"
CONF_COVERS = "covers"
CONF_CURRENCY = "currency"
CONF_CUSTOMIZE = "customize"
CONF_CUSTOMIZE_DOMAIN = "customize_domain"
CONF_CUSTOMIZE_GLOB = "customize_glob"
CONF_DEFAULT = "default"
CONF_DELAY = "delay"
CONF_DELAY_TIME = "delay_time"
CONF_DESCRIPTION = "description"
CONF_DEVICE = "device"
CONF_DEVICES = "devices"
CONF_DEVICE_CLASS = "device_class"
CONF_DEVICE_ID = "device_id"
CONF_DISARM_AFTER_TRIGGER = "disarm_after_trigger"
CONF_DISCOVERY = "discovery"
CONF_DISKS = "disks"
CONF_DISPLAY_CURRENCY = "display_currency"
CONF_DISPLAY_OPTIONS = "display_options"
CONF_DOMAIN = "domain"
CONF_DOMAINS = "domains"
CONF_EFFECT = "effect"
CONF_ELEVATION = "elevation"
CONF_EMAIL = "email"
CONF_ENTITIES = "entities"
CONF_ENTITY_ID = "entity_id"
CONF_ENTITY_NAMESPACE = "entity_namespace"
CONF_ENTITY_PICTURE_TEMPLATE = "entity_picture_template"
CONF_EVENT = "event"
CONF_EVENT_DATA = "event_data"
CONF_EVENT_DATA_TEMPLATE = "event_data_template"
CONF_EXCLUDE = "exclude"
CONF_EXTERNAL_URL = "external_url"
CONF_FILENAME = "filename"
CONF_FILE_PATH = "file_path"
CONF_FOR = "for"
CONF_FORCE_UPDATE = "force_update"
CONF_FRIENDLY_NAME = "friendly_name"
CONF_FRIENDLY_NAME_TEMPLATE = "friendly_name_template"
CONF_HEADERS = "headers"
CONF_HOST = "host"
CONF_HOSTS = "hosts"
CONF_HS = "hs"
CONF_ICON = "icon"
CONF_ICON_TEMPLATE = "icon_template"
CONF_ID = "id"
CONF_INCLUDE = "include"
CONF_INTERNAL_URL = "internal_url"
CONF_IP_ADDRESS = "ip_address"
CONF_LATITUDE = "latitude"
CONF_LEGACY_TEMPLATES = "legacy_templates"
CONF_LIGHTS = "lights"
CONF_LONGITUDE = "longitude"
CONF_MAC = "mac"
CONF_MAXIMUM = "maximum"
CONF_MEDIA_DIRS = "media_dirs"
CONF_METHOD = "method"
CONF_MINIMUM = "minimum"
CONF_MODE = "mode"
CONF_MONITORED_CONDITIONS = "monitored_conditions"
CONF_MONITORED_VARIABLES = "monitored_variables"
CONF_NAME = "name"
CONF_OFFSET = "offset"
CONF_OPTIMISTIC = "optimistic"
CONF_PACKAGES = "packages"
CONF_PARAMS = "params"
CONF_PASSWORD = "password"
CONF_PATH = "path"
CONF_PAYLOAD = "payload"
CONF_PAYLOAD_OFF = "payload_off"
CONF_PAYLOAD_ON = "payload_on"
CONF_PENDING_TIME = "pending_time"
CONF_PIN = "pin"
CONF_PLATFORM = "platform"
CONF_PORT = "port"
CONF_PREFIX = "prefix"
CONF_PROFILE_NAME = "profile_name"
CONF_PROTOCOL = "protocol"
CONF_PROXY_SSL = "proxy_ssl"
CONF_QUOTE = "quote"
CONF_RADIUS = "radius"
CONF_RECIPIENT = "recipient"
CONF_REGION = "region"
CONF_REPEAT = "repeat"
CONF_RESOURCE = "resource"
CONF_RESOURCES = "resources"
CONF_RESOURCE_TEMPLATE = "resource_template"
CONF_RGB = "rgb"
CONF_ROOM = "room"
CONF_SCAN_INTERVAL = "scan_interval"
CONF_SCENE = "scene"
CONF_SELECTOR = "selector"
CONF_SENDER = "sender"
CONF_SENSORS = "sensors"
CONF_SENSOR_TYPE = "sensor_type"
CONF_SEQUENCE = "sequence"
CONF_SERVICE = "service"
CONF_SERVICE_DATA = "data"
CONF_SERVICE_TEMPLATE = "service_template"
CONF_SHOW_ON_MAP = "show_on_map"
CONF_SLAVE = "slave"
CONF_SOURCE = "source"
CONF_SSL = "ssl"
CONF_STATE = "state"
CONF_STATE_TEMPLATE = "state_template"
CONF_STRUCTURE = "structure"
CONF_SWITCHES = "switches"
CONF_TARGET = "target"
CONF_TEMPERATURE_UNIT = "temperature_unit"
CONF_TIMEOUT = "timeout"
CONF_TIME_ZONE = "time_zone"
CONF_TOKEN = "token"
CONF_TRIGGER_TIME = "trigger_time"
CONF_TTL = "ttl"
CONF_TYPE = "type"
CONF_UNIQUE_ID = "unique_id"
CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement"
CONF_UNIT_SYSTEM = "unit_system"
CONF_UNTIL = "until"
CONF_URL = "url"
CONF_USERNAME = "username"
CONF_VALUE_TEMPLATE = "value_template"
CONF_VARIABLES = "variables"
CONF_VERIFY_SSL = "verify_ssl"
CONF_WAIT_FOR_TRIGGER = "wait_for_trigger"
CONF_WAIT_TEMPLATE = "wait_template"
CONF_WEBHOOK_ID = "webhook_id"
CONF_WEEKDAY = "weekday"
CONF_WHILE = "while"
CONF_WHITELIST = "whitelist"
CONF_ALLOWLIST_EXTERNAL_DIRS = "allowlist_external_dirs"
LEGACY_CONF_WHITELIST_EXTERNAL_DIRS = "whitelist_external_dirs"
CONF_WHITE_VALUE = "white_value"
CONF_XY = "xy"
CONF_ZONE = "zone"

# #### EVENTS ####
EVENT_CALL_SERVICE = "call_service"
EVENT_COMPONENT_LOADED = "component_loaded"
EVENT_CORE_CONFIG_UPDATE = "core_config_updated"
EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close"
EVENT_HOMEASSISTANT_START = "homeassistant_start"
EVENT_HOMEASSISTANT_STARTED = "homeassistant_started"
EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
EVENT_HOMEASSISTANT_FINAL_WRITE = "homeassistant_final_write"
EVENT_LOGBOOK_ENTRY = "logbook_entry"
EVENT_SERVICE_REGISTERED = "service_registered"
EVENT_SERVICE_REMOVED = "service_removed"
EVENT_STATE_CHANGED = "state_changed"
EVENT_THEMES_UPDATED = "themes_updated"
EVENT_TIMER_OUT_OF_SYNC = "timer_out_of_sync"
EVENT_TIME_CHANGED = "time_changed"


# #### DEVICE CLASSES ####
DEVICE_CLASS_BATTERY = "battery"
DEVICE_CLASS_CO = "carbon_monoxide"
DEVICE_CLASS_CO2 = "carbon_dioxide"
DEVICE_CLASS_HUMIDITY = "humidity"
DEVICE_CLASS_ILLUMINANCE = "illuminance"
DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength"
DEVICE_CLASS_TEMPERATURE = "temperature"
DEVICE_CLASS_TIMESTAMP = "timestamp"
DEVICE_CLASS_PRESSURE = "pressure"
DEVICE_CLASS_POWER = "power"
DEVICE_CLASS_CURRENT = "current"
DEVICE_CLASS_ENERGY = "energy"
DEVICE_CLASS_POWER_FACTOR = "power_factor"
DEVICE_CLASS_VOLTAGE = "voltage"

# #### STATES ####
STATE_ON = "on"
STATE_OFF = "off"
STATE_HOME = "home"
STATE_NOT_HOME = "not_home"
STATE_UNKNOWN = "unknown"
STATE_OPEN = "open"
STATE_OPENING = "opening"
STATE_CLOSED = "closed"
STATE_CLOSING = "closing"
STATE_PLAYING = "playing"
STATE_PAUSED = "paused"
STATE_IDLE = "idle"
STATE_STANDBY = "standby"
STATE_ALARM_DISARMED = "disarmed"
STATE_ALARM_ARMED_HOME = "armed_home"
STATE_ALARM_ARMED_AWAY = "armed_away"
STATE_ALARM_ARMED_NIGHT = "armed_night"
STATE_ALARM_ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
STATE_ALARM_PENDING = "pending"
STATE_ALARM_ARMING = "arming"
STATE_ALARM_DISARMING = "disarming"
STATE_ALARM_TRIGGERED = "triggered"
STATE_LOCKED = "locked"
STATE_UNLOCKED = "unlocked"
STATE_UNAVAILABLE = "unavailable"
STATE_OK = "ok"
STATE_PROBLEM = "problem"

# #### STATE AND EVENT ATTRIBUTES ####
# Attribution
ATTR_ATTRIBUTION = "attribution"

# Credentials
ATTR_CREDENTIALS = "credentials"

# Contains time-related attributes
ATTR_NOW = "now"
ATTR_DATE = "date"
ATTR_TIME = "time"
ATTR_SECONDS = "seconds"

# Contains domain, service for a SERVICE_CALL event
ATTR_DOMAIN = "domain"
ATTR_SERVICE = "service"
ATTR_SERVICE_DATA = "service_data"

# IDs
ATTR_ID = "id"

# Name
ATTR_NAME = "name"

# Contains one string or a list of strings, each being an entity id
ATTR_ENTITY_ID = "entity_id"

# Contains one string or a list of strings, each being an area id
ATTR_AREA_ID = "area_id"

# Contains one string, the device ID
ATTR_DEVICE_ID = "device_id"

# String with a friendly name for the entity
ATTR_FRIENDLY_NAME = "friendly_name"

# A picture to represent entity
ATTR_ENTITY_PICTURE = "entity_picture"

# Icon to use in the frontend
ATTR_ICON = "icon"

# The unit of measurement if applicable
ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement"

CONF_UNIT_SYSTEM_METRIC: str = "metric"
CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial"

# Electrical attributes
ATTR_VOLTAGE = "voltage"

# Location of the device/sensor
ATTR_LOCATION = "location"

ATTR_MODE = "mode"

ATTR_BATTERY_CHARGING = "battery_charging"
ATTR_BATTERY_LEVEL = "battery_level"
ATTR_WAKEUP = "wake_up_interval"

# For devices which support a code attribute
ATTR_CODE = "code"
ATTR_CODE_FORMAT = "code_format"

# For calling a device specific command
ATTR_COMMAND = "command"

# For devices which support an armed state
ATTR_ARMED = "device_armed"

# For devices which support a locked state
ATTR_LOCKED = "locked"

# For sensors that support 'tripping', eg. motion and door sensors
ATTR_TRIPPED = "device_tripped"

# For sensors that support 'tripping' this holds the most recent
# time the device was tripped
ATTR_LAST_TRIP_TIME = "last_tripped_time"

# For all entity's, this hold whether or not it should be hidden
ATTR_HIDDEN = "hidden"

# Location of the entity
ATTR_LATITUDE = "latitude"
ATTR_LONGITUDE = "longitude"

# Accuracy of location in meters
ATTR_GPS_ACCURACY = "gps_accuracy"

# If state is assumed
ATTR_ASSUMED_STATE = "assumed_state"
ATTR_STATE = "state"

ATTR_EDITABLE = "editable"
ATTR_OPTION = "option"

# The entity has been restored with restore state
ATTR_RESTORED = "restored"

# Bitfield of supported component features for the entity
ATTR_SUPPORTED_FEATURES = "supported_features"

# Class of device within its domain
ATTR_DEVICE_CLASS = "device_class"

# Temperature attribute
ATTR_TEMPERATURE = "temperature"

# #### UNITS OF MEASUREMENT ####
# Power units
POWER_WATT = "W"
POWER_KILO_WATT = "kW"

# Voltage units
VOLT = "V"

# Energy units
ENERGY_WATT_HOUR = "Wh"
ENERGY_KILO_WATT_HOUR = "kWh"

# Electrical units
ELECTRICAL_CURRENT_AMPERE = "A"
ELECTRICAL_VOLT_AMPERE = "VA"

# Degree units
DEGREE = "°"

# Currency units
CURRENCY_EURO = "€"
CURRENCY_DOLLAR = "$"
CURRENCY_CENT = "¢"

# Temperature units
TEMP_CELSIUS = "°C"
TEMP_FAHRENHEIT = "°F"
TEMP_KELVIN = "K"

# Time units
TIME_MICROSECONDS = "μs"
TIME_MILLISECONDS = "ms"
TIME_SECONDS = "s"
TIME_MINUTES = "min"
TIME_HOURS = "h"
TIME_DAYS = "d"
TIME_WEEKS = "w"
TIME_MONTHS = "m"
TIME_YEARS = "y"

# Length units
LENGTH_MILLIMETERS: str = "mm"
LENGTH_CENTIMETERS: str = "cm"
LENGTH_METERS: str = "m"
LENGTH_KILOMETERS: str = "km"

LENGTH_INCHES: str = "in"
LENGTH_FEET: str = "ft"
LENGTH_YARD: str = "yd"
LENGTH_MILES: str = "mi"

# Frequency units
FREQUENCY_HERTZ = "Hz"
FREQUENCY_GIGAHERTZ = "GHz"

# Pressure units
PRESSURE_PA: str = "Pa"
PRESSURE_HPA: str = "hPa"
PRESSURE_BAR: str = "bar"
PRESSURE_MBAR: str = "mbar"
PRESSURE_INHG: str = "inHg"
PRESSURE_PSI: str = "psi"

# Volume units
VOLUME_LITERS: str = "L"
VOLUME_MILLILITERS: str = "mL"
VOLUME_CUBIC_METERS = "m³"
VOLUME_CUBIC_FEET = "ft³"

VOLUME_GALLONS: str = "gal"
VOLUME_FLUID_OUNCE: str = "fl. oz."

# Volume Flow Rate units
VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR = "m³/h"
VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE = "ft³/m"

# Area units
AREA_SQUARE_METERS = "m²"

# Mass units
MASS_GRAMS: str = "g"
MASS_KILOGRAMS: str = "kg"
MASS_MILLIGRAMS = "mg"
MASS_MICROGRAMS = "µg"

MASS_OUNCES: str = "oz"
MASS_POUNDS: str = "lb"

# Conductivity units
CONDUCTIVITY: str = "µS/cm"

# Light units
LIGHT_LUX: str = "lx"

# UV Index units
UV_INDEX: str = "UV index"

# Percentage units
PERCENTAGE = "%"

# Irradiation units
IRRADIATION_WATTS_PER_SQUARE_METER = "W/m²"

# Precipitation units
PRECIPITATION_MILLIMETERS_PER_HOUR = "mm/h"

# Concentration units
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "µg/m³"
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER = "mg/m³"
CONCENTRATION_PARTS_PER_CUBIC_METER = "p/m³"
CONCENTRATION_PARTS_PER_MILLION = "ppm"
CONCENTRATION_PARTS_PER_BILLION = "ppb"

# Speed units
SPEED_MILLIMETERS_PER_DAY = "mm/d"
SPEED_INCHES_PER_DAY = "in/d"
SPEED_METERS_PER_SECOND = "m/s"
SPEED_INCHES_PER_HOUR = "in/h"
SPEED_KILOMETERS_PER_HOUR = "km/h"
SPEED_MILES_PER_HOUR = "mph"

# Signal_strength units
SIGNAL_STRENGTH_DECIBELS = "dB"
SIGNAL_STRENGTH_DECIBELS_MILLIWATT = "dBm"

# Data units
DATA_BITS = "bit"
DATA_KILOBITS = "kbit"
DATA_MEGABITS = "Mbit"
DATA_GIGABITS = "Gbit"
DATA_BYTES = "B"
DATA_KILOBYTES = "kB"
DATA_MEGABYTES = "MB"
DATA_GIGABYTES = "GB"
DATA_TERABYTES = "TB"
DATA_PETABYTES = "PB"
DATA_EXABYTES = "EB"
DATA_ZETTABYTES = "ZB"
DATA_YOTTABYTES = "YB"
DATA_KIBIBYTES = "KiB"
DATA_MEBIBYTES = "MiB"
DATA_GIBIBYTES = "GiB"
DATA_TEBIBYTES = "TiB"
DATA_PEBIBYTES = "PiB"
DATA_EXBIBYTES = "EiB"
DATA_ZEBIBYTES = "ZiB"
DATA_YOBIBYTES = "YiB"
DATA_RATE_BITS_PER_SECOND = "bit/s"
DATA_RATE_KILOBITS_PER_SECOND = "kbit/s"
DATA_RATE_MEGABITS_PER_SECOND = "Mbit/s"
DATA_RATE_GIGABITS_PER_SECOND = "Gbit/s"
DATA_RATE_BYTES_PER_SECOND = "B/s"
DATA_RATE_KILOBYTES_PER_SECOND = "kB/s"
DATA_RATE_MEGABYTES_PER_SECOND = "MB/s"
DATA_RATE_GIGABYTES_PER_SECOND = "GB/s"
DATA_RATE_KIBIBYTES_PER_SECOND = "KiB/s"
DATA_RATE_MEBIBYTES_PER_SECOND = "MiB/s"
DATA_RATE_GIBIBYTES_PER_SECOND = "GiB/s"

# #### SERVICES ####
SERVICE_HOMEASSISTANT_STOP = "stop"
SERVICE_HOMEASSISTANT_RESTART = "restart"

SERVICE_TURN_ON = "turn_on"
SERVICE_TURN_OFF = "turn_off"
SERVICE_TOGGLE = "toggle"
SERVICE_RELOAD = "reload"

SERVICE_VOLUME_UP = "volume_up"
SERVICE_VOLUME_DOWN = "volume_down"
SERVICE_VOLUME_MUTE = "volume_mute"
SERVICE_VOLUME_SET = "volume_set"
SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause"
SERVICE_MEDIA_PLAY = "media_play"
SERVICE_MEDIA_PAUSE = "media_pause"
SERVICE_MEDIA_STOP = "media_stop"
SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track"
SERVICE_MEDIA_SEEK = "media_seek"
SERVICE_REPEAT_SET = "repeat_set"
SERVICE_SHUFFLE_SET = "shuffle_set"

SERVICE_ALARM_DISARM = "alarm_disarm"
SERVICE_ALARM_ARM_HOME = "alarm_arm_home"
SERVICE_ALARM_ARM_AWAY = "alarm_arm_away"
SERVICE_ALARM_ARM_NIGHT = "alarm_arm_night"
SERVICE_ALARM_ARM_CUSTOM_BYPASS = "alarm_arm_custom_bypass"
SERVICE_ALARM_TRIGGER = "alarm_trigger"


SERVICE_LOCK = "lock"
SERVICE_UNLOCK = "unlock"

SERVICE_OPEN = "open"
SERVICE_CLOSE = "close"

SERVICE_CLOSE_COVER = "close_cover"
SERVICE_CLOSE_COVER_TILT = "close_cover_tilt"
SERVICE_OPEN_COVER = "open_cover"
SERVICE_OPEN_COVER_TILT = "open_cover_tilt"
SERVICE_SET_COVER_POSITION = "set_cover_position"
SERVICE_SET_COVER_TILT_POSITION = "set_cover_tilt_position"
SERVICE_STOP_COVER = "stop_cover"
SERVICE_STOP_COVER_TILT = "stop_cover_tilt"
SERVICE_TOGGLE_COVER_TILT = "toggle_cover_tilt"

SERVICE_SELECT_OPTION = "select_option"

# #### API / REMOTE ####
SERVER_PORT = 8123

URL_ROOT = "/"
URL_API = "/api/"
URL_API_STREAM = "/api/stream"
URL_API_CONFIG = "/api/config"
URL_API_STATES = "/api/states"
URL_API_STATES_ENTITY = "/api/states/{}"
URL_API_EVENTS = "/api/events"
URL_API_EVENTS_EVENT = "/api/events/{}"
URL_API_SERVICES = "/api/services"
URL_API_SERVICES_SERVICE = "/api/services/{}/{}"
URL_API_COMPONENTS = "/api/components"
URL_API_ERROR_LOG = "/api/error_log"
URL_API_LOG_OUT = "/api/log_out"
URL_API_TEMPLATE = "/api/template"
URL_API_HISTORY_PERIOD = "/api/history/period/{}"

HTTP_OK = 200
HTTP_CREATED = 201
HTTP_ACCEPTED = 202
HTTP_MOVED_PERMANENTLY = 301
HTTP_BAD_REQUEST = 400
HTTP_UNAUTHORIZED = 401
HTTP_FORBIDDEN = 403
HTTP_NOT_FOUND = 404
HTTP_METHOD_NOT_ALLOWED = 405
HTTP_UNPROCESSABLE_ENTITY = 422
HTTP_TOO_MANY_REQUESTS = 429
HTTP_INTERNAL_SERVER_ERROR = 500
HTTP_BAD_GATEWAY = 502
HTTP_SERVICE_UNAVAILABLE = 503

HTTP_BASIC_AUTHENTICATION = "basic"
HTTP_DIGEST_AUTHENTICATION = "digest"

HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With"

CONTENT_TYPE_JSON = "application/json"
CONTENT_TYPE_MULTIPART = "multipart/x-mixed-replace; boundary={}"
CONTENT_TYPE_TEXT_PLAIN = "text/plain"

# The exit code to send to request a restart
RESTART_EXIT_CODE = 100

UNIT_NOT_RECOGNIZED_TEMPLATE: str = "{} is not a recognized {} unit."

LENGTH: str = "length"
MASS: str = "mass"
PRESSURE: str = "pressure"
VOLUME: str = "volume"
TEMPERATURE: str = "temperature"
SPEED_MS: str = "speed_ms"
ILLUMINANCE: str = "illuminance"

WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]

# The degree of precision for platforms
PRECISION_WHOLE = 1
PRECISION_HALVES = 0.5
PRECISION_TENTHS = 0.1

# Static list of entities that will never be exposed to
# cloud, alexa, or google_home components
CLOUD_NEVER_EXPOSED_ENTITIES = ["group.all_locks"]

# The ID of the Home Assistant Cast App
CAST_APP_ID_HOMEASSISTANT = "B12CE3CA"


================================================
FILE: homeassistant_cli/helper.py
================================================
"""Helpers used by Home Assistant CLI (hass-cli)."""

import ast
import contextlib
import json
import logging
import shlex
from collections.abc import Generator
from http.client import HTTPConnection
from typing import Any, cast

from ruamel.yaml import YAML
from tabulate import tabulate

import homeassistant_cli.const as const
import homeassistant_cli.yaml as yaml
from homeassistant_cli.config import Configuration

_LOGGING = logging.getLogger(__name__)


def to_attributes(entry: str) -> dict[str, str]:
    """Convert list of key=value pairs to dictionary."""
    if not entry:
        return {}

    lexer = shlex.shlex(entry, posix=True)
    lexer.whitespace_split = True
    lexer.whitespace = ","
    attributes_dict = {}  # type: Dict[str, str]
    for pair in lexer:
        if "=" not in pair:
            continue
        key, value = pair.split("=", 1)
        if value.strip().startswith("[") and value.strip().endswith("]"):
            try:
                value = ast.literal_eval(value)
            except Exception:
                pass
        attributes_dict[key] = value
    return attributes_dict


def to_tuples(entry: str) -> list[tuple[str, str]]:
    """Convert list of key=value pairs to list of tuples."""
    if not entry:
        return []

    lexer = shlex.shlex(entry, posix=True)
    lexer.whitespace_split = True
    lexer.whitespace = ","
    attributes_list = []  # type: List[Tuple[str,str]]
    attributes_list = list(
        tuple(pair.split("=", 1))
        for pair in lexer  # type: ignore
    )
    return attributes_list


def raw_format_output(
    output: str,
    data: dict[str, Any] | list[dict[str, Any]],
    yamlparser: YAML,
    columns: list | None = None,
    no_headers: bool = False,
    table_format: str = "plain",
    sort_by: str | None = None,
) -> str:
    """Format the raw output."""
    if output == "auto":
        _LOGGING.debug("Output `auto` thus using %s", const.DEFAULT_DATAOUTPUT)
        output = const.DEFAULT_DATAOUTPUT

    if sort_by and isinstance(data, list):
        _sort_table(data, sort_by)

    if output == "json":
        try:
            return json.dumps(data, indent=2, sort_keys=False)
        except ValueError:
            return str(data)
    elif output == "ndjson":
        try:
            return json.dumps(data)
        except ValueError:
            return str(data)
    elif output == "yaml":
        try:
            return cast(str, yaml.dumpyaml(yamlparser, data))
        except ValueError:
            return str(data)
    elif output == "table":
        from jsonpath_ng import parse

        if not columns:
            columns = const.COLUMNS_DEFAULT

        fmt = [(v[0], parse(v[1] if len(v) > 1 else v[0])) for v in columns]

        result = []

        if no_headers:
            headers = []  # type: List[str]
        else:
            headers = [v[0] for v in fmt]

        # In case data passed in is a single element
        # we turn it into a single item list for better table output
        if not isinstance(data, list):
            data = [data]

        for item in data:
            row = []
            for fmtpair in fmt:
                val = [match.value for match in fmtpair[1].find(item)]
                row.append(", ".join(map(str, val)))
            result.append(row)

        res = tabulate(result, headers=headers, tablefmt=table_format)  # type: str
        return res
    else:
        raise ValueError(
            f"Output Format was {output}, expected either 'json' or 'yaml'"
        )


def _sort_table(result: list[Any], sort_by: str) -> list[Any]:
    """Sort the content of a table."""
    from jsonpath_ng import parse

    expr = parse(sort_by)

    def _internal_sort(row: dict[Any, str]) -> Any:
        val = next(iter([match.value for match in expr.find(row)]), None)
        return (val is None, val)

    result.sort(key=_internal_sort)
    return result


def format_output(
    ctx: Configuration,
    data: list[dict[str, Any]],
    columns: list | None = None,
) -> str:
    """Format data to output based on settings in ctx/Context."""
    return raw_format_output(
        ctx.output,
        data,
        ctx.yaml(),
        columns,
        ctx.no_headers,
        ctx.table_format,
        ctx.sort_by,
    )


def debug_requests_on() -> None:
    """Switch on logging of the requests module."""
    HTTPConnection.set_debuglevel(cast(HTTPConnection, HTTPConnection), 1)

    logging.basicConfig()
    logging.getLogger().setLevel(logging.DEBUG)
    requests_log = logging.getLogger("requests.packages.urllib3")
    requests_log.setLevel(logging.DEBUG)
    requests_log.propagate = True


def debug_requests_off() -> None:
    """Switch off logging of the requests module.

    Might have some side-effects.
    """
    HTTPConnection.set_debuglevel(cast(HTTPConnection, HTTPConnection), 1)

    root_logger = logging.getLogger()
    root_logger.setLevel(logging.WARNING)
    root_logger.handlers = []
    requests_log = logging.getLogger("requests.packages.urllib3")
    requests_log.setLevel(logging.WARNING)
    requests_log.propagate = False


@contextlib.contextmanager
def debug_requests() -> Generator:
    """Yieldable way to turn on debugs for requests.

    with debug_requests(): <do things>
    """
    debug_requests_on()
    yield
    debug_requests_off()


================================================
FILE: homeassistant_cli/plugins/area.py
================================================
"""Area (registry) plugin for Home Assistant CLI (hass-cli)."""

import logging
import re
import sys
from re import Pattern

import click

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.const as const
import homeassistant_cli.helper as helper
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration

_LOGGING = logging.getLogger(__name__)


@click.group("area")
@pass_context
def cli(ctx: Configuration) -> None:
    """Get info and operate on areas from Home Assistant (EXPERIMENTAL)."""


@cli.command("list")
@click.argument("area_filter", default=".*", required=False)
@pass_context
def listcmd(ctx: Configuration, area_filter: str) -> None:
    """List all areas from Home Assistant.

    AREA_FILTER - optional regex to filter by area name
    """
    ctx.auto_output("table")

    areas = api.get_areas(ctx)

    result: list[dict] = []
    if area_filter == ".*":
        result = areas
    else:
        area_filter_regex: Pattern[str] = re.compile(area_filter)

        for area in areas:
            if area_filter_regex.search(area["name"]):
                result.append(area)

    cols = [("ID", "area_id"), ("NAME", "name")]

    ctx.echo(
        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)
    )


@cli.command("create")
@click.argument("names", nargs=-1, required=True)
@pass_context
def create(ctx: Configuration, names: tuple[str, ...]) -> None:
    """Create an area.

    NAMES - one or more area names to create
    """
    ctx.auto_output("data")

    for name in names:
        result = api.create_area(ctx, name)

        ctx.echo(
            helper.format_output(
                ctx,
                [result],
                columns=ctx.columns if ctx.columns else const.COLUMNS_DEFAULT,
            )
        )


@cli.command("delete")
@click.argument(
    "names",
    nargs=-1,
    required=True,
    shell_complete=autocompletion.areas,  # type: ignore
)
@click.option(
    "--confirm",
    is_flag=True,
    default=False,
    help="Confirm deletion without prompting",
)
@pass_context
def delete(ctx: Configuration, names: tuple[str, ...], confirm: bool) -> None:
    """Delete an area.

    NAMES - one or more area names or id to delete
    """
    ctx.auto_output("data")
    excode = 0

    for name in names:
        area = api.find_area(ctx, name)
        if not area:
            _LOGGING.error("Could not find area with id or name: %s", name)
            excode = 1
            continue

        if not confirm:
            click.confirm(
                f"Are you sure you want to delete '{area['name']}'"
                f" ({area['area_id']})?",
                abort=True,
            )

        result = api.delete_area(ctx, area["area_id"])

        if result.get("success"):
            ctx.echo(f"Successfully deleted area: {area['name']} ({area['area_id']})")
        else:
            ctx.echo(helper.format_output(ctx, result))

    if excode != 0:
        sys.exit(excode)


@cli.command("rename")
@click.argument(
    "old_name",
    required=True,
    shell_complete=autocompletion.areas,  # type: ignore
)
@click.argument("new_name", required=True)
@pass_context
def rename(ctx: Configuration, old_name: str, new_name: str) -> None:
    """Rename an area."""
    ctx.auto_output("data")

    area = api.find_area(ctx, old_name)
    if not area:
        _LOGGING.error("Could not find area with id or name: %s", old_name)
        sys.exit(1)

    result = api.rename_area(ctx, area["area_id"], new_name)

    ctx.echo(
        helper.format_output(
            ctx,
            [result],
            columns=ctx.columns if ctx.columns else const.COLUMNS_DEFAULT,
        )
    )


================================================
FILE: homeassistant_cli/plugins/completion.py
================================================
"""Auto-completion for Home Assistant CLI (hass-cli)."""

import click
from click._bashcomplete import get_completion_script

from homeassistant_cli.cli import pass_context


@click.group("completion")
@pass_context
def cli(ctx):
    """Output shell completion code for the specified shell (bash or zsh)."""


def dump_script(shell: str) -> None:
    """Dump the script content."""
    # todo resolve actual script name in case user aliased it
    prog_name = "hass-cli"
    cvar = f"_{prog_name.replace('-', '_').upper()}_COMPLETE"

    click.echo(get_completion_script(prog_name, cvar, shell))


@cli.command()
@pass_context
def bash(ctx):
    """Output shell completion code for bash."""
    dump_script("bash")


@cli.command()
@pass_context
def zsh(ctx):
    """Output shell completion code for zsh."""
    dump_script("zsh")


================================================
FILE: homeassistant_cli/plugins/config.py
================================================
"""Configuration plugin for Home Assistant CLI (hass-cli)."""

import click

import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration
from homeassistant_cli.helper import format_output


@click.group("config")
@pass_context
def cli(ctx):
    """Get configuration from a Home Assistant instance."""
    ctx.auto_output("table")


COLUMNS_DETAILS = [
    ("VERSION", "version"),
    ("CONFIG", "config_dir"),
    ("TZ", "time_zone"),
    ("LOCATION", "location_name"),
    ("LONGITUDE", "longitude"),
    ("LATITUDE", "latitude"),
    ("ELEVATION", "elevation"),
    ("TZ", "time_zone"),
    ("UNITS", "unit_system"),
]


@cli.command()
@pass_context
def full(ctx: Configuration):
    """Get full details on the configuration from Home Assistant."""
    click.echo(
        format_output(
            ctx,
            [api.get_config(ctx)],
            columns=ctx.columns if ctx.columns else COLUMNS_DETAILS,
        )
    )


@cli.command()
@pass_context
def integrations(ctx: Configuration):
    """Get loaded integrations from Home Assistant."""
    click.echo(
        format_output(
            ctx,
            api.get_config(ctx)["components"],
            columns=ctx.columns if ctx.columns else [("INTEGRATIONS", "$")],
        )
    )


@cli.command()
@pass_context
def whitelist_dirs(ctx: Configuration):
    """Get the whitelisted directories from Home Assistant."""
    click.echo(
        format_output(
            ctx,
            api.get_config(ctx)["whitelist_external_dirs"],
            columns=ctx.columns if ctx.columns else [("DIRECTORY", "$")],
        )
    )


@cli.command()
@pass_context
def release(ctx: Configuration):
    """Get the release of Home Assistant."""
    click.echo(
        format_output(
            ctx,
            [api.get_config(ctx)["version"]],
            columns=ctx.columns if ctx.columns else [("VERSION", "$")],
        )
    )


================================================
FILE: homeassistant_cli/plugins/device.py
================================================
"""Device (registry) plugin for Home Assistant CLI (hass-cli)."""

import logging
import re
import sys
from typing import Any, Dict, List, Optional

import click

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.helper as helper
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration

_LOGGING = logging.getLogger(__name__)


@click.group("device")
@pass_context
def cli(ctx):
    """Get info and operate on devices from Home Assistant."""


@cli.command("list")
@click.argument("device_filter", default=".*", required=False)
@pass_context
def list_cmd(ctx: Configuration, device_filter: str):
    """List all devices from Home Assistant.

    DEVICE_FILTER - regular expression to filter devices by name
    """
    ctx.auto_output("table")

    areas = api.get_areas(ctx)

    devices = api.get_devices(ctx)

    result = []  # type: List[Dict]
    if device_filter == ".*":
        result = devices
    else:
        device_filter_regex = re.compile(device_filter)  # type: Pattern

        for device in devices:
            if device_filter_regex.search(device["name"]):
                result.append(device)

    for device in devices:
        area = next((a for a in areas if a["area_id"] == device["area_id"]), None)
        if area:
            device["area_name"] = area["name"]

    cols = [
        ("ID", "id"),
        ("NAME", "name"),
        ("NAME BY USER", "name_by_user"),
        ("MODEL", "model"),
        ("MANUFACTURER", "manufacturer"),
        ("AREA", "area_name"),
    ]

    ctx.echo(
        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)
    )


@cli.command("assign")
@click.argument(
    "area_id_or_name",
    required=True,
    shell_complete=autocompletion.areas,  # type: ignore
)
@click.argument("names", nargs=-1, required=False)
@click.option("--match", help="Expression used to find devices matching that name")
@pass_context
def assign(
    ctx: Configuration,
    area_id_or_name,
    names: list[str],
    match: str | None = None,
):
    """Update area on one or more devices.

    NAMES - one or more name or id (Optional)
    """
    ctx.auto_output("data")

    devices = api.get_devices(ctx)

    result = []  # type: List[Dict]

    area = api.find_area(ctx, area_id_or_name)
    if not area:
        _LOGGING.error("Could not find area with id or name: %s", area_id_or_name)
        sys.exit(1)

    if match:
        if match == ".*":
            result = devices
        else:
            device_filter_regex = re.compile(match)  # type: Pattern

            for device in devices:
                if device_filter_regex.search(device["name"]):
                    result.append(device)

    for id_or_name in names:
        device = next(
            (x for x in devices if x["id"] == id_or_name),
            None,  # type: ignore
        )
        if not device:
            device = next(
                (x for x in devices if x["name"] == id_or_name),
                None,  # type: ignore
            )
        if not device:
            _LOGGING.error("Could not find device with id or name: %s", id_or_name)
            sys.exit(1)
        result.append(device)

    for device in result:
        output = api.assign_area(ctx, device["id"], area["area_id"])
        if output["success"]:
            ctx.echo(
                "Successfully assigned '{}' to '{}'".format(
                    area["name"], device["name"]
                )
            )
        else:
            _LOGGING.error(
                "Failed to assign '%s' to '%s'", area["name"], device["name"]
            )

            ctx.echo(str(output))


@cli.command("rename")
@click.argument("device_id_or_name", required=True)
@click.argument("new_name", required=True)
@pass_context
def rename(
    ctx: Configuration,
    device_id_or_name,
    new_name,
):
    """Update name of specified device."""
    ctx.auto_output("data")

    devices = api.get_devices(ctx)

    device = next(
        (x for x in devices if x["id"] == device_id_or_name),
        None,  # type: ignore
    )
    if not device:
        device = next(
            (x for x in devices if x["name"] == device_id_or_name),
            None,  # type: ignore
        )
    if not device:
        _LOGGING.error("Could not find device with id or name: %s", device_id_or_name)
        sys.exit(1)

    output = api.rename_device(ctx, device["id"], new_name)
    if output["success"]:
        ctx.echo(
            "Successfully renamed '{}' from {} to '{}'".format(
                device_id_or_name, device["name_by_user"], new_name
            )
        )
    else:
        _LOGGING.error("Failed to rename '%s' to '%s'", device_id_or_name, new_name)

        ctx.echo(str(output))


@cli.command("list-by-area")
@click.argument(
    "area_id_or_name",
    required=True,
    shell_complete=autocompletion.areas,  # type: ignore
)
@pass_context
def list_by_area(ctx: Configuration, area_id_or_name: str):
    """List all devices in a specified area.

    AREA_ID_OR_NAME - area id or name
    """
    ctx.auto_output("table")

    area = api.find_area(ctx, area_id_or_name)
    if not area:
        _LOGGING.error("Could not find area with id or name: %s", area_id_or_name)
        sys.exit(1)

    devices = api.get_devices(ctx)
    result = [d for d in devices if d["area_id"] == area["area_id"]]

    cols = [
        ("ID", "id"),
        ("NAME", "name"),
        ("MODEL", "model"),
        ("MANUFACTURER", "manufacturer"),
    ]

    ctx.echo(
        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)
    )


@cli.command("delete")
@click.argument("device_id_or_name", required=True)
@click.option(
    "--confirm",
    is_flag=True,
    default=False,
    help="Confirm deletion without prompting",
)
@pass_context
def delete(ctx: Configuration, device_id_or_name: str, confirm: bool):
    """Delete a specified device."""
    ctx.auto_output("data")

    devices = api.get_devices(ctx)

    device = next(
        (x for x in devices if x["id"] == device_id_or_name),
        None,  # type: ignore
    )
    if not device:
        device = next(
            (x for x in devices if x["name"] == device_id_or_name),
            None,  # type: ignore
        )
    if not device:
        _LOGGING.error("Could not find device with id or name: %s", device_id_or_name)
        sys.exit(1)

    if not confirm:
        click.confirm(
            f"Are you sure you want to delete '{device['name']}' [{device['id']}]?",
            abort=True,
        )

    output = api.delete_device(ctx, device["id"])
    if output["success"]:
        ctx.echo("Successfully deleted device '{}'".format(device["name"]))
    else:
        _LOGGING.error("Failed to delete device '%s'", device_id_or_name)
        ctx.echo(str(output))


================================================
FILE: homeassistant_cli/plugins/discover.py
================================================
"""Discovery plugin for Home Assistant CLI (hass-cli)."""

import click

from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration
from homeassistant_cli.helper import format_output


@click.command("discover")
@click.option("--raw", is_flag=True, help="Include raw data found during scan.")
@pass_context
def cli(ctx: Configuration, raw):
    """Discovery for the local network."""
    from netdisco.discovery import NetworkDiscovery

    click.echo("Running discovery on network (might take a while)...")
    netdiscovery = NetworkDiscovery()
    netdiscovery.scan()

    for device in netdiscovery.discover():
        info = netdiscovery.get_info(device)
        click.echo(f"{device}:\n{format_output(ctx, info)}")

    if raw:
        click.echo("Raw data:")
        netdiscovery.print_raw_data()

    netdiscovery.stop()


================================================
FILE: homeassistant_cli/plugins/entity.py
================================================
"""Entity plugin for Home Assistant CLI (hass-cli)."""

import logging
import re
import sys

import click

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.const as const
import homeassistant_cli.helper as helper
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration

_LOGGING = logging.getLogger(__name__)


@click.group("entity")
@pass_context
def cli(ctx):
    """Get info on entities from Home Assistant."""


@cli.command("list")
@click.argument("entity_filter", default=".*", required=False)
@pass_context
def listcmd(ctx: Configuration, entity_filter: str):
    """List all entities from Home Assistant."""
    ctx.auto_output("table")

    areas = api.get_areas(ctx)

    entities = api.get_entities(ctx)

    result = []  # type: List[Dict]
    if entity_filter == ".*":
        result = entities
    else:
        entity_filter_regex = re.compile(entity_filter)  # type: Pattern

        for entity in entities:
            if entity_filter_regex.search(entity["entity_id"]):
                result.append(entity)

    for entity in entities:
        area = next((a for a in areas if a["area_id"] == entity["area_id"]), None)
        if area:
            entity["area_name"] = area["name"]

    cols = [
        ("ENTITY_ID", "entity_id"),
        ("NAME", "name"),
        ("DEVICE_ID", "device_id"),
        ("PLATFORM", "platform"),
        ("AREA", "area_name"),
        ("CONFIG_ENTRY_ID", "config_entry_id"),
        ("DISABLED_BY", "disabled_by"),
    ]

    ctx.echo(
        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)
    )


@cli.command("assign")
@click.argument(
    "area_id_or_name",
    required=True,
    shell_complete=autocompletion.areas,  # type: ignore
)
@click.argument("names", nargs=-1, required=False)
@click.option("--match", help="Expression used to find entities matching that name")
@pass_context
def assign(
    ctx: Configuration,
    area_id_or_name,
    names: list[str],
    match: str | None = None,
):
    """Update area on one or more entities.

    NAMES - one or more name or id (Optional)
    """
    ctx.auto_output("data")

    entities = api.get_entities(ctx)

    result = []  # type: List[Dict]

    area = api.find_area(ctx, area_id_or_name)
    if not area:
        _LOGGING.error("Could not find area with id or name: %s", area_id_or_name)
        sys.exit(1)

    if match:
        if match == ".*":
            result = entities
        else:
            entity_filter_regex = re.compile(match)  # type: Pattern

            for entity in entities:
                if entity_filter_regex.search(entity["name"]):
                    result.append(entity)

    for id_or_name in names:
        entity = next(
            (x for x in entities if x["entity_id"] == id_or_name),
            None,  # type: ignore
        )
        if not entity:
            entity = next(
                (x for x in entities if x["name"] == id_or_name),
                None,  # type: ignore
            )
        if not entity:
            _LOGGING.error("Could not find entity with id or name: %s", id_or_name)
            sys.exit(1)
        result.append(entity)

    for entity in result:
        output = api.assign_entity_area(ctx, entity["entity_id"], area["area_id"])
        if output["success"]:
            ctx.echo(
                "Successfully assigned '{}' to '{}'".format(
                    area["name"], entity["entity_id"]
                )
            )
        else:
            _LOGGING.error(
                "Failed to assign '%s' to '%s'",
                area["name"],
                entity["entity_id"],
            )

            ctx.echo(str(output))


@cli.command("rename")
@click.argument(
    "old_id",
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@click.option("--name", required=False)
@click.argument(
    "new_id",
    required=False,
    shell_complete=autocompletion.entities,  # type: ignore
)
@pass_context
def rename(ctx, old_id, new_id, name):
    """Rename a entity."""
    ctx.auto_output("data")

    if not new_id and not name:
        _LOGGING.error("Need to at least specify either a new id or new name")
        sys.exit(1)

    entity = api.get_entity(ctx, old_id)
    if not entity:
        _LOGGING.error("Could not find entity with ID: %s", old_id)
        sys.exit(1)

    result = api.rename_entity(ctx, old_id, new_id, name)

    ctx.echo(
        helper.format_output(
            ctx,
            [result],
            columns=ctx.columns if ctx.columns else const.COLUMNS_DEFAULT,
        )
    )


@cli.command("delete")
@click.argument(
    "entity_id",
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@click.option(
    "--confirm",
    is_flag=True,
    default=False,
    help="Confirm deletion without prompting",
)
@pass_context
def delete(ctx: Configuration, entity_id: str, confirm: bool) -> None:
    """Delete an entity.

    ENTITY_ID - the entity_id of the entity to delete
    """
    ctx.auto_output("data")

    entity = api.get_entity(ctx, entity_id)
    if not entity:
        _LOGGING.error("Could not find entity with ID: %s", entity_id)
        sys.exit(1)

    if not confirm:
        click.confirm(
            f"Are you sure you want to delete '{entity_id}'?",
            abort=True,
        )

    result = api.delete_entity(ctx, entity_id)

    if result.get("success"):
        ctx.echo(f"Successfully deleted entity '{entity_id}'")
    else:
        _LOGGING.error("Failed to delete entity: %s", entity_id)
        ctx.echo(str(result))


@cli.command("enable")
@click.argument(
    "entity_id",
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@pass_context
def enable(ctx: Configuration, entity_id: str) -> None:
    """Enable an entity.

    ENTITY_ID - the entity_id of the entity to enable
    """
    ctx.auto_output("data")

    entity = api.get_entity(ctx, entity_id)
    if not entity:
        _LOGGING.error("Could not find entity with ID: %s", entity_id)
        sys.exit(1)

    result = api.enable_entity(ctx, entity_id, None)

    if result.get("success"):
        ctx.echo(f"Successfully enabled entity '{entity_id}'")
    else:
        _LOGGING.error("Failed to enable entity: %s", entity_id)
        ctx.echo(str(result))


@cli.command("disable")
@click.argument(
    "entity_id",
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@pass_context
def disable(ctx: Configuration, entity_id: str) -> None:
    """Disable an entity.

    ENTITY_ID - the entity_id of the entity to disable
    """
    ctx.auto_output("data")

    entity = api.get_entity(ctx, entity_id)
    if not entity:
        _LOGGING.error("Could not find entity with ID: %s", entity_id)
        sys.exit(1)

    result = api.enable_entity(ctx, entity_id, "user")

    if result.get("success"):
        ctx.echo(f"Successfully disabled entity '{entity_id}'")
    else:
        _LOGGING.error("Failed to disable entity: %s", entity_id)
        ctx.echo(str(result))


================================================
FILE: homeassistant_cli/plugins/event.py
================================================
"""Event plugin for Home Assistant CLI (hass-cli)."""

import json as json_
import logging

import click

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration
from homeassistant_cli.exceptions import HomeAssistantCliError
from homeassistant_cli.helper import format_output, raw_format_output

_LOGGING = logging.getLogger(__name__)


@click.group("event")
@pass_context
def cli(ctx):
    """Interact with events."""


@cli.command()
@click.argument(
    "event",
    required=True,
    shell_complete=autocompletion.events,  # type: ignore
)
@click.option(
    "--json",
    help="Raw JSON state to use for event. Overrides any other statevalues provided.",
)
@pass_context
def fire(ctx: Configuration, event, json):
    """Fire event in Home Assistant."""
    if json:
        click.echo(f"Fire {event}")
        response = api.fire_event(ctx, event, json_.loads(json))
    else:
        existing = raw_format_output(ctx.output, [{}], ctx.yaml())
        new = click.edit(existing, extension=f".{ctx.output}")

        if new:
            click.echo(f"Fire {event}")
            if ctx.output == "yaml":
                data = ctx.yamlload(new)
            else:
                data = json_.loads(new)

            response = api.fire_event(ctx, event, data)
        else:
            click.echo("No edits/changes.")
            return

    if response:
        ctx.echo(raw_format_output(ctx.output, [response], ctx.yaml()))


@cli.command()
@click.argument("event_type", required=False)
@pass_context
def watch(ctx: Configuration, event_type):
    """Subscribe and print events.

    EVENT-TYPE even type to subscribe to. if empty subscribe to all.
    """
    frame = {"type": "subscribe_events"}

    cols = [("EVENT_TYPE", "event_type"), ("DATA", "$.data")]

    def _msghandler(msg: dict) -> None:
        if msg["type"] == "event":
            ctx.echo(
                format_output(
                    ctx,
                    msg["event"],
                    columns=ctx.columns if ctx.columns else cols,
                )
            )
        elif msg["type"] == "auth_invalid":
            raise HomeAssistantCliError(msg.get("message"))

    if event_type:
        frame["event_type"] = event_type

    api.wsapi(ctx, frame, _msghandler)


================================================
FILE: homeassistant_cli/plugins/ha.py
================================================
"""Home Assistant Operating System plugin for Home Assistant CLI (hass-cli)."""

import json as json_
import logging
from typing import Any, Dict, List, cast

import click
from packaging.version import Version
from requests.exceptions import HTTPError

import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration
from homeassistant_cli.exceptions import HomeAssistantCliError
from homeassistant_cli.helper import format_output

_LOGGING = logging.getLogger(__name__)

# These commands loosely based on what can be found in
# https://developers.home-assistant.io/docs/api/supervisor/endpoints


@click.group("ha")
@pass_context
def cli(ctx: Configuration):
    """Home Assistant Operating System commands."""
    ctx.auto_output("data")


def _report(ctx, cmd, method, response) -> None:
    """Create a report."""
    response.raise_for_status()

    if response.ok:
        try:
            ctx.echo(format_output(ctx, response.json()))
        except json_.decoder.JSONDecodeError:
            _LOGGING.debug("Response could not be parsed as JSON")
            ctx.echo(response.text)
    else:
        _LOGGING.warning(
            "%s: <No output returned from %s %s>",
            response.status_code,
            cmd,
            method,
        )


def _handle(ctx, method, httpmethod="get", raw=False) -> None:
    """Handle the data."""
    method = f"/{method}"
    response = api.restapi_supervisor(ctx, httpmethod, method)

    _report(ctx, httpmethod, method, response)


def _handle_raw(ctx, method, httpmethod="get") -> dict:
    """Handle raw data."""
    method = f"/{method}"
    response = api.restapi_supervisor(ctx, httpmethod, method)
    return response.json()


# Addon/Apps endpoints
#########################################################################
@cli.group("addons")
@pass_context
def addons(ctx: Configuration):
    """Home Assistant addons commands."""
    ctx.auto_output("data")


@addons.command("all")
@pass_context
def addons_all(ctx: Configuration):
    """Home Assistant addons info."""
    _handle(ctx, "addons")


@addons.command("reload")
@pass_context
def addons_reload(ctx: Configuration):
    """Home Assistant addons reload."""
    _handle(ctx, "addons/reload", "post")


# Audio endpoints
#########################################################################
@cli.group("audio")
@pass_context
def audio(ctx: Configuration):
    """Home Assistant audio commands."""
    ctx.auto_output("data")


@audio.command("info")
@pass_context
def audio_info(ctx: Configuration):
    """Home Assistant audio info."""
    _handle(ctx, "audio/info")


@audio.command("stats")
@pass_context
def audio_stats(ctx: Configuration):
    """Home Assistant audio stats."""
    _handle(ctx, "audio/stats")


@audio.command("logs")
@pass_context
def audio_logs(ctx: Configuration):
    """Home Assistant audio logs."""
    _handle(ctx, "audio/logs")


@audio.command("reload")
@pass_context
def audio_reload(ctx: Configuration):
    """Home Assistant audio reload."""
    _handle(ctx, "audio/reload", "post")


@audio.command("restart")
@pass_context
def audio_restart(ctx: Configuration):
    """Home Assistant audio restart."""
    _handle(ctx, "audio/restart", "post")


# Auth endpoints
#########################################################################
@cli.group("auth")
@pass_context
def auth(ctx: Configuration):
    """Home Assistant auth commands."""
    ctx.auto_output("data")


@auth.command("list")
@pass_context
def auth_list(ctx: Configuration):
    """Home Assistant auth list."""
    _handle(ctx, "auth/list")


# Backup endpoints
#########################################################################
@cli.group("backup")
@pass_context
def backup(ctx: Configuration):
    """Home Assistant backup commands."""
    ctx.auto_output("data")


@backup.command("info")
@pass_context
def backup_info(ctx: Configuration):
    """Home Assistant backup info."""
    _handle(ctx, "backups/info")


@backup.command("reload")
@pass_context
def backup_reload(ctx: Configuration):
    """Home Assistant backups reload."""
    _handle(ctx, "backups/reload", "post")


# CLI endpoints
#########################################################################
@cli.group("ha-cli")
@pass_context
def ha_cli(ctx: Configuration):
    """Home Assistant ha-cli commands."""
    ctx.auto_output("data")


@ha_cli.command("info")
@pass_context
def ha_info(ctx: Configuration):
    """Home Assistant ha-cli info."""
    _handle(ctx, "cli/info")


@ha_cli.command("update")
@pass_context
def ha_update(ctx: Configuration):
    """Home Assistant ha-cli update."""
    response = _handle_raw(ctx, "cli/info")
    data = response["data"]
    current_version = int(data["version"])
    latest_version = int(data["version_latest"])
    if current_version == latest_version:
        ctx.echo("Already running the latest release")
    else:
        try:
            _handle(ctx, "cli/update", "post")
        except (HomeAssistantCliError, HTTPError):
            pass


@ha_cli.command("stats")
@pass_context
def ha_stats(ctx: Configuration):
    """Home Assistant ha-cli stats."""
    _handle(ctx, "cli/stats")


# Core endpoints
#########################################################################
@cli.group("core")
@pass_context
def core(ctx: Configuration):
    """Home Assistant core commands."""
    ctx.auto_output("data")


@core.command("info")
@pass_context
def core_info(ctx: Configuration):
    """Home Assistant core info."""
    _handle(ctx, "core/info")


@core.command("update")
@pass_context
def core_update(ctx: Configuration):
    """Home Assistant core update."""
    response = _handle_raw(ctx, "core/info")
    data = response["data"]
    current_version = data["version"]
    latest_version = data["version_latest"]
    if Version(current_version) == Version(latest_version):
        ctx.echo("Already running the latest release")
    else:
        try:
            _handle(ctx, "core/update", "post")
        except (HomeAssistantCliError, HTTPError):
            pass


@core.command("logs")
@pass_context
def core_logs(ctx: Configuration):
    """Home Assistant core logs."""
    _handle(ctx, "core/logs")


@core.command("restart")
@pass_context
def core_restart(ctx: Configuration):
    """Home Assistant core restart."""
    try:
        _handle(ctx, "core/restart", "post")
    except HomeAssistantCliError:
        pass


@core.command("check")
@pass_context
def core_check(ctx: Configuration):
    """Home Assistant core check."""
    try:
        _handle(ctx, "core/check", "post")
    except (HomeAssistantCliError, HTTPError):
        _handle(ctx, "core/logs")


@core.command("start")
@pass_context
def core_start(ctx: Configuration):
    """Home Assistant core start."""
    try:
        _handle(ctx, "core/start", "post")
    except HomeAssistantCliError:
        pass


@core.command("stop")
@pass_context
def core_stop(ctx: Configuration):
    """Home Assistant core stop."""
    try:
        _handle(ctx, "core/stop", "post")
    except HomeAssistantCliError:
        pass


@core.command("rebuild")
@pass_context
def core_rebuild(ctx: Configuration):
    """Home Assistant core rebuild."""
    try:
        _handle(ctx, "core/rebuild", "post")
    except HomeAssistantCliError:
        pass


@core.command("options")
@pass_context
def core_options(ctx: Configuration):
    """Home Assistant core options."""
    _handle(ctx, "core/options", "post")


@core.command("websocket")
@pass_context
def core_websocket(ctx: Configuration):
    """Home Assistant core websocket."""
    try:
        _handle(ctx, "core/websocket")
    except (HomeAssistantCliError, HTTPError):
        pass


@core.command("stats")
@pass_context
def core_stats(ctx: Configuration):
    """Home Assistant core stats."""
    _handle(ctx, "core/stats")


# Discovery endpoints
#########################################################################
# Not implemented
# @cli.group("discovery")
# @pass_context
# def discovery(ctx: Configuration):
#     """Home Assistant discovery commands."""
#     ctx.auto_output("data")


# DNS endpoints
#########################################################################
@cli.group("dns")
@pass_context
def dns(ctx: Configuration):
    """Home Assistant DNS commands."""
    ctx.auto_output("data")


@dns.command("info")
@pass_context
def dns_info(ctx: Configuration):
    """Home Assistant DNS info."""
    _handle(ctx, "dns/info")


@dns.command("options")
@pass_context
def dns_options(ctx: Configuration):
    """Home Assistant DNS options."""
    _handle(ctx, "dns/options", "post")


@dns.command("restart")
@pass_context
def dns_restart(ctx: Configuration):
    """Home Assistant DNS restart."""
    try:
        _handle(ctx, "dns/restart", "post")
    except HomeAssistantCliError:
        pass


@dns.command("logs")
@pass_context
def dns_logs(ctx: Configuration):
    """Home Assistant DNS logs."""
    _handle(ctx, "dns/logs")


@dns.command("stats")
@pass_context
def dns_stats(ctx: Configuration):
    """Home Assistant DNS stats."""
    _handle(ctx, "dns/stats")


@dns.command("update")
@pass_context
def dns_update(ctx: Configuration):
    """Home Assistant DNS update."""
    try:
        _handle(ctx, "dns/update", "post")
    except (HomeAssistantCliError, HTTPError):
        pass


@dns.command("reset")
@pass_context
def dns_reset(ctx: Configuration):
    """Home Assistant DNS reset."""
    try:
        _handle(ctx, "dns/reset", "post")
    except (HomeAssistantCliError, HTTPError):
        pass


# Docker endpoints
#########################################################################
@cli.group("docker")
@pass_context
def docker(ctx: Configuration):
    """Home Assistant Docker commands."""
    ctx.auto_output("data")


@docker.command("info")
@pass_context
def docker_info(ctx: Configuration):
    """Home Assistant Docker info."""
    _handle(ctx, "docker/info")


@docker.command("registries")
@pass_context
def docker_registries(ctx: Configuration):
    """Home Assistant Docker registries."""
    _handle(ctx, "docker/registries")


# Hardware endpoints
#########################################################################
@cli.group("hardware")
@pass_context
def hardware(ctx: Configuration):
    """Home Assistant hardware info."""
    ctx.auto_output("data")


@hardware.command("info")
@pass_context
def hardware_info(ctx: Configuration):
    """Home Assistant hardware info."""
    _handle(ctx, "hardware/info")


@hardware.command("audio")
@pass_context
def hardware_audio(ctx: Configuration):
    """Home Assistant hardware audio."""
    _handle(ctx, "hardware/audio")


# Host endpoints
#########################################################################
@cli.group("host")
@pass_context
def host(ctx: Configuration):
    """Home Assistant host commands."""
    ctx.auto_output("data")


@host.command("reboot")
@pass_context
def host_reboot(ctx: Configuration):
    """Home Assistant host reboot."""
    _handle(ctx, "host/reboot", "post")


@host.command("reload")
@pass_context
def host_reload(ctx: Configuration):
    """Home Assistant host reload."""
    _handle(ctx, "host/reload", "post")


@host.command("shutdown")
@pass_context
def host_shutdown(ctx: Configuration):
    """Home Assistant host shutdown."""
    _handle(ctx, "host/shutdown", "post")


@host.command("info")
@pass_context
def host_info(ctx: Configuration):
    """Home Assistant host info."""
    _handle(ctx, "host/info")


@host.command("options")
@pass_context
def host_options(ctx: Configuration):
    """Home Assistant options shutdown."""
    _handle(ctx, "host/options", "post")


@host.command("services")
@pass_context
def host_services(ctx: Configuration):
    """Home Assistant host reboot."""
    _handle(ctx, "host/services")


# Ingress endpoints
#########################################################################
@cli.group("ingress")
@pass_context
def ingress(ctx: Configuration):
    """Home Assistant ingress info."""
    ctx.auto_output("data")


@ingress.command("info")
@pass_context
def ingress_info(ctx: Configuration):
    """Home Assistant ingress info."""
    _handle(ctx, "ingress/panels")


# Jobs endpoints
#########################################################################
@cli.group("jobs")
@pass_context
def jobs(ctx: Configuration):
    """Home Assistant jobs info."""
    ctx.auto_output("data")


@jobs.command("info")
@pass_context
def jobs_info(ctx: Configuration):
    """Home Assistant jobs info."""
    _handle(ctx, "jobs/info")


# Root endpoints
#########################################################################
@cli.group("root")
@pass_context
def root(ctx: Configuration):
    """Home Assistant root info."""
    ctx.auto_output("data")


@root.command("info")
@pass_context
def root_info(ctx: Configuration):
    """Home Assistant root info."""
    _handle(ctx, "info")


@root.command("info")
@pass_context
def root_available_updates(ctx: Configuration):
    """Home Assistant root available updates."""
    _handle(ctx, "available_updates")


# Mount endpoints
#########################################################################
@cli.group("mount")
@pass_context
def mount(ctx: Configuration):
    """Home Assistant mount info."""
    ctx.auto_output("data")


@mount.command("info")
@pass_context
def mount_info(ctx: Configuration):
    """Home Assistant mount info."""
    _handle(ctx, "mounts")


# Multicast endpoints
#########################################################################
@cli.group("multicast")
@pass_context
def multicast(ctx: Configuration):
    """Home Assistant Multicast commands."""
    ctx.auto_output("data")


@multicast.command("info")
@pass_context
def multicast_info(ctx: Configuration):
    """Home Assistant Multicast info."""
    _handle(ctx, "multicast/info")


@multicast.command("update")
@pass_context
def multicast_update(ctx: Configuration):
    """Home Assistant Multicast update."""
    response = _handle_raw(ctx, "multicast/info")
    data = response["data"]
    current_version = int(data["version"])
    latest_version = int(data["version_latest"])
    if current_version == latest_version:
        ctx.echo("Already running the latest release")
    else:
        try:
            _handle(ctx, "multicast/update", "post")
        except (HomeAssistantCliError, HTTPError):
            pass


@multicast.command("restart")
@pass_context
def multicast_restart(ctx: Configuration):
    """Home Assistant Multicast restart."""
    try:
        _handle(ctx, "multicast/restart", "post")
    except HomeAssistantCliError:
        pass


@multicast.command("logs")
@pass_context
def multicast_logs(ctx: Configuration):
    """Home Assistant DNS logs."""
    _handle(ctx, "multicast/logs")


@multicast.command("stats")
@pass_context
def multicast_stats(ctx: Configuration):
    """Home Assistant Multicast stats."""
    _handle(ctx, "multicast/stats")


# Network endpoints
#########################################################################
@cli.group("network")
@pass_context
def network(ctx: Configuration):
    """Home Assistant Network commands."""
    ctx.auto_output("data")


@network.command("info")
@pass_context
def network_info(ctx: Configuration):
    """Home Assistant network info."""
    _handle(ctx, "network/info")


@network.command("reload")
@pass_context
def network_reload(ctx: Configuration):
    """Home Assistant Network reload."""
    try:
        _handle(ctx, "network/reload", "post")
    except HomeAssistantCliError:
        pass


# Observer endpoints
#########################################################################
@cli.group("observer")
@pass_context
def observer(ctx: Configuration):
    """Home Assistant Observer commands."""
    ctx.auto_output("data")


@observer.command("info")
@pass_context
def observer_info(ctx: Configuration):
    """Home Assistant observer info."""
    _handle(ctx, "observer/info")


@observer.command("stats")
@pass_context
def observer_stats(ctx: Configuration):
    """Home Assistant observer stats."""
    _handle(ctx, "observer/stats")


# OS endpoints
#########################################################################
@cli.group("os")
@pass_context
def os(ctx: Configuration):
    """Home Assistant Operating System commands."""
    ctx.auto_output("data")


@os.command("info")
@pass_context
def os_info(ctx: Configuration):
    """Home Assistant os info."""
    _handle(ctx, "os/info")


@os.command("swap")
@pass_context
def os_swap(ctx: Configuration):
    """Home Assistant os swap."""
    _handle(ctx, "os/config/swap")


@os.command("datadisk")
@pass_context
def os_datadisk(ctx: Configuration):
    """Home Assistant os datadisk."""
    _handle(ctx, "os/datadisk/list")


@os.command("update")
@pass_context
def os_update(ctx: Configuration):
    """Home Assistant Operating System update."""
    response = _handle_raw(ctx, "os/info")
    data = response["data"]
    current_version = data["version"]
    latest_version = data["version_latest"]
    if Version(current_version) == Version(latest_version):
        ctx.echo("Already running the latest release")
    else:
        try:
            _handle(ctx, "os/update", "post")
        except (HomeAssistantCliError, HTTPError):
            pass


# Resolution endpoints
#########################################################################
@cli.group("resolution")
@pass_context
def resolution(ctx: Configuration):
    """Home Assistant Resolution commands."""
    ctx.auto_output("data")


@resolution.command("info")
@pass_context
def resolution_info(ctx: Configuration):
    """Home Assistant resolution info."""
    _handle(ctx, "resolution/info")


# Services endpoints
#########################################################################
@cli.group("service")
@pass_context
def service(ctx: Configuration):
    """Home Assistant Service commands."""
    ctx.auto_output("data")


@service.command("info")
@pass_context
def service_info(ctx: Configuration):
    """Home Assistant service info."""
    _handle(ctx, "services")


@service.command("mqtt")
@pass_context
def service_mqtt(ctx: Configuration):
    """Home Assistant MQTT service info."""
    _handle(ctx, "services/mqtt")


@service.command("mysql")
@pass_context
def service_mysql(ctx: Configuration):
    """Home Assistant MySQL service info."""
    _handle(ctx, "services/mysql")


# Store endpoints
#########################################################################
@cli.group("store")
@pass_context
def store(ctx: Configuration):
    """Home Assistant Store commands."""
    ctx.auto_output("data")


@store.command("info")
@pass_context
def store_info(ctx: Configuration):
    """Home Assistant store info."""
    _handle(ctx, "store/info")


@store.command("addon")
@pass_context
def store_addon(ctx: Configuration):
    """Home Assistant addon store info."""
    _handle(ctx, "store/addons")


@store.command("repositories")
@pass_context
def store_repositories(ctx: Configuration):
    """Home Assistant store repositories info."""
    _handle(ctx, "store/repositories")


@store.command("reload")
@pass_context
def store_reload(ctx: Configuration):
    """Home Assistant Store reload."""
    try:
        _handle(ctx, "store/reload", "post")
    except HomeAssistantCliError:
        pass


# Security endpoints
#########################################################################
@cli.group("security")
@pass_context
def security(ctx: Configuration):
    """Home Assistant Security commands."""
    ctx.auto_output("data")


@security.command("info")
@pass_context
def security_info(ctx: Configuration):
    """Home Assistant security info."""
    _handle(ctx, "security/info")


# Supervisor endpoints
#########################################################################
@cli.group("supervisor")
@pass_context
def supervisor(ctx: Configuration):
    """Home Assistant supervisor commands."""
    ctx.auto_output("data")


@supervisor.command("ping")
@pass_context
def supervisor_ping(ctx: Configuration):
    """Home Assistant supervisor ping."""
    _handle(ctx, "supervisor/ping")


@supervisor.command("info")
@pass_context
def supervisor_info(ctx: Configuration):
    """Home Assistant supervisor info."""
    _handle(ctx, "supervisor/info")


@supervisor.command("update")
@pass_context
def supervisor_update(ctx: Configuration):
    """Home Assistant supervisor update."""
    response = _handle_raw(ctx, "supervisor/info")
    data = response["data"]
    current_version = int(data["version"])
    latest_version = int(data["version_latest"])
    if current_version == latest_version:
        ctx.echo("Already running the latest release")
    else:
        try:
            _handle(ctx, "supervisor/update", "post")
        except (HomeAssistantCliError, HTTPError):
            pass


@supervisor.command("options")
@pass_context
def supervisor_options(ctx: Configuration):
    """Home Assistant supervisor options."""
    _handle(ctx, "supervisor/options", "post")


@supervisor.command("reload")
@pass_context
def supervisor_reload(ctx: Configuration):
    """Home Assistant supervisor reload."""
    _handle(ctx, "supervisor/reload", "post")


@supervisor.command("logs")
@pass_context
def supervisor_logs(ctx: Configuration):
    """Home Assistant supervisor logs."""
    _handle(ctx, "supervisor/logs")


@supervisor.command("repair")
@pass_context
def supervisor_repair(ctx: Configuration):
    """Home Assistant supervisor repair."""
    _handle(ctx, "supervisor/repair", "post")


@supervisor.command("restart")
@pass_context
def supervisor_restart(ctx: Configuration):
    """Home Assistant supervisor restart."""
    try:
        _handle(ctx, "supervisor/restart", "post")
    except HomeAssistantCliError:
        pass


@supervisor.command("stats")
@pass_context
def supervisor_stats(ctx: Configuration):
    """Home Assistant supervisor stats."""
    _handle(ctx, "supervisor/stats")


================================================
FILE: homeassistant_cli/plugins/info.py
================================================
"""Information plugin for Home Assistant CLI (hass-cli)."""

import logging
from typing import Any, Dict, List

import click

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration, set_supervisor_server
from homeassistant_cli.const import __version__ as cli_version
from homeassistant_cli.helper import format_output, to_attributes

_LOGGING = logging.getLogger(__name__)


def redacted_output(token: str) -> str:
    """Redact the token for display."""
    return (
        token[:4] + "*" * (len(token) // 8 - 8) + token[-4:]
        if token and len(token) > 8
        else "***"
    )


@click.group("info")
@pass_context
def cli(ctx):
    """Display information about Home Assistant CLI."""


@cli.command()
@pass_context
def cli(ctx):
    """Show information about Home Assistant CLI."""
    information = {
        "Server URL": ctx.resolved_server if ctx.resolved_server else ctx.server,
        "Password": True if ctx.password else False,
        "Token": redacted_output(ctx.token),
        "Supervisor URL": set_supervisor_server(ctx),
        "Supervisor Token": redacted_output(ctx.supervisor_token),
        "Verbose": ctx.verbose,
        "Debug": ctx.debug,
        "Insecure": ctx.insecure,
        "Show Exceptions": ctx.showexceptions,
        "Timeout": ctx.timeout,
        "CLI version": cli_version,
    }

    click.echo(format_output(ctx, information))


================================================
FILE: homeassistant_cli/plugins/integration.py
================================================
"""Integrations (config entries) plugin for Home Assistant CLI (hass-cli)."""

import logging
import re
import sys

import click

import homeassistant_cli.helper as helper
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration

_LOGGING = logging.getLogger(__name__)


@click.group("integration")
@pass_context
def cli(ctx):
    """Get info and operate on integrations (config entries) from Home Assistant."""


@cli.command("list")
@click.argument("integration_filter", default=".*", required=False)
@pass_context
def list_integrations(ctx: Configuration, integration_filter: str):
    """List all integrations (config entries) from Home Assistant.

    INTEGRATION_FILTER - optional regex to filter by domain or title
    """
    ctx.auto_output("table")

    entries = api.get_config_entries(ctx)

    result = []
    if integration_filter == ".*":
        result = entries
    else:
        filter_regex = re.compile(integration_filter, re.IGNORECASE)

        for entry in entries:
            if filter_regex.search(entry.get("domain", "")) or filter_regex.search(
                entry.get("title", "")
            ):
                result.append(entry)

    cols = [
        ("ENTRY_ID", "entry_id"),
        ("DOMAIN", "domain"),
        ("TITLE", "title"),
        ("STATE", "state"),
        ("DISABLED_BY", "disabled_by"),
    ]

    ctx.echo(
        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)
    )


@cli.command("info")
@click.argument("entry_id", required=True)
@pass_context
def info(ctx: Configuration, entry_id: str):
    """Show detailed information about an integration.

    ENTRY_ID - the entry_id of the config entry
    """
    ctx.auto_output("data")

    entries = api.get_config_entries(ctx)

    # Find by entry_id or partial match
    entry = None
    for e in entries:
        if e.get("entry_id") == entry_id or e.get("entry_id", "").startswith(entry_id):
            entry = e
            break

    if not entry:
        _LOGGING.error("Could not find integration with entry_id: %s", entry_id)
        sys.exit(1)

    ctx.echo(helper.format_output(ctx, entry))


@cli.command("reload")
@click.argument("entry_id", required=True)
@pass_context
def reload(ctx: Configuration, entry_id: str):
    """Reload an integration.

    ENTRY_ID - the entry_id of the config entry to reload
    """
    ctx.auto_output("data")

    # Find full entry_id if partial match
    entries = api.get_config_entries(ctx)
    full_entry_id = None
    for e in entries:
        if e.get("entry_id") == entry_id or e.get("entry_id", "").startswith(entry_id):
            full_entry_id = e.get("entry_id")
            break

    if not full_entry_id:
        _LOGGING.error("Could not find integration with entry_id: %s", entry_id)
        sys.exit(1)

    result = api.reload_config_entry(ctx, full_entry_id)

    if result.get("success"):
        ctx.echo(f"Successfully reloaded integration: {full_entry_id}")
    else:
        ctx.echo(helper.format_output(ctx, result))


@cli.command("delete")
@click.argument("entry_id", required=True)
@click.option(
    "--confirm",
    is_flag=True,
    default=False,
    help="Confirm deletion without prompting",
)
@pass_context
def delete(ctx: Configuration, entry_id: str, confirm: bool):
    """Delete an integration.

    ENTRY_ID - the entry_id of the config entry to delete
    """
    ctx.auto_output("data")

    # Find full entry_id if partial match
    entries = api.get_config_entries(ctx)
    entry = None
    for e in entries:
        if e.get("entry_id") == entry_id or e.get("entry_id", "").startswith(entry_id):
            entry = e
            break

    if not entry:
        _LOGGING.error("Could not find integration with entry_id: %s", entry_id)
        sys.exit(1)

    full_entry_id = entry.get("entry_id")
    domain = entry.get("domain", "unknown")
    title = entry.get("title", "unknown")

    if not confirm:
        click.confirm(
            f"Are you sure you want to delete '{domain}' ({title}) [{full_entry_id}]?",
            abort=True,
        )

    result = api.delete_config_entry(ctx, full_entry_id)

    if result.get("success"):
        ctx.echo(f"Successfully deleted integration: {domain} ({title})")
    else:
        ctx.echo(helper.format_output(ctx, result))


@cli.command("disable")
@click.argument("entry_id", required=True)
@pass_context
def disable(ctx: Configuration, entry_id: str):
    """Disable an integration.

    ENTRY_ID - the entry_id of the config entry to disable
    """
    ctx.auto_output("data")

    # Find full entry_id if partial match
    entries = api.get_config_entries(ctx)
    entry = None
    for e in entries:
        if e.get("entry_id") == entry_id or e.get("entry_id", "").startswith(entry_id):
            entry = e
            break

    if not entry:
        _LOGGING.error("Could not find integration with entry_id: %s", entry_id)
        sys.exit(1)

    full_entry_id = entry.get("entry_id")
    result = api.disable_config_entry(ctx, full_entry_id, "user")

    if result.get("success"):
        ctx.echo(f"Successfully disabled integration: {full_entry_id}")
    else:
        ctx.echo(helper.format_output(ctx, result))


@cli.command("enable")
@click.argument("entry_id", required=True)
@pass_context
def enable(ctx: Configuration, entry_id: str):
    """Enable a disabled integration.

    ENTRY_ID - the entry_id of the config entry to enable
    """
    ctx.auto_output("data")

    # Find full entry_id if partial match
    entries = api.get_config_entries(ctx)
    entry = None
    for e in entries:
        if e.get("entry_id") == entry_id or e.get("entry_id", "").startswith(entry_id):
            entry = e
            break

    if not entry:
        _LOGGING.error("Could not find integration with entry_id: %s", entry_id)
        sys.exit(1)

    full_entry_id = entry.get("entry_id")
    result = api.disable_config_entry(ctx, full_entry_id, None)

    if result.get("success"):
        ctx.echo(f"Successfully enabled integration: {full_entry_id}")
    else:
        ctx.echo(helper.format_output(ctx, result))


@cli.command("list-disabled")
@pass_context
def list_disabled(ctx: Configuration):
    """List all disabled integrations (config entries) from Home Assistant."""
    ctx.auto_output("table")

    entries = api.get_config_entries(ctx)

    result = [entry for entry in entries if entry.get("disabled_by")]

    cols = [
        ("ENTRY_ID", "entry_id"),
        ("DOMAIN", "domain"),
        ("TITLE", "title"),
        ("DISABLED_BY", "disabled_by"),
    ]

    ctx.echo(
        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)
    )

@cli.command("list-loaded")
@pass_context
def list_loaded(ctx: Configuration):
    """List all loaded integrations (config entries) from Home Assistant."""
    ctx.auto_output("table")

    entries = api.get_config_entries(ctx)

    result = [entry for entry in entries if entry.get("state") == "loaded"]

    cols = [
        ("ENTRY_ID", "entry_id"),
        ("DOMAIN", "domain"),
        ("TITLE", "title"),
        ("STATE", "state"),
    ]

    ctx.echo(
        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)
    )


@cli.command("list-unloaded")
@pass_context
def list_unloaded(ctx: Configuration):
    """List all unloaded integrations (config entries) from Home Assistant."""
    ctx.auto_output("table")

    entries = api.get_config_entries(ctx)

    result = [entry for entry in entries if entry.get("state") != "loaded"]

    cols = [
        ("ENTRY_ID", "entry_id"),
        ("DOMAIN", "domain"),
        ("TITLE", "title"),
        ("STATE", "state"),
    ]

    ctx.echo(
        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)
    )


================================================
FILE: homeassistant_cli/plugins/map.py
================================================
"""Map plugin for Home Assistant CLI (hass-cli)."""

import sys
import webbrowser

import click

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration

OSM_URL = "https://www.openstreetmap.org/"
GOOGLE_URL = "https://www.google.com/maps/search/"
BING_URL = "https://www.bing.com/maps"
SERVICE = {
    "openstreetmap": OSM_URL + "?mlat={0}&mlon={1}#map=17/{0}/{1}",
    "google": GOOGLE_URL + "?api=1&query={0},{1}",
    "bing": BING_URL + "?v=2&cp={0}~{1}&lvl=17&sp=point.{0}_{1}_{2}",
}


@click.command("map")
@click.argument(
    "entity",
    required=False,
    shell_complete=autocompletion.entities,  # type: ignore
)
@click.option("--service", default="openstreetmap", type=click.Choice(SERVICE.keys()))
@pass_context
def cli(ctx: Configuration, service: str, entity: str) -> None:
    """Show the location of the config or an entity on a map."""
    latitude = None
    longitude = None

    if entity:
        thing = entity
        data = api.get_state(ctx, entity)
        if data:
            attr = data.get("attributes", {})
            latitude = attr.get("latitude")
            longitude = attr.get("longitude")
            thing = attr.get("friendly_name", entity)
    else:
        thing = "configuration"
        response = api.get_config(ctx)
        if response:
            latitude = response.get("latitude")
            longitude = response.get("longitude")
            thing = response.get("location_name", thing)

    if latitude and longitude:
        urlpattern = SERVICE.get(service)
        import urllib.parse

        if urlpattern:
            url = urlpattern.format(latitude, longitude, urllib.parse.quote_plus(thing))
            ctx.echo(f"{thing} location is at {latitude}, {longitude}")
            webbrowser.open_new_tab(url)
        else:
            ctx.echo(f"Could not find URL pattern for service {service}")
    else:
        ctx.echo(f"No exact location info found in {thing}")
        sys.exit(2)


================================================
FILE: homeassistant_cli/plugins/raw.py
================================================
"""Raw plugin for Home Assistant CLI (hass-cli)."""

import json as json_
import logging
from typing import Any, Dict, List, cast

import click

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration
from homeassistant_cli.helper import format_output

_LOGGING = logging.getLogger(__name__)


@click.group("raw")
@pass_context
def cli(ctx: Configuration):
    """Call the raw API (advanced)."""
    ctx.auto_output("data")


def _report(ctx, cmd, method, response) -> None:
    """Create a report."""
    response.raise_for_status()

    if response.ok:
        try:
            ctx.echo(format_output(ctx, response.json()))
        except json_.decoder.JSONDecodeError:
            _LOGGING.debug("Response could not be parsed as JSON")
            ctx.echo(response.text)
    else:
        _LOGGING.warning(
            "%s: <No output returned from %s %s>",
            response.status_code,
            cmd,
            method,
        )


@cli.command()
@click.argument(
    "method",
    shell_complete=autocompletion.api_methods,  # type: ignore
)
@pass_context
def get(ctx: Configuration, method):
    """Do a GET request against api/<method>."""
    response = api.restapi(ctx, "get", method)

    _report(ctx, "GET", method, response)


@cli.command()
@click.argument(
    "method",
    shell_complete=autocompletion.api_methods,  # type: ignore
)
@click.option("--json")
@pass_context
def post(ctx: Configuration, method, json):
    """Do a POST request against api/<method>."""
    if json:
        data = json_.loads(
            json if json != "-" else click.get_text_stream("stdin").read()
        )
    else:
        data = {}

    response = api.restapi(ctx, "post", method, data)

    _report(ctx, "GET", method, response)


@cli.command("ws")
@click.argument(
    "wstype",
    shell_complete=autocompletion.wsapi_methods,  # type: ignore
)
@click.option("--json")
@pass_context
def websocket(ctx: Configuration, wstype, json):
    r"""Send a websocket request against /api/websocket.

    WSTYPE is name of websocket methods.

    \b
    --json is dictionary to pass in addition to the type.
           Example: --json='{ "area_id":"2c8bf93c8082492f99c989896962f207" }'
    """
    if json:
        data = json_.loads(
            json if json != "-" else click.get_text_stream("stdin").read()
        )
    else:
        data = {}

    frame = {"type": wstype}
    frame = {**frame, **data}  # merging data into frame

    response = cast(list[dict[str, Any]], api.wsapi(ctx, frame))

    ctx.echo(format_output(ctx, response))


================================================
FILE: homeassistant_cli/plugins/service.py
================================================
"""Service plugin for Home Assistant CLI (hass-cli)."""

import logging
import re as reg
import sys
from typing import Any, Dict, List

import click

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration
from homeassistant_cli.helper import format_output, to_attributes

_LOGGING = logging.getLogger(__name__)


@click.group("service")
@pass_context
def cli(ctx):
    """Call and work with services."""


@cli.command("list")
@click.argument("servicefilter", default=".*", required=False)
@pass_context
def list_cmd(ctx: Configuration, servicefilter):
    """Get list of services."""
    ctx.auto_output("table")
    services = api.get_services(ctx)
    service_filter = servicefilter

    result = []  # type: List[Dict[Any,Any]]
    if service_filter == ".*":
        result = services
    else:
        result = services
        service_filter_re = reg.compile(service_filter)  # type: Pattern

        domains = []
        for domain in services:
            domain_name = domain["domain"]
            domain_data = {}
            services_dict = domain["services"]
            service_data = {}
            for service in services_dict:
                if service_filter_re.search(f"{domain_name}.{service}"):
                    service_data[service] = services_dict[service]

            if service_data:
                domain_data["services"] = service_data
                domain_data["domain"] = domain_name
                domains.append(domain_data)
        result = domains

    flatten_result = []  # type: List[Dict[str,Any]]
    for domain in result:
        for service in domain["services"]:
            item = {}
            item["domain"] = domain["domain"]
            item["service"] = service
            item = {**item, **domain["services"][service]}
            flatten_result.append(item)

    cols = [
        ("DOMAIN", "domain"),
        ("SERVICE", "service"),
        ("DESCRIPTION", "description"),
    ]
    ctx.echo(
        format_output(ctx, flatten_result, columns=ctx.columns if ctx.columns else cols)
    )


@cli.command("call")
@click.argument(
    "service",
    required=True,
    shell_complete=autocompletion.services,  # type: ignore
)
@click.option(
    "--arguments", help="Comma separated key/value pairs to use as arguments."
)
@pass_context
def call(ctx: Configuration, service, arguments):
    """Call a service."""
    ctx.auto_output("data")
    _LOGGING.debug("service call <start>")
    parts = service.split(".")
    if len(parts) != 2:
        _LOGGING.error("Service name not following <domain>.<service> format")
        sys.exit(1)

    _LOGGING.debug("Convert arguments %s to dict", arguments)
    data = to_attributes(arguments)

    _LOGGING.debug("service call_service")

    result = api.call_service(ctx, parts[0], parts[1], data)

    _LOGGING.debug("Formatting output")
    ctx.echo(format_output(ctx, result))


================================================
FILE: homeassistant_cli/plugins/state.py
================================================
"""Entity plugin for Home Assistant CLI (hass-cli)."""

import json as json_
import logging
import re
from typing import Any, Dict, List

import click

import homeassistant_cli.autocompletion as autocompletion
import homeassistant_cli.const as const
import homeassistant_cli.helper as helper
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration

_LOGGING = logging.getLogger(__name__)


@click.group("state")
@pass_context
def cli(ctx):
    """Get info on entity state from Home Assistant."""


@cli.command()
@click.argument(
    "entity",
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@pass_context
def get(ctx: Configuration, entity):
    """Get/read entity state from Home Assistant."""
    ctx.auto_output("table")
    state = api.get_state(ctx, entity)

    if state:
        ctx.echo(
            helper.format_output(
                ctx,
                [state],
                columns=ctx.columns if ctx.columns else const.COLUMNS_ENTITIES,
            )
        )
    else:
        _LOGGING.error("Entity with ID: '%s' not found.", entity)


@cli.command()
@click.argument(
    "entity",
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@pass_context
def delete(ctx: Configuration, entity):
    """Delete entity state from Home Assistant."""
    ctx.auto_output("table")
    deleted = api.remove_state(ctx, entity)

    if deleted:
        ctx.echo("State for entity %s deleted.", entity)
    else:
        ctx.echo("Entity %s not found.", entity)


@cli.command("list")
@click.argument("entityfilter", default=".*", required=False)
@pass_context
def list_command(ctx, entityfilter):
    """List all state from Home Assistant."""
    ctx.auto_output("table")
    states = api.get_states(ctx)
    entity_filter = entityfilter

    result = []  # type: List[Dict]
    if entity_filter == ".*":
        result = states
    else:
        entity_filter_re = re.compile(entity_filter)  # type: Pattern

        for entity in states:
            if entity_filter_re.search(entity["entity_id"]):
                result.append(entity)
    ctx.echo(
        helper.format_output(
            ctx,
            result,
            columns=ctx.columns if ctx.columns else const.COLUMNS_ENTITIES,
        )
    )


@cli.command()
@click.argument(
    "entity",
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@click.argument("newstate", required=False)
@click.option(
    "--attributes",
    help="Comma separated key/value pairs to use as attributes.",
)
@click.option(
    "--json",
    help="Raw JSON state to use for setting. Overrides any otherstate values provided.",
)
@click.option(
    "--merge",
    is_flag=True,
    default=False,
    help="If set and the entity state exists the state and attributes will"
    "be merged into the state rather than overwrite.",
    show_default=True,
)
@pass_context
def edit(ctx: Configuration, entity, newstate, attributes, merge, json):
    """Edit entity state from Home Assistant."""
    ctx.auto_output("data")
    new_state = newstate
    if json:
        _LOGGING.debug("JSON found overriding/creating new state for entity %s", entity)
        wanted_state = json_.loads(json)
    elif new_state or attributes:
        wanted_state = {}
        existing_state = api.get_state(ctx, entity)

        if existing_state:
            ctx.echo("Existing state found for %s", entity)
            if merge:
                wanted_state = existing_state
        else:
            ctx.echo("No existing state found for '%s'", entity)

        if attributes:
            attributes_dict = helper.to_attributes(attributes)

            new_attr = wanted_state.get("attributes", {})
            new_attr.update(attributes_dict)
            # This is not honoring merge!
            wanted_state["attributes"] = new_attr

        if newstate:
            wanted_state["state"] = newstate
        else:
            if not existing_state:
                raise ValueError("No new or existing state provided.")
            wanted_state["state"] = existing_state["state"]

    else:
        existing = api.get_state(ctx, entity)
        if existing:
            existing_raw = helper.raw_format_output(ctx.output, existing, ctx.yaml())
        else:
            existing_raw = helper.raw_format_output(ctx.output, {}, ctx.yaml())

        new = click.edit(existing_raw, extension=f".{ctx.output}")

        if new is not None:
            ctx.echo("Updating '%s'", entity)
            if ctx.output == "yaml":
                wanted_state = ctx.yamlload(new)
            if ctx.output == "json":
                wanted_state = json_.loads(new)

            api.set_state(ctx, entity, wanted_state)
        else:
            ctx.echo("No edits/changes returned from editor.")
            return

    _LOGGING.debug("wanted: %s", str(wanted_state))
    result = api.set_state(ctx, entity, wanted_state)
    ctx.echo("Entity %s updated successfully", entity)
    _LOGGING.debug("Updated to: %s", result)


def _report(ctx: Configuration, result: list[dict[str, Any]], action: str):
    """Create a report."""
    ctx.echo(
        helper.format_output(
            ctx,
            result,
            columns=ctx.columns if ctx.columns else const.COLUMNS_ENTITIES,
        )
    )
    if ctx.verbose:
        ctx.echo("%s entities reported to be %s", len(result), action)


def _homeassistant_cmd(ctx: Configuration, entities, cmd, action):
    """Run command on Home Assistant."""
    data = {"entity_id": entities}
    _LOGGING.debug("%s on %s", cmd, entities)
    result = api.call_service(ctx, "homeassistant", cmd, data)

    _report(ctx, result, action)


@cli.command()
@click.argument(
    "entities",
    nargs=-1,
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@pass_context
def toggle(ctx: Configuration, entities):
    """Toggle state for one or more entities in Home Assistant."""
    ctx.auto_output("table")
    _homeassistant_cmd(ctx, entities, "toggle", "toggled")


@cli.command("turn_off")
@click.argument(
    "entities",
    nargs=-1,
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@pass_context
def off_cmd(ctx: Configuration, entities):
    """Turn entity off."""
    ctx.auto_output("table")
    _homeassistant_cmd(ctx, entities, "turn_off", "turned off")


@cli.command("turn_on")
@click.argument(
    "entities",
    nargs=-1,
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@pass_context
def on_cmd(ctx: Configuration, entities):
    """Turn entity on."""
    ctx.auto_output("table")
    _homeassistant_cmd(ctx, entities, "turn_on", "turned on")


@cli.command()
@click.argument(
    "entities",
    nargs=-1,
    required=True,
    shell_complete=autocompletion.entities,  # type: ignore
)
@click.option(
    "--since",
    required=False,
    default="1d",
    help="Start of the period to get history from. A timestamp or relative "
    "expression relative to now. Defaults to 1 day.",
)
@click.option(
    "--end",
    required=False,
    default="now",
    help="End of the period to query history from. A timestamp or relative "
    "expression relative to now. Defaults to now.",
)
@pass_context
def history(ctx: Configuration, entities: list, since: str, end: str):
    """Get state history from Home Assistant, all or per entity.

    You can use `--since` and `--end` to narrow or expand the time period.

    Both options accepts a full timestamp i.e. `2016-02-06T22:15:00+00:00`
    or a relative expression i.e. `3m` for three minutes, `5d` for 5 days.
    Even `3 minutes` or `5 days` will work.
    See https://dateparser.readthedocs.io/en/latest/#features for examples.
    """
    import dateparser

    ctx.auto_output("table")
    settings = {
        "DATE_ORDER": "DMY",
        "TIMEZONE": "UTC",
        "RETURN_AS_TIMEZONE_AWARE": True,
    }

    start_time = dateparser.parse(since, settings=settings)

    end_time = dateparser.parse(end, settings=settings)

    delta = end_time - start_time

    if ctx.verbose:
        click.echo(
            f"Querying from {since}:{start_time.isoformat()} to "
            f"{end}:{end_time.isoformat()} a span of {delta}"
        )

    data = api.get_history(ctx, list(entities), start_time, end_time)

    result = []  # type: List[Dict[str, Any]]
    entity_count = 0
    for item in data:
        result.extend(item)  # type: ignore
        entity_count = entity_count + 1

    click.echo(
        helper.format_output(
            ctx,
            result,
            columns=ctx.columns if ctx.columns else const.COLUMNS_ENTITIES,
        )
    )

    if ctx.verbose:
        click.echo(
            f"History with {len(result)} rows from {entity_count} entities found."
        )


================================================
FILE: homeassistant_cli/plugins/system.py
================================================
"""System plugin for Home Assistant CLI (hass-cli)."""

import logging

import click

import homeassistant_cli.const as const
import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration
from homeassistant_cli.helper import format_output

_LOGGING = logging.getLogger(__name__)


@click.group("system")
@pass_context
def cli(ctx):
    """System details and operations for Home Assistant."""


@cli.command()
@pass_context
def log(ctx):
    """Get errors from Home Assistant."""
    click.echo(api.get_raw_error_log(ctx))


@cli.command()
@pass_context
def health(ctx: Configuration):
    """Get system health from Home Assistant."""
    info = api.get_health(ctx)

    ctx.echo(
        format_output(
            ctx,
            [info],
            columns=ctx.columns if ctx.columns else const.COLUMNS_DEFAULT,
        )
    )


================================================
FILE: homeassistant_cli/plugins/template.py
================================================
"""Template plugin for Home Assistant CLI (hass-cli)."""

import logging
import os

import click
from jinja2 import FileSystemLoader
from jinja2.exceptions import SecurityError
from jinja2.sandbox import ImmutableSandboxedEnvironment

import homeassistant_cli.remote as api
from homeassistant_cli.cli import pass_context
from homeassistant_cli.config import Configuration
from homeassistant_cli.exceptions import HomeAssistantCliError, UnsafeTemplateError

_LOGGING = logging.getLogger(__name__)

# Allowlist of environment variables accessible from templates
SAFE_ENV_VARS = frozenset({
    "HASS_SERVER",
    "LANG",
    "TZ",
})


def _safe_environ_get(key: str, default: str | None = None) -> str | None:
    """Return env var only if it is in the allowlist."""
    if key in SAFE_ENV_VARS:
        return os.environ.get(key, default)
    return default


def render(template_path, data, strict=False) -> str:
    """Render template."""
    env = ImmutableSandboxedEnvironment(
        loader=FileSystemLoader(os.path.dirname(template_path)),
        keep_trailing_newline=True,
    )
    if strict:
        from jinja2 import StrictUndefined

        env.undefined = StrictUndefined

    # Add environ global (allowlisted)
    env.globals["environ"] = _safe_environ_get

    try:
        output = env.get_template(os.path.basename(template_path)).render(data)
    except SecurityError as err:
        raise UnsafeTemplateError(
            f"Template '{os.path.basename(template_path)}' contains unsafe "
            f"operations: {err}"
        ) from None
    return output


@click.command("template")
@click.argument("template", required=True, type=click.File())
@click.argument("datafile", type=click.File(), required=False)
@click.option(
    "--local",
    default=False,
    is_flag=True,
    help="If should render template locally.",
)
@pass_context
def cli(ctx: Configuration, template, datafile, local: bool) -> None:
    """Render templates on server or locally.

    TEMPLATE - jinja2 template file
    DATAFILE - YAML file with variables to pass to rendering
    """
    variables = {}  # type: Dict[str, Any]
    if datafile:
        variables = ctx.yamlload(datafile)

    template_string = template.read()

    _LOGGING.debug("Rendering: %s Variables: %s", template_string, variables)

    try:
        if local:
            output = render(template.name, variables, True)
        else:
            output = api.render_template(ctx, template_string, variables)
    except (UnsafeTemplateError, HomeAssistantCliError) as err:
        raise click.ClickException(str(err)) from None

    ctx.echo(output)


================================================
FILE: homeassistant_cli/remote.py
================================================
"""
Basic API to access remote instance of Home Assistant.

If a connection error occurs while communicating with the API a
HomeAssistantCliError will be raised.
"""

import asyncio
import collections
import enum
import json
import logging
import urllib.parse
from collections.abc import Callable
from datetime import datetime
from typing import Any, cast
from urllib.parse import urlencode

import aiohttp
import requests

import homeassistant_cli.hassconst as hass
from homeassistant_cli.config import (
    Configuration,
    resolve_server,
    set_supervisor_server,
)
from homeassistant_cli.exceptions import HomeAssistantCliError

_LOGGER = logging.getLogger(__name__)

# Copied from aiohttp.hdrs
CONTENT_TYPE = "Content-Type"
METH_DELETE = "DELETE"
METH_GET = "GET"
METH_POST = "POST"


class APIStatus(enum.Enum):
    """Representation of an API status."""

    OK = "ok"
    INVALID_PASSWORD = "invalid_password"
    CANNOT_CONNECT = "cannot_connect"
    UNKNOWN = "unknown"

    def __str__(self) -> str:
        """Return the state."""
        return self.value  # type: ignore


def restapi(
    ctx: Configuration, method: str, path: str, data: dict | None = None
) -> requests.Response:
    """Make a call to the Home Assistant REST API."""
    if data is None:
        data_str = None
    else:
        data_str = json.dumps(data, cls=JSONEncoder)

    if not ctx.session:
        ctx.session = requests.Session()
        ctx.session.verify = not ctx.insecure
        if ctx.cert:
            ctx.session.cert = ctx.cert

        _LOGGER.debug(
            "Session: verify(%s), cert(%s)",
            ctx.session.verify,
            ctx.session.cert,
        )

    headers = {CONTENT_TYPE: hass.CONTENT_TYPE_JSON}  # type: Dict[str, Any]

    if ctx.token:
        headers["Authorization"] = f"Bearer {ctx.token}"
    if ctx.password:
        headers["x-ha-access"] = ctx.password

    url = urllib.parse.urljoin(resolve_server(ctx) + path, "")

    try:
        if method == METH_GET:
            return requests.get(url, params=data_str, headers=headers)

        return requests.request(method, url, data=data_str, headers=headers)

    except requests.exceptions.ConnectionError:
        raise HomeAssistantCliError(f"Error connecting to {url}") from None

    except requests.exceptions.Timeout:
        error = f"Timeout when talking to {url}"
        _LOGGER.exception(error)
        raise HomeAssistantCliError(error) from None


def restapi_supervisor(
    ctx: Configuration, method: str, path: str, data: dict | None = None
) -> requests.Response:
    """Make a call to the Supervisor REST API."""
    if data is None:
        data_str = None
    else:
        data_str = json.dumps(data, cls=JSONEncoder)

    if not ctx.session:
        ctx.session = requests.Session()
        ctx.session.verify = not ctx.insecure
        if ctx.cert:
            ctx.session.cert = ctx.cert

        _LOGGER.debug(
            "Session: verify(%s), cert(%s)",
            ctx.session.verify,
            ctx.session.cert,
        )

    headers = {CONTENT_TYPE: hass.CONTENT_TYPE_JSON}  # type: Dict[str, Any]

    if ctx.token:
        headers["Authorization"] = f"Bearer {ctx.supervisor_token}"

    url = urllib.parse.urljoin(set_supervisor_server(ctx) + path, "")

    try:
        if method == METH_GET:
            return requests.get(url, params=data_str, headers=headers)

        return requests.request(method, url, data=data_str, headers=headers)

    except requests.exceptions.ConnectionError:
        raise HomeAssistantCliError(f"Error connecting to {url}") from None

    except requests.exceptions.Timeout:
        error = f"Timeout when talking to {url}"
        _LOGGER.exception(error)
        raise HomeAssistantCliError(error) from None


def wsapi(
    ctx: Configuration,
    frame: dict,
    callback: Callable[[dict], Any] | None = None,
) -> dict | None:
    """Make a call to Home Assistant using WS API.

    if callback provided will keep listening and call
    on every message.

    If no callback return data returned.
    """
    async def fetcher() -> dict | None:
        """Fetch data from WS API."""
        async with aiohttp.ClientSession() as session:
            async with session.ws_connect(
                resolve_server(ctx) + "/api/websocket",
                max_msg_size=16 * 1024 * 1024,  # 16MB to handle large responses
            ) as wsconn:
                await wsconn.send_str(
                    json.dumps({"type": "auth", "access_token": ctx.token})
                )

                frame["id"] = 1

                await wsconn.send_str(json.dumps(frame))

                while True:
                    msg = await wsconn.receive()
                    if msg.type == aiohttp.WSMsgType.ERROR:
                        break
                    elif msg.type == aiohttp.WSMsgType.CLOSED:
                        break
                    elif msg.type == aiohttp.WSMsgType.TEXT:
                        mydata = json.loads(msg.data)  # type: Dict

                        if callback:
                            callback(mydata)
                        elif mydata["type"] == "result":
                            return mydata
                        elif mydata["type"] == "auth_invalid":
                            raise HomeAssistantCliError(mydata.get("message"))
        return None

    result = asyncio.run(fetcher())
    return result


class JSONEncoder(json.JSONEncoder):
    """JSONEncoder that supports Home Assistant objects."""

    # pylint: disable=method-hidden
    def default(self, o: Any) -> Any:
        """Convert Home Assistant objects.

        Hand other objects to the original method.
        """
        if isinstance(o, datetime):
            return o.isoformat()
        if isinstance(o, set):
            return list(o)
        if hasattr(o, "as_dict"):
            return o.as_dict()

        return json.JSONEncoder.default(self, o)


def get_areas(ctx: Configuration) -> list[dict[str, Any]]:
    """Return all areas."""
    frame = {"type": hass.WS_TYPE_AREA_REGISTRY_LIST}

    areas = cast(dict, wsapi(ctx, frame))["result"]  # type: List[Dict[str, Any]]

    return areas


def find_area(ctx: Configuration, id_or_name: str) -> dict[str, str] | None:
    """Find area first by id and if no match by name."""
    areas = get_areas(ctx)

    area = next((x for x in areas if x["area_id"] == id_or_name), None)
    if not area:
        area = next((x for x in areas if x["name"] == id_or_name), None)

    return area


def create_area(ctx: Configuration, name: str) -> dict[str, Any]:
    """Create area."""
    frame = {"type": hass.WS_TYPE_AREA_REGISTRY_CREATE, "name": name}

    return cast(dict[str, Any], wsapi(ctx, frame))


def delete_area(ctx: Configuration, area_id: str) -> dict[str, Any]:
    """Delete area."""
    frame = {"type": hass.WS_TYPE_AREA_REGISTRY_DELETE, "area_id": area_id}

    return cast(dict[str, Any], wsapi(ctx, frame))


def rename_area(ctx: Configuration, area_id: str, new_name: str) -> dict[str, Any]:
    """Rename area."""
    frame = {
        "type": hass.WS_TYPE_AREA_REGISTRY_UPDATE,
        "area_id": area_id,
        "name": new_name,
    }

    return cast(dict[str, Any], wsapi(ctx, frame))


def rename_entity(
    ctx: Configuration,
    entity_id: str,
    new_id: str | None,
    new_name: str | None,
) -> dict[str, Any]:
    """Rename entity."""
    frame = {
        "type": hass.WS_TYPE_ENTITY_REGISTRY_UPDATE,
        "entity_id": entity_id,
    }

    if new_name:
        frame["name"] = new_name
    if new_id:
        frame["new_entity_id"] = new_id

    return cast(dict[str, Any], wsapi(ctx, frame))


def rename_device(ctx: Configuration, device_id: str, new_name: str) -> dict[str, Any]:
    """Rename device."""
    frame = {
        "type": hass.WS_TYPE_DEVICE_REGISTRY_UPDATE,
        "device_id": device_id,
        "name_by_user": new_name,
    }

    return cast(dict[str, Any], wsapi(ctx, frame))


def assign_area(ctx: Configuration, device_id: str, area_id: str) -> dict[str, Any]:
    """Assign area."""
    frame = {
        "type": hass.WS_TYPE_DEVICE_REGISTRY_UPDATE,
        "area_id": area_id,
        "device_id": device_id,
    }

    return cast(dict[str, Any], wsapi(ctx, frame))


def assign_entity_area(
    ctx: Configuration, entity_id: str, area_id: str
) -> dict[str, Any]:
    """Assign area to entity."""
    frame = {
        "type": hass.WS_TYPE_ENTITY_REGISTRY_UPDATE,
        "area_id": area_id,
        "entity_id": entity_id,
    }

    return cast(dict[str, Any], wsapi(ctx, frame))


def delete_entity(ctx: Configuration, entity_id: str) -> dict[str, Any]:
    """Delete entity from registry."""
    frame = {
        "type": hass.WS_TYPE_ENTITY_REGISTRY_REMOVE,
        "entity_id": entity_id,
    }

    return cast(dict[str, Any], wsapi(ctx, frame))


def enable_entity(
    ctx: Configuration, entity_id: str, disabled_by: str | None
) -> dict[str, Any]:
    """Enable or disable an entity."""
    frame = {
        "type": hass.WS_TYPE_ENTITY_REGISTRY_UPDATE,
        "entity_id": entity_id,
        "disabled_by": disabled_by,
    }

    return cast(dict[str, Any], wsapi(ctx, frame))


def get_health(ctx: Configuration) -> dict[str, Any]:
    """Get system Health."""
    frame = {"type": "system_health/info"}

    info = cast(dict[str, dict[str, Any]], wsapi(ctx, frame))["result"]

    return info


def get_devices(ctx: Configuration) -> list[dict[str, Any]]:
    """Return all devices."""
    frame = {"type": hass.WS_TYPE_DEVICE_REGISTRY_LIST}

    devices = cast(dict[str, list[dict[str, Any]]], wsapi(ctx, frame))["result"]

    return devices


def get_entities(ctx: Configuration) -> list[dict[str, Any]]:
    """Return all entities."""
    frame = {"type": hass.WS_TYPE_ENTITY_REGISTRY_LIST}

    devices = cast(dict[str, list[dict[str, Any]]], wsapi(ctx, frame))["result"]

    return devices


def get_entity(ctx: Configuration, entity_id: str) -> list[dict[str, Any]]:
    """Return id."""
    frame = {"type": hass.WS_TYPE_ENTITY_REGISTRY_GET, "entity_id": entity_id}

    result = cast(dict[str, list[dict[str, Any]]], wsapi(ctx, frame))

    return result["id"]


def get_config_entries(ctx: Configuration) -> list[dict[str, Any]]:
    """Return all config entries (integrations)."""
    req = restapi(ctx, METH_GET, "/api/config/config_entries/entry")
    req.raise_for_status()
    return cast(list[dict[str, Any]], req.json())


def get_config_entry(ctx: Configuration, entry_id: str) -> dict[str, Any]:
    """Return a specific config entry."""
    frame = {"type": hass.WS_TYPE_CONFIG_ENTRIES_GET_SINGLE, "entry_id": entry_id}
    result = cast(dict[str, Any], wsapi(ctx, frame))
    return result.get("result", {}).get("config_entry", {})


def reload_config_entry(ctx: Configuration, entry_id: str) -> dict[str, Any]:
    """Reload a config entry via REST API."""
    req = restapi(ctx, METH_POST, f"/api/config/config_entries/entry/{entry_id}/reload")
    if req.status_code == 200:
        return {"success": True, **req.json()}
    elif req.status_code == 404:
        return {"success": False, "error": "Config entry not found"}
    elif req.status_code == 403:
        return {"success": False, "error": "Entry cannot be reloaded"}
    else:
        req.raise_for_status()
        return {"success": False}


def delete_config_entry(ctx: Configuration, entry_id: str) -> dict[str, Any]:
    """Delete a config entry via REST API."""
    req = restapi(ctx, METH_DELETE, f"/api/config/config_entries/entry/{entry_id}")
    if req.status_code == 200:
        return {"success": True, **req.json()}
    elif req.status_code == 404:
        return {"success": False, "error": "Config entry not found"}
    else:
        req.raise_for_status()
        return {"success": False}


def disable_config_entry(
    ctx: Configuration, entry_id: str, disabled_by: str | None
) -> dict[str, Any]:
    """Enable or disable a config entry.

    Set disabled_by to "user" to disable, or None to enable.
    """
    frame = {
        "type": hass.WS_TYPE_CONFIG_ENTRIES_DISABLE,
        "entry_id": entry_id,
        "disabled_by": disabled_by,
    }
    return cast(dict[str, Any], wsapi(ctx, frame))


def validate_api(ctx: Configuration) -> APIStatus:
    """Make a call to validate API."""
    try:
        req = restapi(ctx, METH_GET, hass.URL_API)

        if req.status_code == 200:
            return APIStatus.OK

        if req.status_code == 401:
            return APIStatus.INVALID_PASSWORD

        return APIStatus.UNKNOWN

    except HomeAssistantCliError:
        return APIStatus.CANNOT_CONNECT


def get_info(ctx: Configuration) -> dict[str, Any]:
    """Get basic info about the Home Assistant instance."""
    try:
        req = restapi(ctx, METH_GET, hass.URL_API_CONFIG)

        req.raise_for_status()

        return cast(dict[str, Any], req.json()) if req.status_code == 200 else {}

    except (HomeAssistantCliError, ValueError) as exception:
        raise HomeAssistantCliError(
            "Unexpected error retrieving information"
        ) from exception
        # ValueError if req.json() can't parse the json


def get_events(ctx: Configuration) -> dict[str, Any]:
    """Return all events."""
    try:
        req = restapi(ctx, METH_GET, hass.URL_API_EVENTS)
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Unexpected error getting events: {exception}"
        ) from exception

    if req.status_code == 200:
        return cast(dict[str, Any], req.json())

    raise HomeAssistantCliError(f"Error while getting all events: {req.text}")


def get_history(
    ctx: Configuration,
    entities: list | None = None,
    start_time: datetime | None = None,
    end_time: datetime | None = None,
) -> list[dict[str, Any]]:
    """Return History."""
    try:
        if start_time:
            method = hass.URL_API_HISTORY_PERIOD.format(start_time.isoformat())
        else:
            method = hass.URL_API_HISTORY

        params = collections.OrderedDict()  # type: Dict[str, str]

        if entities:
            params["filter_entity_id"] = ",".join(entities)
        if end_time:
            params["end_time"] = end_time.isoformat()

        if params:
            method = f"{method}?{urlencode(params)}"

        req = restapi(ctx, METH_GET, method)
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Unexpected error getting history: {exception}"
        ) from exception

    if req.status_code == 200:
        return cast(list[dict[str, Any]], req.json())

    raise HomeAssistantCliError(f"Error while getting all events: {req.text}")


def get_states(ctx: Configuration) -> list[dict[str, Any]]:
    """Return all states."""
    try:
        req = restapi(ctx, METH_GET, hass.URL_API_STATES)
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Unexpected error getting state: {exception}"
        ) from exception

    if req.status_code == 200:
        data = req.json()  # type: List[Dict[str, Any]]
        return data

    raise HomeAssistantCliError(f"Error while getting all states: {req.text}")


def get_raw_error_log(ctx: Configuration) -> str:
    """Return the error log."""
    try:
        req = restapi(ctx, METH_GET, hass.URL_API_ERROR_LOG)
        req.raise_for_status()
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Unexpected error getting error log: {exception}"
        ) from exception

    return req.text


def get_config(ctx: Configuration) -> dict[str, Any]:
    """Return the running configuration."""
    try:
        req = restapi(ctx, METH_GET, hass.URL_API_CONFIG)
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Unexpected error getting configuration: {exception}"
        ) from exception

    if req.status_code == 200:
        return cast(dict[str, str], req.json())

    raise HomeAssistantCliError(f"Error while getting all configuration: {req.text}")


def get_state(ctx: Configuration, entity_id: str) -> dict[str, Any] | None:
    """Get entity state. If ok, return dictionary with state.

    If no entity found return None - otherwise exception raised
    with details.
    """
    try:
        req = restapi(ctx, METH_GET, hass.URL_API_STATES_ENTITY.format(entity_id))
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Unexpected error getting state: {exception}"
        ) from exception

    if req.status_code == 200:
        return cast(dict[str, Any], req.json())
    if req.status_code == 404:
        return None

    raise HomeAssistantCliError(f"Error while getting Entity {entity_id}: {req.text}")


def remove_state(ctx: Configuration, entity_id: str) -> bool:
    """Call API to remove state for entity_id.

    If success return True, if could not find the entity return False.
    Otherwise raise exception with details.
    """
    try:
        req = restapi(ctx, METH_DELETE, hass.URL_API_STATES_ENTITY.format(entity_id))

        if req.status_code == 200:
            return True
        if req.status_code == 404:
            return False
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError("Unexpected error removing state") from exception

    raise HomeAssistantCliError(f"Error removing state: {req.status_code} - {req.text}")


def set_state(ctx: Configuration, entity_id: str, data: dict) -> dict[str, Any]:
    """Set/update state for entity id."""
    try:
        req = restapi(
            ctx, METH_POST, hass.URL_API_STATES_ENTITY.format(entity_id), data
        )
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Error updating state for entity {entity_id}: {exception}"
        ) from exception

    if req.status_code not in (200, 201):
        raise HomeAssistantCliError(
            f"Error changing state for entity {entity_id}: {req.status_code} - "
            f"{req.text}"
        )
    return cast(dict[str, Any], req.json())


def render_template(ctx: Configuration, template: str, variables: dict) -> str:
    """Render template."""
    data = {"template": template, "variables": variables}

    try:
        req = restapi(ctx, METH_POST, hass.URL_API_TEMPLATE, data)
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Error applying template: {exception}"
        ) from exception

    if req.status_code not in (200, 201):
        raise HomeAssistantCliError(
            f"Error applying template: {req.status_code} - {req.text}"
        )
    return req.text


def get_event_listeners(ctx: Configuration) -> dict:
    """List of events that is being listened for."""
    try:
        req = restapi(ctx, METH_GET, hass.URL_API_EVENTS)

        return req.json() if req.status_code == 200 else {}  # type: ignore

    except (HomeAssistantCliError, ValueError):
        # ValueError if req.json() can't parse the json
        _LOGGER.exception("Unexpected result retrieving event listeners")

        return {}


def fire_event(
    ctx: Configuration, event_type: str, data: dict[str, Any] | None = None
) -> dict[str, Any] | None:
    """Fire an event at remote API."""
    try:
        req = restapi(
            ctx, METH_POST, hass.URL_API_EVENTS_EVENT.format(event_type), data
        )

        if req.status_code != 200:
            _LOGGER.error("Error firing event: %d - %s", req.status_code, req.text)

        return cast(dict[str, Any], req.json())

    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(f"Error firing event: {exception}") from exception


def call_service(
    ctx: Configuration,
    domain: str,
    service: str,
    service_data: dict | None = None,
) -> list[dict[str, Any]]:
    """Call a service."""
    try:
        req = restapi(
            ctx,
            METH_POST,
            hass.URL_API_SERVICES_SERVICE.format(domain, service),
            service_data,
        )
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Error calling service: {exception}"
        ) from exception

    if req.status_code != 200:
        raise HomeAssistantCliError(
            f"Error calling service: {req.status_code} - {req.text}"
        )

    return cast(list[dict[str, Any]], req.json())


def get_services(
    ctx: Configuration,
) -> list[dict[str, Any]]:
    """Get list of services."""
    try:
        req = restapi(ctx, METH_GET, hass.URL_API_SERVICES)
    except HomeAssistantCliError as exception:
        raise HomeAssistantCliError(
            f"Unexpected error getting services: {exception}"
        ) from exception

    if req.status_code == 200:
        return cast(list[dict[str, Any]], req.json())

    raise HomeAssistantCliError(f"Error while getting all services: {req.text}")


def get_network(ctx: Configuration) -> dict[str, Any]:
    """Get network information."""
    frame = {"type": "config/network"}

    print(wsapi(ctx, frame))

    network = cast(dict[str, dict[str, Any]], wsapi(ctx, frame))["result"]

    return network


================================================
FILE: homeassistant_cli/yaml.py
================================================
"""Yaml utility for hass-cli."""

from typing import Any, cast

from ruamel.yaml import YAML
from ruamel.yaml.compat import StringIO


def yaml() -> YAML:
    """Return default YAML parser."""
    yamlp = YAML(typ="safe", pure=True)
    yamlp.preserve_quotes = cast(None, True)
    yamlp.default_flow_style = False
    return yamlp


def loadyaml(yamlp: YAML, source: str) -> Any:
    """Load YAML."""
    return yamlp.load(source)


def dumpyaml(yamlp: YAML, data: Any, stream: Any = None, **kw: Any) -> str | None:
    """Dump YAML to string."""
    inefficient = False
    if stream is None:
        inefficient = True
        stream = StringIO()
    # overriding here to get dumping to
    # not sort keys.
    yamlp = YAML()
    yamlp.indent(mapping=4, sequence=6, offset=3)
    # yamlp.compact(seq_seq=False, seq_map=False)
    yamlp.dump(data, stream, **kw)
    if inefficient:
        return cast(str, stream.getvalue())
    return None


================================================
FILE: mypy.ini
================================================
[mypy]
check_untyped_defs = true
disallow_untyped_calls = true
follow_imports = silent
ignore_missing_imports = true
no_implicit_optional = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true

[mypy-homeassistant_cli.*]
disallow_untyped_defs = true


================================================
FILE: mypyrc
================================================
homeassistant_cli
homeassistant_cli/plugins
tests


================================================
FILE: pylintrc
================================================
[MESSAGES CONTROL]
# Reasons disabled:
# locally-disabled - it spams too much
# duplicate-code - unavoidable
# cyclic-import - doesn't test if both import on load
# abstract-class-little-used - prevents from setting right foundation
# unused-argument - generic callbacks and setup methods create a lot of warnings
# global-statement - used for the on-demand requirement installation
# redefined-variable-type - this is Python, we're duck typing!
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
# abstract-method - with intro of async there are always methods missing
# inconsistent-return-statements - doesn't handle raise
# not-an-iterable - https://github.com/PyCQA/pylint/issues/2311
# C0330 - https://github.com/PyCQA/pylint/issues/289
# C0414 - https://github.com/PyCQA/pylint/issues/2423 can be re-added when upgrading to pylint 2.2
# fixme - for now it is useful having these. When getting more stable lets remove it.
# unused-import - flake8 does this anyway
disable=
  abstract-class-little-used,
  abstract-method,
  cyclic-import,
  duplicate-code,
  global-statement,
  inconsistent-return-statements,
  locally-disabled,
  not-an-iterable,
  not-context-manager,
  redefined-variable-type,
  too-few-public-methods,
  too-many-arguments,
  too-many-branches,
  too-many-instance-attributes,
  too-many-lines,
  too-many-locals,
  too-many-public-methods,
  too-many-return-statements,
  too-many-statements,
  unused-argument,
  C0330,
  C0414,
  fixme,
  unused-import

[REPORTS]
reports=no

[TYPECHECK]
# For attrs
ignored-classes=_CountingAttr
generated-members=botocore.errorfactory

[FORMAT]
expected-line-ending-format=LF

[EXCEPTIONS]
overgeneral-exceptions=Exception,HomeAssistantError


================================================
FILE: pyproject.toml
================================================
[tool.poetry]
name = "homeassistant-cli"
version = "1.0.1"
description  = "Command-line tool for Home Assistant."
license = "Apache Software License 2.0"
readme = "README.rst"
authors = ["The Home Assistant CLI Authors <fabian@affolter-engineering.ch>"]
keywords = [ "home", "automation" ]
homepage = "https://github.com/home-assistant-ecosystem/home-assistant-cli"
repository = "https://github.com/home-assistant-ecosystem/home-assistant-cli"
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: End Users/Desktop",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: Apache Software License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: 3.14",
    "Topic :: Home Automation",
]
exclude = ["tests", "tests.*"]

[tool.poetry.scripts]
hass-cli = "homeassistant_cli.cli:run"

[tool.poetry.dependencies]
python = "^3.13"
aiohttp = "^3.13.3"
dateparser = "^1.2.0"
jsonpath-ng = "^1.6.1"
jinja2 = "^3.1.4"
requests = "^2.31.0"
tabulate = "^0.9.0"
ruamel-yaml = "^0.19.0"
click = "^8.1.7"
click-log = "^0.4.0"
netdisco = "^3.0.0"
packaging = "^25.0"
zeroconf = ">=0.148.0"

[dependency-groups]
test = [
    "pytest (>=9.0.0,<10.0.0)",
    "mypy (>=1.10.0,<2.0.0)",
    "pytest-timeout (>=2.3.1,<3.0.0)",
    "requests-mock (>=1.12.1,<2.0.0)",
    "pytest-sugar (>=1.0.0,<2.0.0)",
    "pytest-cov (>=5.0.0,<6.0.0)",
    "types-requests (>=2.31.0.20240406,<3.0.0.0)",
    "types-dateparser (>=1.2.0.20240420,<2.0.0.0)",
    "types-tabulate (>=0.9.0.20240106,<0.10.0.0)",
    "pre-commit (>=3.0.0,<4.0.0)",
    "ruff (>=0.15.10,<0.16.0)"
]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.ruff]
src = ["homeassistant_cli", "tests"]
line-length = 88

[tool.ruff.format]
docstring-code-format = true
skip-magic-trailing-comma = true

[tool.ruff.lint]
select = [
    "B",  # bugbear
    "D",  # pydocstyle
    "E",  # pycodestyle
    "F",  # pyflakes
    "I",  # isort
    "PYI", # flake8-pyi
    "UP", # pyupgrade
    "RUF", # ruff
    "W",  # pycodestyle
    "PIE", # flake8-pie
    "PGH004", # pygrep-hooks - Use specific rule codes when using noqa
    "PLE", # pylint error
    "PLW", # pylint warning
    "PLR1714", # Consider merging multiple comparisons
    "T100", # flake8-debugger
]
ignore = [
    "B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
    "B007", # Loop control variable `i` not used within loop body
    "B009", # Do not call `getattr` with a constant attribute value
    "B010", # [*] Do not call `setattr` with a constant attribute value.
    "B011", # Do not `assert False` (`python -O` removes these calls)
    "B028", # No explicit `stacklevel` keyword argument found
    "D203", # 1 blank line required before class docstring
    "D212", # Multi-line docstring summary should start at the first line
    "E721", # Do not compare types, use `isinstance()`
    "F401", # Module imported but unused
    "D415", # First line should end with a period, question mark, or exclamation point
    "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
    "PLW0603",  # Using the global statement
    "PLW0120",  # remove the else and dedent its contents
    "PLW2901",  # for loop variable overwritten by assignment target
    "PLR5501",  # Use `elif` instead of `else` then `if`
    "UP035",  # Syntax features not supported by minimum Python version
]

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.isort]
known-first-party = ["homeassistant_cli"]
split-on-trailing-comma = false

[tool.mypy]
files = "homeassistant_cli, tests"
mypy_path = "homeassistant_cli"
check_untyped_defs = true
disallow_untyped_calls = true
explicit_package_bases = true
ignore_missing_imports = true
namespace_packages = true
no_implicit_optional = true
show_error_codes = true
strict = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true
enable_error_code = [
  "ignore-without-code",
  "redundant-expr",
  "truthy-bool",
  "no-any-return",
  "no-untyped-def",
]


================================================
FILE: tests/__init__.py
================================================
"""Init file for Home Assistant CLI tests."""


================================================
FILE: tests/bandit.yaml
================================================
# https://bandit.readthedocs.io/en/latest/config.html

tests:
  - B108
  - B306
  - B307
  - B313
  - B314
  - B315
  - B316
  - B317
  - B318
  - B319
  - B320
  - B325
  - B602
  - B604


================================================
FILE: tests/conftest.py
================================================
"""conftest.py loads all fixtures found in fixtures/.

Each file are made available as follows:

Given a file named: `mydata.json`
it will be available as:

mydata_text - str with the raw text
mydata      - Dict with the content parsed from json
"""

import json
import os
from pathlib import Path

import click_log.core as logcore
import pytest

FIXTURES_PATH = Path(__file__).parent / "fixtures"


logcore.basic_config()


# Environment variables that should be cleared during tests
HASS_ENV_VARS = [
    "HASS_SERVER",
    "HASS_TOKEN",
    "HASS_PASSWORD",
    "HASSIO_TOKEN",
]


@pytest.fixture(autouse=True)
def clean_hass_env(monkeypatch):
    """Clear Home Assistant environment variables for test isolation."""
    for var in HASS_ENV_VARS:
        monkeypatch.delenv(var, raising=False)


def generate_fixture(content: str):
    """Generate the individual fixtures."""
    # pylint: disable=unnecessary-pass

    @pytest.fixture(scope="module")
    def my_fixture():
        return content

    return my_fixture


def _inject_fixture(name: str, someparam: str):
    globals()[name] = generate_fixture(someparam)


def _all_fixtures():
    for fname in os.listdir(FIXTURES_PATH):
        name, ext = os.path.splitext(fname)

        with open(FIXTURES_PATH / fname) as file:
            content = file.read()

        _inject_fixture(name + "_text", content)
        if ext == ".json":
            _inject_fixture(name, json.loads(content))


_all_fixtures()  # type: ignore


================================================
FILE: tests/fixtures/basic_entities.json
================================================
[{
    "attributes": {
      "auto": true,
      "entity_id": [
        "remote.tv"
      ],
      "friendly_name": "friendly long name",
      "hidden": true,
      "order": 16
    },
    "context": {
      "id": "4c511277c55647eb8e7e4acf10fcd617",
      "user_id": null
    },
    "entity_id": "sensor.one",
    "last_changed": "2018-12-02T10:13:05.914548+00:00",
    "last_updated": "2018-12-04T10:13:05.914548+00:00",
    "state": "on"
  },
  {
    "attributes": {
      "event_data": 1002,
      "event_received": "2018-12-05 13:17:51.905847"
    },
    "context": {
      "id": "b0e24511a0fd4eb69ab5afeac0082993",
      "user_id": "2b0f58a02c35408c86e9e34f1d6e141d"
    },
    "entity_id": "sensor.two",
    "last_changed": "2018-12-01T12:17:52.434229+00:00",
    "last_updated": "2018-12-05T12:17:52.434229+00:00",
    "state": "off"
  },
  {
    "attributes": {
    },
    "context": {
      "id": "b0e24511a0fd4eb69ab5afeac0082993",
      "user_id": "2b0f58a02c35408c86e9e34f1d6e141d"
    },
    "entity_id": "sensor.three",
    "last_changed": "2018-12-03T12:17:52.434229+00:00",
    "last_updated": "2018-12-05T12:17:52.434229+00:00",
    "state": "off"
  }
]


================================================
FILE: tests/fixtures/basic_entities_table.txt
================================================
ENTITY        DESCRIPTION         STATE    CHANGED
sensor.one    friendly long name  on       2018-12-02T10:13:05.914548+00:00
sensor.two                        off      2018-12-01T12:17:52.434229+00:00
sensor.three                      off      2018-12-03T12:17:52.434229+00:00


================================================
FILE: tests/fixtures/basic_entities_table_columns.txt
================================================
entity              state
friendly long name  on
                    off
                    off


================================================
FILE: tests/fixtures/basic_entities_table_format.txt
================================================
<table>
<thead>
<tr><th>ENTITY      </th><th>DESCRIPTION       </th><th>STATE  </th><th>CHANGED                         </th></tr>
</thead>
<tbody>
<tr><td>sensor.one  </td><td>friendly long name</td><td>on     </td><td>2018-12-02T10:13:05.914548+00:00</td></tr>
<tr><td>sensor.two  </td><td>                  </td><td>off    </td><td>2018-12-01T12:17:52.434229+00:00</td></tr>
<tr><td>sensor.three</td><td>                  </td><td>off    </td><td>2018-12-03T12:17:52.434229+00:00</td></tr>
</tbody>
</table>


================================================
FILE: tests/fixtures/basic_entities_table_no_header.txt
================================================
sensor.one    friendly long name  on   2018-12-02T10:13:05.914548+00:00
sensor.two                        off  2018-12-01T12:17:52.434229+00:00
sensor.three                      off  2018-12-03T12:17:52.434229+00:00


================================================
FILE: tests/fixtures/basic_entities_table_sorted.txt
================================================
entity              state    last_changed
                    off      2018-12-01T12:17:52.434229+00:00
friendly long name  on       2018-12-02T10:13:05.914548+00:00
                    off      2018-12-03T12:17:52.434229+00:00


================================================
FILE: tests/fixtures/default_areas.json
================================================
[
    { "area_id": 1, "name": "Kitchen"},
    { "area_id": 2, "name": "Kitchen Light"},
    { "area_id": 3, "name": "Bedroom"}
]


================================================
FILE: tests/fixtures/default_devices.json
================================================
[
  {
    "config_entries": [
      "424ae83a64a54fa8b6b01d71aa7d9b3d"
    ],
    "connections": [],
    "manufacturer": "Sonos",
    "model": "Play:5",
    "name": "Kitchen",
    "sw_version": null,
    "id": "fa56ea5934f44fa19161bbf2a3d33732",
    "hub_device_id": null,
    "area_id": "e6ebd3e6f6e04b63a0e4a109b4748584",
    "area_name": "Bedroom"
  },
  {
    "config_entries": [],
    "connections": [],
    "manufacturer": "Philips",
    "model": "Hue color lamp",
    "name": "Kitchen table left",
    "sw_version": "5.105.0.21536",
    "id": "b6f1087b94c84bc8bbe2a01adbd014d8",
    "hub_device_id": "3e2f3eaccc0a4dedbbc86c32275e6249",
    "area_id": "e6ebd3e6f6e04b63a0e4a109b4748584",
    "area_name": "Bedroom"
  },
  {
    "config_entries": [],
    "connections": [],
    "manufacturer": "Philips",
    "model": "Hue color spot",
    "name": "Kitchen front right at table",
    "sw_version": "5.105.0.21536",
    "id": "c022c2a832194a9aadbc40d39e7d5ee7",
    "hub_device_id": "3e2f3eaccc0a4dedbbc86c32275e6249",
    "area_id": "e6ebd3e6f6e04b63a0e4a109b4748584",
    "area_name": "Bedroom"
  },
  {
    "config_entries": [],
    "connections": [],
    "manufacturer": "Philips",
    "model": "Hue color spot",
    "name": "Kitchen left back at hub",
    "sw_version": "5.105.0.21536",
    "id": "cabab7fdfc97462f959aec7434989c82",
    "hub_device_id": "3e2f3eaccc0a4dedbbc86c32275e6249",
    "area_id": "e6ebd3e6f6e04b63a0e4a109b4748584",
    "area_name": "Bedroom"
  },
  {
    "config_entries": [],
    "connections": [],
    "manufacturer": "Philips",
    "model": "Hue color spot",
    "name": "Kitchen left back at bar",
    "sw_version": "5
Download .txt
gitextract_uoxhyuya/

├── .coveragerc
├── .dockerignore
├── .github/
│   ├── release-drafter.yml
│   └── workflows/
│       ├── publish-to-pypi.yml
│       └── testing.yml
├── .gitignore
├── .pre-commit-config.yaml
├── Dockerfile
├── Dockerfile.armhf
├── LICENSE.md
├── MANIFEST.in
├── README.rst
├── SECURITY.md
├── docker-hass-cli
├── homeassistant_cli/
│   ├── __init__.py
│   ├── autocompletion.py
│   ├── cli.py
│   ├── config.py
│   ├── const.py
│   ├── exceptions.py
│   ├── hassconst.py
│   ├── helper.py
│   ├── plugins/
│   │   ├── area.py
│   │   ├── completion.py
│   │   ├── config.py
│   │   ├── device.py
│   │   ├── discover.py
│   │   ├── entity.py
│   │   ├── event.py
│   │   ├── ha.py
│   │   ├── info.py
│   │   ├── integration.py
│   │   ├── map.py
│   │   ├── raw.py
│   │   ├── service.py
│   │   ├── state.py
│   │   ├── system.py
│   │   └── template.py
│   ├── remote.py
│   └── yaml.py
├── mypy.ini
├── mypyrc
├── pylintrc
├── pyproject.toml
└── tests/
    ├── __init__.py
    ├── bandit.yaml
    ├── conftest.py
    ├── fixtures/
    │   ├── basic_entities.json
    │   ├── basic_entities_table.txt
    │   ├── basic_entities_table_columns.txt
    │   ├── basic_entities_table_format.txt
    │   ├── basic_entities_table_no_header.txt
    │   ├── basic_entities_table_sorted.txt
    │   ├── default_areas.json
    │   ├── default_devices.json
    │   ├── default_entities.json
    │   ├── default_events.json
    │   └── default_services.json
    ├── test_area.py
    ├── test_completion.py
    ├── test_defaults.py
    ├── test_device.py
    ├── test_ha.py
    ├── test_helper.py
    ├── test_info.py
    ├── test_integration.py
    ├── test_map.py
    ├── test_plugins.py
    ├── test_raw.py
    ├── test_service.py
    ├── test_state.py
    └── test_template.py
Download .txt
SYMBOL INDEX (354 symbols across 38 files)

FILE: homeassistant_cli/autocompletion.py
  function _init_ctx (line 12) | def _init_ctx(ctx: Configuration) -> None:
  function services (line 41) | def services(ctx: Configuration, args: list, incomplete: str) -> list[tu...
  function entities (line 70) | def entities(ctx: Configuration, args: list, incomplete: str) -> list[tu...
  function events (line 92) | def events(ctx: Configuration, args: list, incomplete: str) -> list[tupl...
  function table_formats (line 113) | def table_formats(
  function api_methods (line 150) | def api_methods(
  function wsapi_methods (line 168) | def wsapi_methods(
  function _quote_if_needed (line 186) | def _quote_if_needed(value: str) -> str:
  function areas (line 193) | def areas(ctx: Configuration, args: list, incomplete: str) -> list[tuple...

FILE: homeassistant_cli/cli.py
  function run (line 28) | def run() -> None:
  class HomeAssistantCli (line 66) | class HomeAssistantCli(click.MultiCommand):
    method list_commands (line 69) | def list_commands(self, ctx: Context) -> list[str]:
    method get_command (line 81) | def get_command(self, ctx: Context, cmd_name: str) -> Group | Command ...
  function _default_token (line 96) | def _default_token() -> str | None:
  function cli (line 213) | def cli(

FILE: homeassistant_cli/config.py
  class _ZeroconfListener (line 18) | class _ZeroconfListener:
    method __init__ (line 21) | def __init__(self) -> None:
    method remove_service (line 25) | def remove_service(
    method add_service (line 31) | def add_service(self, _zeroconf: zeroconf.Zeroconf, _type: str, name: ...
    method update_service (line 35) | def update_service(
  function _locate_ha (line 42) | def _locate_ha() -> str | None:
  function resolve_server (line 76) | def resolve_server(ctx: Any) -> str:
  function set_supervisor_server (line 105) | def set_supervisor_server(ctx: Any) -> str:
  class Configuration (line 121) | class Configuration:
    method __init__ (line 124) | def __init__(self) -> None:
    method echo (line 145) | def echo(self, msg: str, *args: Any | None) -> None:
    method log (line 149) | def log(  # pylint: disable=no-self-use
    method vlog (line 157) | def vlog(self, msg: str, *args: str | None) -> None:
    method __repr__ (line 162) | def __repr__(self) -> str:
    method resolve_server (line 178) | def resolve_server(self) -> str:
    method set_supervisor_server (line 182) | def set_supervisor_server(self) -> str:
    method auto_output (line 186) | def auto_output(self, auto_output: str) -> str:
    method yaml (line 195) | def yaml(self) -> YAML:
    method yamlload (line 201) | def yamlload(self, source: str) -> Any:
    method yamldump (line 205) | def yamldump(self, source: Any) -> str:

FILE: homeassistant_cli/exceptions.py
  class HomeAssistantCliError (line 4) | class HomeAssistantCliError(Exception):
  class UnsafeTemplateError (line 8) | class UnsafeTemplateError(HomeAssistantCliError):

FILE: homeassistant_cli/helper.py
  function to_attributes (line 22) | def to_attributes(entry: str) -> dict[str, str]:
  function to_tuples (line 44) | def to_tuples(entry: str) -> list[tuple[str, str]]:
  function raw_format_output (line 60) | def raw_format_output(
  function _sort_table (line 127) | def _sort_table(result: list[Any], sort_by: str) -> list[Any]:
  function format_output (line 141) | def format_output(
  function debug_requests_on (line 158) | def debug_requests_on() -> None:
  function debug_requests_off (line 169) | def debug_requests_off() -> None:
  function debug_requests (line 185) | def debug_requests() -> Generator:

FILE: homeassistant_cli/plugins/area.py
  function cli (line 22) | def cli(ctx: Configuration) -> None:
  function listcmd (line 29) | def listcmd(ctx: Configuration, area_filter: str) -> None:
  function create (line 58) | def create(ctx: Configuration, names: tuple[str, ...]) -> None:
  function delete (line 91) | def delete(ctx: Configuration, names: tuple[str, ...], confirm: bool) ->...
  function rename (line 132) | def rename(ctx: Configuration, old_name: str, new_name: str) -> None:

FILE: homeassistant_cli/plugins/completion.py
  function cli (line 11) | def cli(ctx):
  function dump_script (line 15) | def dump_script(shell: str) -> None:
  function bash (line 26) | def bash(ctx):
  function zsh (line 33) | def zsh(ctx):

FILE: homeassistant_cli/plugins/config.py
  function cli (line 13) | def cli(ctx):
  function full (line 33) | def full(ctx: Configuration):
  function integrations (line 46) | def integrations(ctx: Configuration):
  function whitelist_dirs (line 59) | def whitelist_dirs(ctx: Configuration):
  function release (line 72) | def release(ctx: Configuration):

FILE: homeassistant_cli/plugins/device.py
  function cli (line 21) | def cli(ctx):
  function list_cmd (line 28) | def list_cmd(ctx: Configuration, device_filter: str):
  function assign (line 77) | def assign(
  function rename (line 143) | def rename(
  function list_by_area (line 186) | def list_by_area(ctx: Configuration, area_id_or_name: str):
  function delete (line 222) | def delete(ctx: Configuration, device_id_or_name: str, confirm: bool):

FILE: homeassistant_cli/plugins/discover.py
  function cli (line 13) | def cli(ctx: Configuration, raw):

FILE: homeassistant_cli/plugins/entity.py
  function cli (line 21) | def cli(ctx):
  function listcmd (line 28) | def listcmd(ctx: Configuration, entity_filter: str):
  function assign (line 75) | def assign(
  function rename (line 152) | def rename(ctx, old_id, new_id, name):
  function delete (line 189) | def delete(ctx: Configuration, entity_id: str, confirm: bool) -> None:
  function enable (line 223) | def enable(ctx: Configuration, entity_id: str) -> None:
  function disable (line 251) | def disable(ctx: Configuration, entity_id: str) -> None:

FILE: homeassistant_cli/plugins/event.py
  function cli (line 20) | def cli(ctx):
  function fire (line 35) | def fire(ctx: Configuration, event, json):
  function watch (line 63) | def watch(ctx: Configuration, event_type):

FILE: homeassistant_cli/plugins/ha.py
  function cli (line 25) | def cli(ctx: Configuration):
  function _report (line 30) | def _report(ctx, cmd, method, response) -> None:
  function _handle (line 49) | def _handle(ctx, method, httpmethod="get", raw=False) -> None:
  function _handle_raw (line 57) | def _handle_raw(ctx, method, httpmethod="get") -> dict:
  function addons (line 68) | def addons(ctx: Configuration):
  function addons_all (line 75) | def addons_all(ctx: Configuration):
  function addons_reload (line 82) | def addons_reload(ctx: Configuration):
  function audio (line 91) | def audio(ctx: Configuration):
  function audio_info (line 98) | def audio_info(ctx: Configuration):
  function audio_stats (line 105) | def audio_stats(ctx: Configuration):
  function audio_logs (line 112) | def audio_logs(ctx: Configuration):
  function audio_reload (line 119) | def audio_reload(ctx: Configuration):
  function audio_restart (line 126) | def audio_restart(ctx: Configuration):
  function auth (line 135) | def auth(ctx: Configuration):
  function auth_list (line 142) | def auth_list(ctx: Configuration):
  function backup (line 151) | def backup(ctx: Configuration):
  function backup_info (line 158) | def backup_info(ctx: Configuration):
  function backup_reload (line 165) | def backup_reload(ctx: Configuration):
  function ha_cli (line 174) | def ha_cli(ctx: Configuration):
  function ha_info (line 181) | def ha_info(ctx: Configuration):
  function ha_update (line 188) | def ha_update(ctx: Configuration):
  function ha_stats (line 205) | def ha_stats(ctx: Configuration):
  function core (line 214) | def core(ctx: Configuration):
  function core_info (line 221) | def core_info(ctx: Configuration):
  function core_update (line 228) | def core_update(ctx: Configuration):
  function core_logs (line 245) | def core_logs(ctx: Configuration):
  function core_restart (line 252) | def core_restart(ctx: Configuration):
  function core_check (line 262) | def core_check(ctx: Configuration):
  function core_start (line 272) | def core_start(ctx: Configuration):
  function core_stop (line 282) | def core_stop(ctx: Configuration):
  function core_rebuild (line 292) | def core_rebuild(ctx: Configuration):
  function core_options (line 302) | def core_options(ctx: Configuration):
  function core_websocket (line 309) | def core_websocket(ctx: Configuration):
  function core_stats (line 319) | def core_stats(ctx: Configuration):
  function dns (line 338) | def dns(ctx: Configuration):
  function dns_info (line 345) | def dns_info(ctx: Configuration):
  function dns_options (line 352) | def dns_options(ctx: Configuration):
  function dns_restart (line 359) | def dns_restart(ctx: Configuration):
  function dns_logs (line 369) | def dns_logs(ctx: Configuration):
  function dns_stats (line 376) | def dns_stats(ctx: Configuration):
  function dns_update (line 383) | def dns_update(ctx: Configuration):
  function dns_reset (line 393) | def dns_reset(ctx: Configuration):
  function docker (line 405) | def docker(ctx: Configuration):
  function docker_info (line 412) | def docker_info(ctx: Configuration):
  function docker_registries (line 419) | def docker_registries(ctx: Configuration):
  function hardware (line 428) | def hardware(ctx: Configuration):
  function hardware_info (line 435) | def hardware_info(ctx: Configuration):
  function hardware_audio (line 442) | def hardware_audio(ctx: Configuration):
  function host (line 451) | def host(ctx: Configuration):
  function host_reboot (line 458) | def host_reboot(ctx: Configuration):
  function host_reload (line 465) | def host_reload(ctx: Configuration):
  function host_shutdown (line 472) | def host_shutdown(ctx: Configuration):
  function host_info (line 479) | def host_info(ctx: Configuration):
  function host_options (line 486) | def host_options(ctx: Configuration):
  function host_services (line 493) | def host_services(ctx: Configuration):
  function ingress (line 502) | def ingress(ctx: Configuration):
  function ingress_info (line 509) | def ingress_info(ctx: Configuration):
  function jobs (line 518) | def jobs(ctx: Configuration):
  function jobs_info (line 525) | def jobs_info(ctx: Configuration):
  function root (line 534) | def root(ctx: Configuration):
  function root_info (line 541) | def root_info(ctx: Configuration):
  function root_available_updates (line 548) | def root_available_updates(ctx: Configuration):
  function mount (line 557) | def mount(ctx: Configuration):
  function mount_info (line 564) | def mount_info(ctx: Configuration):
  function multicast (line 573) | def multicast(ctx: Configuration):
  function multicast_info (line 580) | def multicast_info(ctx: Configuration):
  function multicast_update (line 587) | def multicast_update(ctx: Configuration):
  function multicast_restart (line 604) | def multicast_restart(ctx: Configuration):
  function multicast_logs (line 614) | def multicast_logs(ctx: Configuration):
  function multicast_stats (line 621) | def multicast_stats(ctx: Configuration):
  function network (line 630) | def network(ctx: Configuration):
  function network_info (line 637) | def network_info(ctx: Configuration):
  function network_reload (line 644) | def network_reload(ctx: Configuration):
  function observer (line 656) | def observer(ctx: Configuration):
  function observer_info (line 663) | def observer_info(ctx: Configuration):
  function observer_stats (line 670) | def observer_stats(ctx: Configuration):
  function os (line 679) | def os(ctx: Configuration):
  function os_info (line 686) | def os_info(ctx: Configuration):
  function os_swap (line 693) | def os_swap(ctx: Configuration):
  function os_datadisk (line 700) | def os_datadisk(ctx: Configuration):
  function os_update (line 707) | def os_update(ctx: Configuration):
  function resolution (line 726) | def resolution(ctx: Configuration):
  function resolution_info (line 733) | def resolution_info(ctx: Configuration):
  function service (line 742) | def service(ctx: Configuration):
  function service_info (line 749) | def service_info(ctx: Configuration):
  function service_mqtt (line 756) | def service_mqtt(ctx: Configuration):
  function service_mysql (line 763) | def service_mysql(ctx: Configuration):
  function store (line 772) | def store(ctx: Configuration):
  function store_info (line 779) | def store_info(ctx: Configuration):
  function store_addon (line 786) | def store_addon(ctx: Configuration):
  function store_repositories (line 793) | def store_repositories(ctx: Configuration):
  function store_reload (line 800) | def store_reload(ctx: Configuration):
  function security (line 812) | def security(ctx: Configuration):
  function security_info (line 819) | def security_info(ctx: Configuration):
  function supervisor (line 828) | def supervisor(ctx: Configuration):
  function supervisor_ping (line 835) | def supervisor_ping(ctx: Configuration):
  function supervisor_info (line 842) | def supervisor_info(ctx: Configuration):
  function supervisor_update (line 849) | def supervisor_update(ctx: Configuration):
  function supervisor_options (line 866) | def supervisor_options(ctx: Configuration):
  function supervisor_reload (line 873) | def supervisor_reload(ctx: Configuration):
  function supervisor_logs (line 880) | def supervisor_logs(ctx: Configuration):
  function supervisor_repair (line 887) | def supervisor_repair(ctx: Configuration):
  function supervisor_restart (line 894) | def supervisor_restart(ctx: Configuration):
  function supervisor_stats (line 904) | def supervisor_stats(ctx: Configuration):

FILE: homeassistant_cli/plugins/info.py
  function redacted_output (line 18) | def redacted_output(token: str) -> str:
  function cli (line 29) | def cli(ctx):
  function cli (line 35) | def cli(ctx):

FILE: homeassistant_cli/plugins/integration.py
  function cli (line 19) | def cli(ctx):
  function list_integrations (line 26) | def list_integrations(ctx: Configuration, integration_filter: str):
  function info (line 63) | def info(ctx: Configuration, entry_id: str):
  function reload (line 89) | def reload(ctx: Configuration, entry_id: str):
  function delete (line 125) | def delete(ctx: Configuration, entry_id: str, confirm: bool):
  function disable (line 165) | def disable(ctx: Configuration, entry_id: str):
  function enable (line 196) | def enable(ctx: Configuration, entry_id: str):
  function list_disabled (line 226) | def list_disabled(ctx: Configuration):
  function list_loaded (line 247) | def list_loaded(ctx: Configuration):
  function list_unloaded (line 269) | def list_unloaded(ctx: Configuration):

FILE: homeassistant_cli/plugins/map.py
  function cli (line 31) | def cli(ctx: Configuration, service: str, entity: str) -> None:

FILE: homeassistant_cli/plugins/raw.py
  function cli (line 20) | def cli(ctx: Configuration):
  function _report (line 25) | def _report(ctx, cmd, method, response) -> None:
  function get (line 50) | def get(ctx: Configuration, method):
  function post (line 64) | def post(ctx: Configuration, method, json):
  function websocket (line 85) | def websocket(ctx: Configuration, wstype, json):

FILE: homeassistant_cli/plugins/service.py
  function cli (line 21) | def cli(ctx):
  function list_cmd (line 28) | def list_cmd(ctx: Configuration, servicefilter):
  function call (line 86) | def call(ctx: Configuration, service, arguments):

FILE: homeassistant_cli/plugins/state.py
  function cli (line 22) | def cli(ctx):
  function get (line 33) | def get(ctx: Configuration, entity):
  function delete (line 57) | def delete(ctx: Configuration, entity):
  function list_command (line 71) | def list_command(ctx, entityfilter):
  function edit (line 119) | def edit(ctx: Configuration, entity, newstate, attributes, merge, json):
  function _report (line 179) | def _report(ctx: Configuration, result: list[dict[str, Any]], action: str):
  function _homeassistant_cmd (line 192) | def _homeassistant_cmd(ctx: Configuration, entities, cmd, action):
  function toggle (line 209) | def toggle(ctx: Configuration, entities):
  function off_cmd (line 223) | def off_cmd(ctx: Configuration, entities):
  function on_cmd (line 237) | def on_cmd(ctx: Configuration, entities):
  function history (line 265) | def history(ctx: Configuration, entities: list, since: str, end: str):

FILE: homeassistant_cli/plugins/system.py
  function cli (line 18) | def cli(ctx):
  function log (line 24) | def log(ctx):
  function health (line 31) | def health(ctx: Configuration):

FILE: homeassistant_cli/plugins/template.py
  function _safe_environ_get (line 26) | def _safe_environ_get(key: str, default: str | None = None) -> str | None:
  function render (line 33) | def render(template_path, data, strict=False) -> str:
  function cli (line 67) | def cli(ctx: Configuration, template, datafile, local: bool) -> None:

FILE: homeassistant_cli/remote.py
  class APIStatus (line 39) | class APIStatus(enum.Enum):
    method __str__ (line 47) | def __str__(self) -> str:
  function restapi (line 52) | def restapi(
  function restapi_supervisor (line 97) | def restapi_supervisor(
  function wsapi (line 140) | def wsapi(
  class JSONEncoder (line 188) | class JSONEncoder(json.JSONEncoder):
    method default (line 192) | def default(self, o: Any) -> Any:
  function get_areas (line 207) | def get_areas(ctx: Configuration) -> list[dict[str, Any]]:
  function find_area (line 216) | def find_area(ctx: Configuration, id_or_name: str) -> dict[str, str] | N...
  function create_area (line 227) | def create_area(ctx: Configuration, name: str) -> dict[str, Any]:
  function delete_area (line 234) | def delete_area(ctx: Configuration, area_id: str) -> dict[str, Any]:
  function rename_area (line 241) | def rename_area(ctx: Configuration, area_id: str, new_name: str) -> dict...
  function rename_entity (line 252) | def rename_entity(
  function rename_device (line 272) | def rename_device(ctx: Configuration, device_id: str, new_name: str) -> ...
  function assign_area (line 283) | def assign_area(ctx: Configuration, device_id: str, area_id: str) -> dic...
  function assign_entity_area (line 294) | def assign_entity_area(
  function delete_entity (line 307) | def delete_entity(ctx: Configuration, entity_id: str) -> dict[str, Any]:
  function enable_entity (line 317) | def enable_entity(
  function get_health (line 330) | def get_health(ctx: Configuration) -> dict[str, Any]:
  function get_devices (line 339) | def get_devices(ctx: Configuration) -> list[dict[str, Any]]:
  function get_entities (line 348) | def get_entities(ctx: Configuration) -> list[dict[str, Any]]:
  function get_entity (line 357) | def get_entity(ctx: Configuration, entity_id: str) -> list[dict[str, Any]]:
  function get_config_entries (line 366) | def get_config_entries(ctx: Configuration) -> list[dict[str, Any]]:
  function get_config_entry (line 373) | def get_config_entry(ctx: Configuration, entry_id: str) -> dict[str, Any]:
  function reload_config_entry (line 380) | def reload_config_entry(ctx: Configuration, entry_id: str) -> dict[str, ...
  function delete_config_entry (line 394) | def delete_config_entry(ctx: Configuration, entry_id: str) -> dict[str, ...
  function disable_config_entry (line 406) | def disable_config_entry(
  function validate_api (line 421) | def validate_api(ctx: Configuration) -> APIStatus:
  function get_info (line 438) | def get_info(ctx: Configuration) -> dict[str, Any]:
  function get_events (line 454) | def get_events(ctx: Configuration) -> dict[str, Any]:
  function get_history (line 469) | def get_history(
  function get_states (line 504) | def get_states(ctx: Configuration) -> list[dict[str, Any]]:
  function get_raw_error_log (line 520) | def get_raw_error_log(ctx: Configuration) -> str:
  function get_config (line 533) | def get_config(ctx: Configuration) -> dict[str, Any]:
  function get_state (line 548) | def get_state(ctx: Configuration, entity_id: str) -> dict[str, Any] | None:
  function remove_state (line 569) | def remove_state(ctx: Configuration, entity_id: str) -> bool:
  function set_state (line 588) | def set_state(ctx: Configuration, entity_id: str, data: dict) -> dict[st...
  function render_template (line 607) | def render_template(ctx: Configuration, template: str, variables: dict) ...
  function get_event_listeners (line 625) | def get_event_listeners(ctx: Configuration) -> dict:
  function fire_event (line 639) | def fire_event(
  function call_service (line 657) | def call_service(
  function get_services (line 684) | def get_services(
  function get_network (line 701) | def get_network(ctx: Configuration) -> dict[str, Any]:

FILE: homeassistant_cli/yaml.py
  function yaml (line 9) | def yaml() -> YAML:
  function loadyaml (line 17) | def loadyaml(yamlp: YAML, source: str) -> Any:
  function dumpyaml (line 22) | def dumpyaml(yamlp: YAML, data: Any, stream: Any = None, **kw: Any) -> s...

FILE: tests/conftest.py
  function clean_hass_env (line 35) | def clean_hass_env(monkeypatch):
  function generate_fixture (line 41) | def generate_fixture(content: str):
  function _inject_fixture (line 52) | def _inject_fixture(name: str, someparam: str):
  function _all_fixtures (line 56) | def _all_fixtures():

FILE: tests/test_area.py
  function test_area_list (line 11) | def test_area_list(default_areas) -> None:
  function test_area_list_filter (line 24) | def test_area_list_filter(default_areas) -> None:

FILE: tests/test_completion.py
  function test_entity_completion (line 12) | def test_entity_completion(basic_entities_text) -> None:
  function test_service_completion (line 35) | def test_service_completion(default_services_text) -> None:
  function test_event_completion (line 60) | def test_event_completion(default_events_text) -> None:
  function test_area_completion (line 84) | def test_area_completion(default_events_text) -> None:

FILE: tests/test_defaults.py
  function test_defaults (line 64) | def test_defaults(

FILE: tests/test_device.py
  function test_device_list (line 11) | def test_device_list(default_devices, default_areas) -> None:
  function test_device_list_filter (line 31) | def test_device_list_filter(default_devices, default_areas) -> None:
  function test_device_assign (line 53) | def test_device_assign(default_areas, default_devices) -> None:

FILE: tests/test_ha.py
  function test_os_update_already_latest (line 9) | def test_os_update_already_latest() -> None:
  function test_os_update_needs_update (line 31) | def test_os_update_needs_update() -> None:
  function test_core_update_already_latest (line 58) | def test_core_update_already_latest() -> None:
  function test_core_update_needs_update (line 80) | def test_core_update_needs_update() -> None:

FILE: tests/test_helper.py
  function test_to_attributes_multiples (line 9) | def test_to_attributes_multiples():
  function test_to_attributes_none (line 18) | def test_to_attributes_none():
  function test_to_tuples (line 24) | def test_to_tuples():
  function test_to_tuples_no_header (line 32) | def test_to_tuples_no_header():
  function test_sorting_by_jsonpath (line 40) | def test_sorting_by_jsonpath():

FILE: tests/test_info.py
  class TestRedactedOutput (line 10) | class TestRedactedOutput:
    method test_none_token (line 13) | def test_none_token(self) -> None:
    method test_empty_token (line 17) | def test_empty_token(self) -> None:
    method test_short_token (line 21) | def test_short_token(self) -> None:
    method test_long_token (line 27) | def test_long_token(self) -> None:
    method test_very_long_token (line 36) | def test_very_long_token(self) -> None:
  class TestInfoCliCommand (line 46) | class TestInfoCliCommand:
    method test_info_cli_output (line 49) | def test_info_cli_output(self) -> None:
    method test_info_cli_with_token (line 64) | def test_info_cli_with_token(self) -> None:
    method test_info_cli_json_output (line 84) | def test_info_cli_json_output(self) -> None:
    method test_info_cli_shows_version (line 96) | def test_info_cli_shows_version(self) -> None:

FILE: tests/test_integration.py
  function test_integration_list (line 37) | def test_integration_list() -> None:
  function test_integration_list_with_filter (line 57) | def test_integration_list_with_filter() -> None:
  function test_integration_list_filter_by_title (line 78) | def test_integration_list_filter_by_title() -> None:
  function test_integration_info (line 99) | def test_integration_info() -> None:
  function test_integration_info_partial_match (line 120) | def test_integration_info_partial_match() -> None:
  function test_integration_reload_success (line 140) | def test_integration_reload_success() -> None:
  function test_integration_reload_partial_id (line 164) | def test_integration_reload_partial_id() -> None:
  function test_integration_delete_with_confirm (line 188) | def test_integration_delete_with_confirm() -> None:
  function test_integration_disable (line 212) | def test_integration_disable() -> None:
  function test_integration_enable (line 235) | def test_integration_enable() -> None:
  function test_integration_list_disabled (line 258) | def test_integration_list_disabled() -> None:
  function test_integration_list_loaded (line 280) | def test_integration_list_loaded() -> None:
  function test_integration_list_unloaded (line 301) | def test_integration_list_unloaded() -> None:

FILE: tests/test_map.py
  function test_map_services (line 22) | def test_map_services(service, url, default_entities) -> None:

FILE: tests/test_plugins.py
  function defaultplugins_fixture (line 30) | def defaultplugins_fixture() -> list[str]:
  function test_commands_match_expected (line 35) | def test_commands_match_expected(defaultplugins_sorted) -> None:
  function test_commands_loads (line 59) | def test_commands_loads(plugin) -> None:

FILE: tests/test_raw.py
  function test_raw_get (line 15) | def test_raw_get() -> None:
  function test_raw_post (line 35) | def test_raw_post() -> None:
  function test_apimethod_completion (line 55) | def test_apimethod_completion(default_services) -> None:
  function test_raw_ws (line 81) | def test_raw_ws() -> None:
  function test_raw_ws_data (line 101) | def test_raw_ws_data() -> None:

FILE: tests/test_service.py
  function test_service_list (line 13) | def test_service_list(default_services) -> None:
  function test_service_filter (line 33) | def test_service_filter(default_services) -> None:
  function test_service_completion (line 53) | def test_service_completion(default_services) -> None:
  function test_service_call (line 73) | def test_service_call(default_services) -> None:

FILE: tests/test_state.py
  function test_state_list (line 38) | def test_state_list(basic_entities_text) -> None:
  function output_formats (line 57) | def output_formats(cmd, data, output) -> None:
  function test_state_list_table (line 74) | def test_state_list_table(basic_entities_text, basic_entities_table_text...
  function test_state_default_list_table (line 83) | def test_state_default_list_table(
  function test_state_list_tblformat (line 90) | def test_state_list_tblformat(
  function test_state_list_table_columns (line 101) | def test_state_list_table_columns(
  function test_state_list_table_columns_sortby (line 117) | def test_state_list_table_columns_sortby(
  function test_state_list_no_header (line 134) | def test_state_list_no_header(
  function test_state_get (line 145) | def test_state_get(basic_entities_text, basic_entities) -> None:
  function test_state_edit (line 169) | def test_state_edit(basic_entities_text, basic_entities) -> None:
  function test_state_toggle (line 203) | def test_state_toggle(basic_entities_text, basic_entities) -> None:
  function test_state_filter (line 233) | def test_state_filter(default_entities) -> None:

FILE: tests/test_template.py
  function _render_template (line 13) | def _render_template(content, data=None, strict=False):
  function test_render_plain_text (line 28) | def test_render_plain_text():
  function test_render_variable (line 33) | def test_render_variable():
  function test_render_environ_allowed (line 39) | def test_render_environ_allowed():
  function test_render_environ_blocked_secret (line 49) | def test_render_environ_blocked_secret():
  function test_render_environ_blocked_supervisor_token (line 62) | def test_render_environ_blocked_supervisor_token():
  function test_render_environ_missing (line 75) | def test_render_environ_missing():
  function test_safe_env_vars_no_secrets (line 83) | def test_safe_env_vars_no_secrets():
  function test_render_strict_undefined (line 89) | def test_render_strict_undefined():
  function test_sandbox_blocks_globals_access (line 96) | def test_sandbox_blocks_globals_access():
  function test_sandbox_blocks_builtins_via_globals (line 103) | def test_sandbox_blocks_builtins_via_globals():
  function test_sandbox_blocks_import (line 109) | def test_sandbox_blocks_import():
  function test_sandbox_blocks_os_system (line 119) | def test_sandbox_blocks_os_system():
  function test_sandbox_blocks_subclass_traversal (line 129) | def test_sandbox_blocks_subclass_traversal():
  function test_sandbox_blocks_mro_traversal (line 135) | def test_sandbox_blocks_mro_traversal():
  function test_sandbox_blocks_file_open (line 143) | def test_sandbox_blocks_file_open():
  function test_sandbox_blocks_reverse_shell (line 154) | def test_sandbox_blocks_reverse_shell():
Condensed preview — 72 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (295K chars).
[
  {
    "path": ".coveragerc",
    "chars": 366,
    "preview": "[run]\nsource = homeassistant_cli\n\n[report]\n# Regexes for lines to exclude from consideration\nexclude_lines =\n    # Have "
  },
  {
    "path": ".dockerignore",
    "chars": 147,
    "preview": "# General files\n.git\n.github\nconfig\n\n# Test related files\n.tox\n\n# Other virtualization methods\nvenv\n.vagrant\n\n# Temporar"
  },
  {
    "path": ".github/release-drafter.yml",
    "chars": 44,
    "preview": "template: |\n  ## What's Changed\n\n  $CHANGES\n"
  },
  {
    "path": ".github/workflows/publish-to-pypi.yml",
    "chars": 1224,
    "preview": "name: Publish to PyPI\n\non:\n  release:\n    types: [published]\n\npermissions:\n  contents: read\n\njobs:\n  deploy:\n\n    runs-o"
  },
  {
    "path": ".github/workflows/testing.yml",
    "chars": 1233,
    "preview": "name: Testing package\n\non:\n  push:\n    branches: [ master, dev ]\n  pull_request:\n    branches: [ master, dev ]\n\njobs:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 1011,
    "preview": "# Hide sublime text stuff\n*.sublime-project\n*.sublime-workspace\n\n# Hide vscode\n.vscode\n*.code-workspace\n\n# Hide some OS "
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1371,
    "preview": "repos:\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v3.21.2\n    hooks:\n      - id: pyupgrade\n  - repo: https"
  },
  {
    "path": "Dockerfile",
    "chars": 314,
    "preview": "FROM python:3.13-alpine\nLABEL maintainer=\"Fabian Affolter <fabian@affolter-engineering.ch>\"\n\nWORKDIR /usr/src/app\n\nCOPY "
  },
  {
    "path": "Dockerfile.armhf",
    "chars": 417,
    "preview": "# Python 3.11 with Alpine\nFROM balenalib/armv7hf-alpine-python:3.11-3.15\nLABEL maintainer=\"Fabian Affolter <fabian@affol"
  },
  {
    "path": "LICENSE.md",
    "chars": 10412,
    "preview": "Apache License\n==============\n\n_Version 2.0, January 2004_\n_&lt;<http://www.apache.org/licenses/>&gt;_\n\n### Terms and Co"
  },
  {
    "path": "MANIFEST.in",
    "chars": 103,
    "preview": "include README.rst\ninclude LICENSE.md\ngraft homeassistant_cli\ngraft tests\nrecursive-exclude * *.py[co]\n"
  },
  {
    "path": "README.rst",
    "chars": 20796,
    "preview": "Home Assistant Command-line Interface (``hass-cli``)\n====================================================\n\n|Coverage| |L"
  },
  {
    "path": "SECURITY.md",
    "chars": 240,
    "preview": "# Security Policy\n\n## Reporting a Vulnerability\n\nIf would find a vulernability in the code base, please contact the auth"
  },
  {
    "path": "docker-hass-cli",
    "chars": 754,
    "preview": "\n## Use the tag that best fits you\n## dev contains last build from dev branch.\n#TAG=dev\n## latest is latest released bui"
  },
  {
    "path": "homeassistant_cli/__init__.py",
    "chars": 51,
    "preview": "\"\"\"Init file for Home Assistant CLI (hass-cli).\"\"\"\n"
  },
  {
    "path": "homeassistant_cli/autocompletion.py",
    "chars": 5865,
    "preview": "\"\"\"Details for the auto-completion.\"\"\"\n\nimport os\n\nfrom requests.exceptions import HTTPError\n\nimport homeassistant_cli.r"
  },
  {
    "path": "homeassistant_cli/cli.py",
    "chars": 6842,
    "preview": "\"\"\"Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport os\nimport sys\nfrom typing import cast\n\nimport click\nimport c"
  },
  {
    "path": "homeassistant_cli/config.py",
    "chars": 7051,
    "preview": "\"\"\"Configuration for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport os\nimport sys\nfrom typing import Any, Dict"
  },
  {
    "path": "homeassistant_cli/const.py",
    "chars": 628,
    "preview": "\"\"\"Constants used by Home Assistant CLI (hass-cli).\"\"\"\n\nPACKAGE_NAME = \"homeassistant_cli\"\n\n__version__ = \"1.0.1\"\n\nAUTO_"
  },
  {
    "path": "homeassistant_cli/exceptions.py",
    "chars": 247,
    "preview": "\"\"\"The exceptions used by Home Assistant CLI.\"\"\"\n\n\nclass HomeAssistantCliError(Exception):\n    \"\"\"General Home Assistant"
  },
  {
    "path": "homeassistant_cli/hassconst.py",
    "chars": 18069,
    "preview": "\"\"\"Constants used by Home Assistant components.\n\nCopy of recent homeassistant.const to make hass-cli run\nwithout install"
  },
  {
    "path": "homeassistant_cli/helper.py",
    "chars": 5344,
    "preview": "\"\"\"Helpers used by Home Assistant CLI (hass-cli).\"\"\"\n\nimport ast\nimport contextlib\nimport json\nimport logging\nimport shl"
  },
  {
    "path": "homeassistant_cli/plugins/area.py",
    "chars": 3790,
    "preview": "\"\"\"Area (registry) plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re\nimport sys\nfrom re import Patt"
  },
  {
    "path": "homeassistant_cli/plugins/completion.py",
    "chars": 831,
    "preview": "\"\"\"Auto-completion for Home Assistant CLI (hass-cli).\"\"\"\n\nimport click\nfrom click._bashcomplete import get_completion_sc"
  },
  {
    "path": "homeassistant_cli/plugins/config.py",
    "chars": 1963,
    "preview": "\"\"\"Configuration plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport click\n\nimport homeassistant_cli.remote as api\nfrom"
  },
  {
    "path": "homeassistant_cli/plugins/device.py",
    "chars": 6908,
    "preview": "\"\"\"Device (registry) plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re\nimport sys\nfrom typing impor"
  },
  {
    "path": "homeassistant_cli/plugins/discover.py",
    "chars": 867,
    "preview": "\"\"\"Discovery plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport click\n\nfrom homeassistant_cli.cli import pass_context\n"
  },
  {
    "path": "homeassistant_cli/plugins/entity.py",
    "chars": 7168,
    "preview": "\"\"\"Entity plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re\nimport sys\n\nimport click\n\nimport homeas"
  },
  {
    "path": "homeassistant_cli/plugins/event.py",
    "chars": 2398,
    "preview": "\"\"\"Event plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json as json_\nimport logging\n\nimport click\n\nimport homeass"
  },
  {
    "path": "homeassistant_cli/plugins/ha.py",
    "chars": 21994,
    "preview": "\"\"\"Home Assistant Operating System plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json as json_\nimport logging\nfro"
  },
  {
    "path": "homeassistant_cli/plugins/info.py",
    "chars": 1520,
    "preview": "\"\"\"Information plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nfrom typing import Any, Dict, List\n\nimport c"
  },
  {
    "path": "homeassistant_cli/plugins/integration.py",
    "chars": 7879,
    "preview": "\"\"\"Integrations (config entries) plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re\nimport sys\n\nimpo"
  },
  {
    "path": "homeassistant_cli/plugins/map.py",
    "chars": 2089,
    "preview": "\"\"\"Map plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport sys\nimport webbrowser\n\nimport click\n\nimport homeassistant_cl"
  },
  {
    "path": "homeassistant_cli/plugins/raw.py",
    "chars": 2690,
    "preview": "\"\"\"Raw plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json as json_\nimport logging\nfrom typing import Any, Dict, L"
  },
  {
    "path": "homeassistant_cli/plugins/service.py",
    "chars": 3022,
    "preview": "\"\"\"Service plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re as reg\nimport sys\nfrom typing import A"
  },
  {
    "path": "homeassistant_cli/plugins/state.py",
    "chars": 8900,
    "preview": "\"\"\"Entity plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json as json_\nimport logging\nimport re\nfrom typing import"
  },
  {
    "path": "homeassistant_cli/plugins/system.py",
    "chars": 906,
    "preview": "\"\"\"System plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\n\nimport click\n\nimport homeassistant_cli.const as "
  },
  {
    "path": "homeassistant_cli/plugins/template.py",
    "chars": 2625,
    "preview": "\"\"\"Template plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport os\n\nimport click\nfrom jinja2 import File"
  },
  {
    "path": "homeassistant_cli/remote.py",
    "chars": 21266,
    "preview": "\"\"\"\nBasic API to access remote instance of Home Assistant.\n\nIf a connection error occurs while communicating with the AP"
  },
  {
    "path": "homeassistant_cli/yaml.py",
    "chars": 945,
    "preview": "\"\"\"Yaml utility for hass-cli.\"\"\"\n\nfrom typing import Any, cast\n\nfrom ruamel.yaml import YAML\nfrom ruamel.yaml.compat imp"
  },
  {
    "path": "mypy.ini",
    "chars": 335,
    "preview": "[mypy]\ncheck_untyped_defs = true\ndisallow_untyped_calls = true\nfollow_imports = silent\nignore_missing_imports = true\nno_"
  },
  {
    "path": "mypyrc",
    "chars": 50,
    "preview": "homeassistant_cli\nhomeassistant_cli/plugins\ntests\n"
  },
  {
    "path": "pylintrc",
    "chars": 1752,
    "preview": "[MESSAGES CONTROL]\n# Reasons disabled:\n# locally-disabled - it spams too much\n# duplicate-code - unavoidable\n# cyclic-im"
  },
  {
    "path": "pyproject.toml",
    "chars": 4194,
    "preview": "[tool.poetry]\nname = \"homeassistant-cli\"\nversion = \"1.0.1\"\ndescription  = \"Command-line tool for Home Assistant.\"\nlicens"
  },
  {
    "path": "tests/__init__.py",
    "chars": 46,
    "preview": "\"\"\"Init file for Home Assistant CLI tests.\"\"\"\n"
  },
  {
    "path": "tests/bandit.yaml",
    "chars": 188,
    "preview": "# https://bandit.readthedocs.io/en/latest/config.html\n\ntests:\n  - B108\n  - B306\n  - B307\n  - B313\n  - B314\n  - B315\n  - "
  },
  {
    "path": "tests/conftest.py",
    "chars": 1486,
    "preview": "\"\"\"conftest.py loads all fixtures found in fixtures/.\n\nEach file are made available as follows:\n\nGiven a file named: `my"
  },
  {
    "path": "tests/fixtures/basic_entities.json",
    "chars": 1171,
    "preview": "[{\n    \"attributes\": {\n      \"auto\": true,\n      \"entity_id\": [\n        \"remote.tv\"\n      ],\n      \"friendly_name\": \"fri"
  },
  {
    "path": "tests/fixtures/basic_entities_table.txt",
    "chars": 279,
    "preview": "ENTITY        DESCRIPTION         STATE    CHANGED\nsensor.one    friendly long name  on       2018-12-02T10:13:05.914548"
  },
  {
    "path": "tests/fixtures/basic_entities_table_columns.txt",
    "chars": 97,
    "preview": "entity              state\nfriendly long name  on\n                    off\n                    off\n"
  },
  {
    "path": "tests/fixtures/basic_entities_table_format.txt",
    "chars": 511,
    "preview": "<table>\n<thead>\n<tr><th>ENTITY      </th><th>DESCRIPTION       </th><th>STATE  </th><th>CHANGED                         "
  },
  {
    "path": "tests/fixtures/basic_entities_table_no_header.txt",
    "chars": 216,
    "preview": "sensor.one    friendly long name  on   2018-12-02T10:13:05.914548+00:00\nsensor.two                        off  2018-12-0"
  },
  {
    "path": "tests/fixtures/basic_entities_table_sorted.txt",
    "chars": 228,
    "preview": "entity              state    last_changed\n                    off      2018-12-01T12:17:52.434229+00:00\nfriendly long na"
  },
  {
    "path": "tests/fixtures/default_areas.json",
    "chars": 129,
    "preview": "[\n    { \"area_id\": 1, \"name\": \"Kitchen\"},\n    { \"area_id\": 2, \"name\": \"Kitchen Light\"},\n    { \"area_id\": 3, \"name\": \"Bed"
  },
  {
    "path": "tests/fixtures/default_devices.json",
    "chars": 3573,
    "preview": "[\n  {\n    \"config_entries\": [\n      \"424ae83a64a54fa8b6b01d71aa7d9b3d\"\n    ],\n    \"connections\": [],\n    \"manufacturer\":"
  },
  {
    "path": "tests/fixtures/default_entities.json",
    "chars": 19394,
    "preview": "[\n  {\n    \"attributes\": {\n      \"azimuth\": 342.38,\n      \"elevation\": -65.59,\n      \"friendly_name\": \"Sun\",\n      \"next_"
  },
  {
    "path": "tests/fixtures/default_events.json",
    "chars": 715,
    "preview": "[\n  {\n    \"event\": \"homeassistant_close\",\n    \"listener_count\": 3\n  },\n  {\n    \"event\": \"call_service\",\n    \"listener_co"
  },
  {
    "path": "tests/fixtures/default_services.json",
    "chars": 6334,
    "preview": "[\n  {\n    \"domain\": \"homeassistant\",\n    \"services\": {\n      \"check_config\": {\n        \"description\": \"Check the Home As"
  },
  {
    "path": "tests/test_area.py",
    "chars": 1066,
    "preview": "\"\"\"Testing Area operations.\"\"\"\n\nimport json\nimport unittest.mock as mock\n\nfrom click.testing import CliRunner\n\nimport ho"
  },
  {
    "path": "tests/test_completion.py",
    "chars": 2910,
    "preview": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nfrom typing import cast\n\nimport requests_mock\n\nimport homeassistant"
  },
  {
    "path": "tests/test_defaults.py",
    "chars": 3269,
    "preview": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport os\nfrom unittest import mock\n\nimport pytest\nimport requests_"
  },
  {
    "path": "tests/test_device.py",
    "chars": 2488,
    "preview": "\"\"\"Testing Device operations.\"\"\"\n\nimport json\nimport unittest.mock as mock\n\nfrom click.testing import CliRunner\n\nimport "
  },
  {
    "path": "tests/test_ha.py",
    "chars": 3235,
    "preview": "\"\"\"Tests for Home Assistant Operating System plugin (ha.py).\"\"\"\n\nimport requests_mock\nfrom click.testing import CliRunne"
  },
  {
    "path": "tests/test_helper.py",
    "chars": 1663,
    "preview": "\"\"\"Tests for helper.\"\"\"\n\nfrom collections.abc import Sized\nfrom typing import cast\n\nimport homeassistant_cli.helper as h"
  },
  {
    "path": "tests/test_info.py",
    "chars": 3687,
    "preview": "\"\"\"Tests for the info plugin.\"\"\"\n\nimport pytest\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.cli as cli"
  },
  {
    "path": "tests/test_integration.py",
    "chars": 10120,
    "preview": "\"\"\"Tests for the integration plugin.\"\"\"\n\nimport json\nfrom unittest import mock\n\nimport requests_mock\nfrom click.testing "
  },
  {
    "path": "tests/test_map.py",
    "chars": 1458,
    "preview": "\"\"\"Tests file for hass-cli map.\"\"\"\n\nfrom typing import no_type_check\nfrom unittest.mock import patch\n\nimport pytest\nimpo"
  },
  {
    "path": "tests/test_plugins.py",
    "chars": 1221,
    "preview": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport pytest\n\nfrom homeassistant_cli.cli import HomeAssistantCli, "
  },
  {
    "path": "tests/test_raw.py",
    "chars": 3431,
    "preview": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json\nimport unittest.mock as mocker\nfrom unittest.mock impor"
  },
  {
    "path": "tests/test_service.py",
    "chars": 2572,
    "preview": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json\n\nimport requests_mock\nfrom click.testing import CliRunn"
  },
  {
    "path": "tests/test_state.py",
    "chars": 8044,
    "preview": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json\n\nimport requests_mock\nfrom click.testing import CliRunn"
  },
  {
    "path": "tests/test_template.py",
    "chars": 5720,
    "preview": "\"\"\"Tests for template plugin.\"\"\"\n\nimport os\nimport tempfile\n\nimport pytest\nfrom jinja2.exceptions import UndefinedError\n"
  }
]

About this extraction

This page contains the full source code of the home-assistant/home-assistant-cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 72 files (265.5 KB), approximately 68.8k tokens, and a symbol index with 354 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!