From 49519f8bdf487aa28a367ee71c228e497c6b7647 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 27 Apr 2026 20:30:31 -0500 Subject: [PATCH 01/17] Modernize index.rst --- docs/conf.py | 5 ----- docs/index.rst | 30 +++++++++++++----------------- examples/basic.py | 12 +++++------- pyproject.toml | 1 + scripts/run_docs_dev.sh | 7 +------ 5 files changed, 20 insertions(+), 35 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index bb47cc19..91c1403c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -90,9 +90,4 @@ None, ), "homeassistant_api": ("https://homeassistantapi.readthedocs.io/en/latest", None), - "requests_cache": ("https://requests-cache.readthedocs.io/en/stable/", None), - "aiohttp_client_cache": ( - "https://aiohttp-client-cache.readthedocs.io/en/latest/", - None, - ), } diff --git a/docs/index.rst b/docs/index.rst index 643204d6..b95275df 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,12 +29,12 @@ Index Features ---------- -- Full consumption of the Home Assistant REST API endpoints. -- Full consumption of the Home Assistant Websocket API (all of the documented commands and some undocumented ones). -- Convenient Pydantic Models for data validation. -- Synchronous and asynchronous support for both REST and WebSocket clients. -- Modular design for intuitive readability. -- Request caching for more efficient repetitive requests. +1. Full consumption of the Home Assistant REST API endpoints. +2. Full consumption of the Home Assistant Websocket API (all of the documented commands and some undocumented ones). +3. Convenient Pydantic Models for data validation. +4. Synchronous and asynchronous support for both REST and WebSocket clients. +5. Modular design for intuitive readability. +6. Request caching for more efficient repetitive requests. Getting Started ------------------- @@ -63,14 +63,10 @@ View the documentation for each class and method :doc:`here `. Contributing Guidelines -------------------------- -We absolutely looooooooooove contributions! -This library has come a long way since its one-file humble beginning, on a Saturday afternoon with some our programming buddies. -But while much has been done already there is still much much much more to do! -Which is exciting! -If you're a developer that has an idea, suggestion or just wants to be helpful because you're an awesome person. -See our \*newly minted\* :ref:`Development and Contribution page ` for contribution ideas, guidelines, procedures and what to expect with your PR. -Happy developing! -We hope to see your PRs soon. - -.. - We would love to give a special shoutout to `FoxNerdSaysMoo ` for contributions to some of the awesome theme styling on these docs! +We absolutely love contributions! +This library has come a long way since its one-file humble beginning on a Saturday afternoon. +However, while much has been done already there is still much much much more to do, +which is as exciting as it daunting! +If you're a developer that has an idea/suggestion or just wants to be helpful, +see our :ref:`Development and Contribution page ` for contribution ideas to get started, guidance, and what to expect with your PR. +Happy developing, and we hope to see your PRs soon. diff --git a/examples/basic.py b/examples/basic.py index d6a7e946..0b85d0a8 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -2,18 +2,16 @@ from homeassistant_api import Client -api_url = "https://homeassistant.duckdns.org:8123/api" # Something like http://localhost:8123/api +api_url = "http://localhost:8123/api" # Something like http://:8123/api, the /api is important! token = os.getenv( - "HOMEASSISTANT_TOKEN", -) # Used to aunthenticate yourself with homeassistant + "HOMEASSISTANT_TOKEN", # Used to aunthenticate yourself with homeassistant +) # See the documentation on how to obtain a Long Lived Access Token def main() -> None: - with Client( - api_url, - token, - ) as client: # Create Client object and check that its running. + # Create Client object and check that its running. + with Client(api_url, token) as client: cover = client.get_domain("cover") if cover is None: return diff --git a/pyproject.toml b/pyproject.toml index 6fd6de9c..3afcc793 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ docs = [ "sphinx-autodoc-typehints>=3,<4", "sphinx-rtd-theme>=3,<4", "autodoc-pydantic>=2,<3", + "sphinx-autobuild>=2025.8.25", ] dev = [ "types-docutils>=0.22", diff --git a/scripts/run_docs_dev.sh b/scripts/run_docs_dev.sh index 793eac6d..f636068f 100755 --- a/scripts/run_docs_dev.sh +++ b/scripts/run_docs_dev.sh @@ -1,7 +1,2 @@ #!/usr/bin/env bash -rm -rf build -mkdir build -sphinx-build docs build -cd build -python -m http.server -cd ../ \ No newline at end of file +sphinx-autobuild docs/ build/ --open-browser --port 8000 --watch examples/ --watch homeassistant_api/ \ No newline at end of file From 9d039a1c96dd3038c5567685405701f34fc0d86a Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 27 Apr 2026 20:48:51 -0500 Subject: [PATCH 02/17] Modernize quickstart --- docs/conf.py | 14 ++++++++------ docs/quickstart.rst | 23 +++++++++++++---------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 91c1403c..72937cd6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,6 +23,7 @@ project = "Homeassistant API" copyright = "2023-2025, Nathan Larsen" # pylint: disable=redefined-builtin author = "Nathan Larsen" +repo_url = "https://github.com/GrandMoff100/HomeassistantAPI" # The full version, including alpha/beta/rc tags with open("../pyproject.toml") as f: @@ -51,11 +52,12 @@ autodoc_pydantic_model_show_json = False resource_links = { - "repo": "https://github.com/GrandMoff100/HomeassistantAPI/", - "issues": "https://github.com/GrandMoff100/HomeassistantAPI/issues", - "discussions": "https://github.com/GrandMoff100/HomeassistantAPI/discussions", - "examples": f"https://github.com/GrandMoff100/HomeassistantAPI/tree/{branch}/examples", - "new_pr": "https://github.com/GrandMoff100/HomeAssistantAPI/compare", + "repo": repo_url, + "issues": f"{repo_url}/issues", + "discussions": f"{repo_url}/discussions", + "examples": f"{repo_url}/tree/{branch}/examples", + "new_pr": f"{repo_url}/compare", + "pypi": "https://pypi.org/project/homeassistant_api", } # Add any paths that contain templates here, relative to this directory. @@ -89,5 +91,5 @@ "https://docs.python.org/3", None, ), - "homeassistant_api": ("https://homeassistantapi.readthedocs.io/en/latest", None), + "homeassistant_api": ("https://homeassistantapi.readthedocs.io/en/stable", None), } diff --git a/docs/quickstart.rst b/docs/quickstart.rst index c93b2273..8f6878dd 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -5,7 +5,7 @@ Quickstart Prerequisites ============== -Homeassistant +1. Homeassistant --------------- Before using this library, you need to have Home Assistant running on a device. Something like a `Raspberry Pi 3 or 4 `_ or spare laptop. @@ -15,7 +15,7 @@ See `here `__ for how to install th Configuring the REST API Server in Homeassistant ======================================================= -Enable the :code:`api` integration in Homeassistant +1. Enable the :code:`api` integration in Homeassistant ------------------------------------------------------ This library requires that the :code:`api` integration on your Home Assistant is enabled. It is enabled by default with the :code:`default_config` integration. @@ -26,7 +26,7 @@ If you are not sure if it is enabled or not, chances are if your frontend is ena .. _access_token_setup: -Access Token +2. Access Token -------------- Then once you have done that you need to head over to your profile and set up a "Long Lived Access Token" to use in your code later. A good guide on how to do that is `here `__ @@ -34,21 +34,23 @@ Also if you are building a website and want to integrate Home Assistant you can See their `Authentication API docs `__ for more information. Every time you refresh your token you will need to update the :py:attr:`Client.token` attribute of your :py:class:`Client` instance. -Exposing Home Assistant to the Web +3. Exposing Home Assistant to the Web (optional) -------------------------------------- You may want to setup remote access through a Dynamic DNS server like DuckDNS (a good youtube tutorial on how to do that `here `__, keep in mind you will need to port forward to set that up.) If you do pursue this your API URL will be something like :code:`https://yourhomeassistant.duckdns.org:8123/api`. -Which is different than what it could have looked like before. -Which might have been something like :code:`http://homeassistant.local:8123/api` or :code:`http://localhost:8123/api` +as opposed to what it might have looked like before, :code:`http://homeassistant.local:8123/api` or :code:`http://localhost:8123/api`. Installation ============== -Installing with :code:`pip` +There are a variety of our different ways you can install the library. + +Installing with :code:`pip` (recommended) ----------------------------------- Installation with pip is really easy and will install the dependencies this project needs. +This installs the latest stable version from :resource:`PyPI ` .. code-block:: bash @@ -62,7 +64,8 @@ Installation with pip is really easy and will install the dependencies this proj Installing with :code:`git` ---------------------------------- -To install with git we're going to clone the repository and then run :code:`$ uv sync` like so. +This installs the latest development version directly from :resource:`GitHub `. +To install with :code:`git` we're going to clone the repository and then run :code:`$ uv sync` like so. .. code-block:: bash @@ -85,8 +88,8 @@ If run into any problems open an issue on our github :resource:`issue tracker `! We hope to see your project soon! From 1549dfdfc76a116fdb956ec0d3ef96247cda0b89 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 4 May 2026 20:08:15 -0500 Subject: [PATCH 03/17] Update contributing section --- docs/CONTRIBUTING.rst | 22 +++++++++++++--------- docs/conf.py | 3 ++- docs/quickstart.rst | 4 ++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/CONTRIBUTING.rst b/docs/CONTRIBUTING.rst index 85a15677..957378e6 100644 --- a/docs/CONTRIBUTING.rst +++ b/docs/CONTRIBUTING.rst @@ -11,7 +11,12 @@ Contribution Ideas ********************* If you don't know what you want to contribute yet you should take a look at our :resource:`issues page `. -See what other people have been up to and if you have an idea for a new feature or a new way to implement a feature you should :resource:`create an issue ` or :resource:`fork the repository ` and start contributing. +Some other places to start with are: + +- **Writing and Rewriting Documentation**---as more code changes happen and this library continues to mature, more features get added which need documenting. +- **Adding examples**--- + +See what other people have been up to all across the home assistant community and if you have an idea for a new feature you should :resource:`create an issue ` or :resource:`fork the repository ` and start contributing. We're always interested in integrating ways to make the library faster, extensible and easier to use. Setting up your Development Environment @@ -22,7 +27,7 @@ So now that you know what you want to contribute it is time to setup a developme Step One: Fork the Repository =============================== -Click `here `__ to fork the repository. +Click :resource:`here ` to fork the repository. Then click your username. Step Two: Clone the Repository Locally @@ -95,9 +100,9 @@ If you add a new test that makes real HTTP or WebSocket requests, you need to re Code Styling Guidelines ************************** -In order to make sure that our code is easy to read, and navigate. -As well as to stop stupid mistakes like typos, undefined variables, etc. -We enforce code standards. +In order to make sure that our code is easy to read, and navigate +as well as to stop stupid mistakes like typos, undefined variables, etc, +we enforce code standards. Using the tools, :code:`ruff`, :code:`zuban`, and :code:`pytest`, we make sure that our code quality is top notch and that changes work everywhere. You can those tools manually yourself, but they also run automatically when you open a PR. @@ -105,10 +110,9 @@ Merging Your Contributions ***************************** Once you have tested your changes and committed them to your fork you can merge them back into the :resource:`original repository `. -Head over to the :resource:`Pull Request Page ` and select your fork to merge into the `GrandMoff100/dev` branch. +Head over to the :resource:`Pull Request Page ` and select your fork to merge into the main branch. Then you can hit "Create Pull Request" and we'll review it as soon as possible. -In order to be merged though, your code needs to follow our :ref:`Styling Guidelines `. -A Github Actions workflow will run on your PR automatically to verify that it does follow the guidelines. -Then once the checks have passed one of our maintainers will review the changes (basically to make sure your changes won't break anything ;)). +In order to be merged, a Github Actions workflow will run on your PR automatically, to check that your code passes the styling checks and tests. +Then once the checks have passed, one of our maintainers will review the changes and ask for clarification or changes (if needed). Then after that your changes will get merged and will be available in the next release! diff --git a/docs/conf.py b/docs/conf.py index 72937cd6..0f475a3b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,8 +56,9 @@ "issues": f"{repo_url}/issues", "discussions": f"{repo_url}/discussions", "examples": f"{repo_url}/tree/{branch}/examples", - "new_pr": f"{repo_url}/compare", + "new_pr": f"{repo_url}/pulls", "pypi": "https://pypi.org/project/homeassistant_api", + "fork": f"{repo_url}/fork", } # Add any paths that contain templates here, relative to this directory. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8f6878dd..232f0e3b 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -86,8 +86,8 @@ Then you should be all set to start using the library! If run into any problems open an issue on our github :resource:`issue tracker ` -Example Usages -================ +Example Use Case Ideas +======================= Some examples applications of this project include integrating it into a another library, web application or a custom automation script. Maybe you want to start a project that allows you to use your Home Assistant from your command line but with some sassy responses. Or maybe add it to a discord bot to manage your Home Assistant from inside discord. From b591f2ef405bfec90fbb55b315e19f92490d6bd6 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 5 May 2026 13:38:07 -0500 Subject: [PATCH 04/17] Continue working on the docs... --- docs/api.rst | 81 +++++++++++++++++++++++++++-- docs/conf.py | 17 ++++-- docs/quickstart.rst | 8 +-- homeassistant_api/__init__.py | 5 ++ homeassistant_api/client.py | 2 +- homeassistant_api/models/domains.py | 11 +++- 6 files changed, 109 insertions(+), 15 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 0007e0ed..73375ffd 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,7 +1,80 @@ Code Reference *************** -.. automodule:: homeassistant_api - :platform: Linux, Windows, MacOS - :inherited-members: - :exclude-members: model_json_schema, model_copy, model_rebuild, model_dump, construct, copy, dict, from_orm, json, parse_file, model_validate, parse_raw, parse_obj, parse_str, parse_url, schema, schema_json, schema_yaml, schema_yml, to_orm, update_forward_refs, validate, validate_file, validate_obj, validate_raw, validate_str, validate_url, model_validate_strings, model_validate_json, model_validate, model_post_init, model_parametrized_name, model_extra, model_fields_set, model_dump_json, model_construct, model_computed_fields \ No newline at end of file +Here you can find a detailed reference for most classes and methods in the library. + + +Clients +======= + +.. autoclass:: homeassistant_api.Client + :members: + + +.. autoclass:: homeassistant_api.AsyncClient + :members: + + +.. autoclass:: homeassistant_api.WebsocketClient + :members: + + +Data Models +=========== + + +Domains +-------- + +.. automodule:: homeassistant_api.models.domains + :members: + +Entities +-------- + +.. automodule:: homeassistant_api.models.entity + :members: + + +Events +------- + +.. automodule:: homeassistant_api.models.events + :members: + +History +-------- + +.. automodule:: homeassistant_api.models.history + :members: + +Logbook +-------- + +.. automodule:: homeassistant_api.models.logbook + :members: + +Entity States +-------------- + +.. automodule:: homeassistant_api.models.states + :members: + +Websocket Models +---------------- + +.. automodule:: homeassistant_api.models.websocket + :members: + +Request Processing +================== + +.. automodule:: homeassistant_api.processing + :members: + + +Errors +======= + +.. automodule:: homeassistant_api.errors + :members: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 0f475a3b..07d1cbdb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,13 +15,14 @@ import re import sys + sys.path.insert(0, os.path.abspath("../")) sys.path.append(os.path.abspath("extensions")) # -- Project information ----------------------------------------------------- project = "Homeassistant API" -copyright = "2023-2025, Nathan Larsen" # pylint: disable=redefined-builtin +copyright = "2023-2026, Nathan Larsen" # pylint: disable=redefined-builtin author = "Nathan Larsen" repo_url = "https://github.com/GrandMoff100/HomeassistantAPI" @@ -50,6 +51,8 @@ ] autodoc_pydantic_model_show_json = False +autodoc_pydantic_model_show_config = False + resource_links = { "repo": repo_url, @@ -86,11 +89,15 @@ autodoc_default_options = { "members": True, "member-order": "bysource", + "exclude-members": "model_config", } intersphinx_mapping = { - "python": ( - "https://docs.python.org/3", - None, - ), + "python": ("https://docs.python.org/3", None), "homeassistant_api": ("https://homeassistantapi.readthedocs.io/en/stable", None), + "niquests": ("https://niquests.readthedocs.io/en/stable", None), + "niquests-cache": ("https://niquests-cache.readthedocs.io/en/stable/", None) } + +autodoc_type_aliases = { + "JsonValue": "typing.Any", +} \ No newline at end of file diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 232f0e3b..f982b7c1 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -6,7 +6,7 @@ Prerequisites ============== 1. Homeassistant ---------------- +---------------- Before using this library, you need to have Home Assistant running on a device. Something like a `Raspberry Pi 3 or 4 `_ or spare laptop. If you don't want to do that you can setup a Home Assistant container on your laptop or desktop with docker. @@ -27,7 +27,7 @@ If you are not sure if it is enabled or not, chances are if your frontend is ena .. _access_token_setup: 2. Access Token --------------- +--------------- Then once you have done that you need to head over to your profile and set up a "Long Lived Access Token" to use in your code later. A good guide on how to do that is `here `__ Also if you are building a website and want to integrate Home Assistant you can use a refresh token instead. @@ -35,7 +35,7 @@ See their `Authentication API docs `__, keep in mind you will need to port forward to set that up.) If you do pursue this your API URL will be something like :code:`https://yourhomeassistant.duckdns.org:8123/api`. @@ -47,7 +47,7 @@ Installation There are a variety of our different ways you can install the library. Installing with :code:`pip` (recommended) ------------------------------------ +----------------------------------------- Installation with pip is really easy and will install the dependencies this project needs. This installs the latest stable version from :resource:`PyPI ` diff --git a/homeassistant_api/__init__.py b/homeassistant_api/__init__.py index caeff0e3..8227e1c4 100644 --- a/homeassistant_api/__init__.py +++ b/homeassistant_api/__init__.py @@ -11,11 +11,13 @@ "AuthInvalid", "AuthOk", "AuthRequired", + "BaseClient", "BaseDomain", "BaseEntity", "BaseEvent", "BaseGroup", "BaseService", + "BaseWebsocketClient", "Client", "ConfigEntry", "ConfigEntryChange", @@ -51,10 +53,13 @@ "ServiceField", "State", "WebsocketClient", + "WebsocketClient", ) from .asyncclient import AsyncClient from .asyncwebsocket import AsyncWebsocketClient +from .baseclient import BaseClient +from .basewebsocket import BaseWebsocketClient from .client import Client from .models.config_entries import ConfigEntry from .models.config_entries import ConfigEntryChange diff --git a/homeassistant_api/client.py b/homeassistant_api/client.py index 66cdd7d1..e41ed1b6 100644 --- a/homeassistant_api/client.py +++ b/homeassistant_api/client.py @@ -44,7 +44,7 @@ class Client(BaseClient): :param token: The refresh or long lived access token to authenticate your requests. Required. :param session: A custom :py:class:`niquests.Session` instance. Optional. :param verify_ssl: Whether to verify SSL certificates. Default :code:`True`. - :param global_request_kwargs: Kwargs to pass to :func:`requests.request`. Optional. + :param global_request_kwargs: Kwargs to pass to :py:meth:`niquests.Session.request`. Optional. """ # pylint: disable=line-too-long _session: Session diff --git a/homeassistant_api/models/domains.py b/homeassistant_api/models/domains.py index 589ac488..294465f4 100644 --- a/homeassistant_api/models/domains.py +++ b/homeassistant_api/models/domains.py @@ -24,6 +24,15 @@ from .states import State +__all__ = ( + "AsyncDomain", + "BaseDomain", + "Domain", + "BaseService", + "AsyncService", + "Service" +) + class BaseDomain(BaseModel): """Model representing the domain that services belong to.""" @@ -64,7 +73,7 @@ def _add_service(self, service_id: str, **data: Any) -> None: raise NotImplementedError def get_service(self, service_id: str) -> BaseService | None: - """Return a Service with the given service_id, returns None if no such service exists""" + """Return a Service with the given :code:`service_id`, returns None if no such service exists""" return self.services.get(service_id) def __getattr__(self, attr: str) -> Any: From 991a1df31c36cdff9563ecbc1eed665f3858b117 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 5 May 2026 13:43:44 -0500 Subject: [PATCH 05/17] Switch to niquests-cache --- docs/advanced.rst | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index 3103adf5..7c7aba90 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -20,22 +20,19 @@ Persistent Caching If you want your cache to persist between runs (e.g. to a filesystem), you can pass your own custom cached session via the :code:`session` parameter. -Depending on whether you are using a sync or async client you will want to use either :py:class:`requests_cache.CachedSession` or :py:class:`aiohttp_client_cache.session.CachedSession` respectively. -See the docs for `requests_cache `__ and `aiohttp_client_cache `__ for backend options and more. +Depending on whether you are using a sync or async client you will want to use either :py:class:`niquests_cache.session.CachedSession` or :py:class:`niquests_cache.session.AsyncCachedSession` respectively. +See the docs for `niquests_cache `__ for backend options and more. .. code-block:: python from datetime import timedelta from homeassistant_api import Client - from requests_cache import CachedSession + from niquests_cache.session import CachedSession client = Client( "", "", - session=CachedSession( - backend="filesystem", - expire_after=timedelta(minutes=5), - ), + session=CachedSession(cache_name=Path('.cache') / 'http'), # defaults to sqlite cache ) with client: @@ -48,15 +45,13 @@ See the docs for `requests_cache ", "", - session=CachedSession( - cache=FileBackend( - expire_after=timedelta(minutes=5), - ), + session=AsyncCachedSession( + cache_name=Path('.cache') / 'http', ), ) @@ -68,7 +63,7 @@ See the docs for `requests_cache Date: Tue, 5 May 2026 16:36:44 -0500 Subject: [PATCH 06/17] Fix autodoc_pydantic --- docs/api.rst | 19 +++++++++++++++---- docs/conf.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 73375ffd..cc730601 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -14,47 +14,58 @@ Clients .. autoclass:: homeassistant_api.AsyncClient :members: +.. autoclass:: homeassistant_api.BaseClient + .. autoclass:: homeassistant_api.WebsocketClient :members: +.. autoclass:: homeassistant_api.BaseWebsocketClient + :members: + + Data Models =========== -Domains +Domain -------- .. automodule:: homeassistant_api.models.domains :members: + :inherited-members: -Entities +Entity -------- .. automodule:: homeassistant_api.models.entity :members: + :inherited-members: -Events +Event ------- .. automodule:: homeassistant_api.models.events :members: + :inherited-members: History -------- .. automodule:: homeassistant_api.models.history :members: + :inherited-members: Logbook -------- .. automodule:: homeassistant_api.models.logbook :members: + :inherited-members: -Entity States +State -------------- .. automodule:: homeassistant_api.models.states diff --git a/docs/conf.py b/docs/conf.py index 07d1cbdb..be5ae758 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ ] autodoc_pydantic_model_show_json = False -autodoc_pydantic_model_show_config = False +autodoc_pydantic_model_show_config_summary = False resource_links = { @@ -89,7 +89,47 @@ autodoc_default_options = { "members": True, "member-order": "bysource", - "exclude-members": "model_config", + "exclude-members": ",".join( + [ + "model_json_schema", + "model_copy", + "model_rebuild", + "model_dump", + "construct", + "copy", + "dict", + "from_orm", + "json", + "parse_file", + "model_validate", + "parse_raw", + "parse_obj", + "parse_str", + "parse_url", + "schema", + "schema_json", + "schema_yaml", + "schema_yml", + "to_orm", + "update_forward_refs", + "validate", + "validate_file", + "validate_obj", + "validate_raw", + "validate_str", + "validate_url", + "model_validate_strings", + "model_validate_json", + "model_validate", + "model_post_init", + "model_parametrized_name", + "model_extra", + "model_fields_set", + "model_dump_json", + "model_construct", + "model_computed_fields", + ] + ) } intersphinx_mapping = { "python": ("https://docs.python.org/3", None), diff --git a/pyproject.toml b/pyproject.toml index 3afcc793..23944a95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ Repository = "https://github.com/GrandMoff100/HomeAssistantAPI" [dependency-groups] docs = [ + "sphinx>=7,<9", "sphinx-autodoc-typehints>=3,<4", "sphinx-rtd-theme>=3,<4", "autodoc-pydantic>=2,<3", From c8e1e9d7c5aa13bef10e7e1ca91c1d37385fd9bd Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 5 May 2026 17:38:19 -0500 Subject: [PATCH 07/17] Modernize the caching docs --- docs/advanced.rst | 27 +++++++++++++++++------ docs/conf.py | 9 ++------ docs/extensions/resourcelinks.py | 6 ++--- homeassistant_api/models/domains.py | 6 ++--- tests/test_client.py | 34 ++++++++++++++++++----------- 5 files changed, 48 insertions(+), 34 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index 7c7aba90..53615df9 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -5,24 +5,37 @@ Advanced Section Caching ********** -By default, caching is **disabled**. You can enable the built-in in-memory cache by passing :code:`use_cache=True`: +The packaged :py:class:`Client` and :py:class:`AsyncClient` do not come with any built-in caching. +A convenient option is to use the :py:class:`niquests_cache.session.CachedSession` or :py:class:`niquests_cache.session.AsyncCachedSession` classes from the `niquests_cache` library. + .. code-block:: python from homeassistant_api import Client - client = Client("", "", use_cache=True) + from niquests_cache.session import CachedSession + from niquests_cache.backend import MemoryBackend + + client = Client("", "", session=CachedSession(backend=MemoryBackend(), expire_after=300)) + +.. code-block:: python + from homeassistant_api import AsyncClient + + from niquests_cache.session import AsyncCachedSession + from niquests_cache.backend import MemoryBackend + + client = AsyncClient("", "", session=AsyncCachedSession(backend=MemoryBackend(), expire_after=300)) + + +This creates an in-memory cache that expires after 300 seconds. You can adjust the `expire_after` value to fit your needs or set it to `-1` to disable expiration. +For more information on the available caching options, see the `niquests_cache ` documentation. -This creates an in-memory cache that expires after 300 seconds. Persistent Caching ******************** If you want your cache to persist between runs (e.g. to a filesystem), you can pass your own custom cached session via the :code:`session` parameter. -Depending on whether you are using a sync or async client you will want to use either :py:class:`niquests_cache.session.CachedSession` or :py:class:`niquests_cache.session.AsyncCachedSession` respectively. -See the docs for `niquests_cache `__ for backend options and more. - .. code-block:: python from datetime import timedelta @@ -32,7 +45,7 @@ See the docs for `niquests_cache ", "", - session=CachedSession(cache_name=Path('.cache') / 'http'), # defaults to sqlite cache + session=CachedSession(cache_name=Path('.cache') / 'http'), # by default uses sqlite backend ) with client: diff --git a/docs/conf.py b/docs/conf.py index be5ae758..72daac30 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,6 @@ import re import sys - sys.path.insert(0, os.path.abspath("../")) sys.path.append(os.path.abspath("extensions")) @@ -129,15 +128,11 @@ "model_construct", "model_computed_fields", ] - ) + ), } intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "homeassistant_api": ("https://homeassistantapi.readthedocs.io/en/stable", None), "niquests": ("https://niquests.readthedocs.io/en/stable", None), - "niquests-cache": ("https://niquests-cache.readthedocs.io/en/stable/", None) + "niquests-cache": ("https://niquests-cache.readthedocs.io/en/stable/", None), } - -autodoc_type_aliases = { - "JsonValue": "typing.Any", -} \ No newline at end of file diff --git a/docs/extensions/resourcelinks.py b/docs/extensions/resourcelinks.py index 9c807c93..4c285f63 100644 --- a/docs/extensions/resourcelinks.py +++ b/docs/extensions/resourcelinks.py @@ -5,10 +5,8 @@ from typing import Any import sphinx -from docutils import nodes -from docutils import utils -from docutils.nodes import Node -from docutils.nodes import system_message +from docutils import nodes, utils +from docutils.nodes import Node, system_message from docutils.parsers.rst.states import Inliner from sphinx.application import Sphinx from sphinx.util.nodes import split_explicit_title diff --git a/homeassistant_api/models/domains.py b/homeassistant_api/models/domains.py index 294465f4..3e2acc47 100644 --- a/homeassistant_api/models/domains.py +++ b/homeassistant_api/models/domains.py @@ -26,11 +26,11 @@ __all__ = ( "AsyncDomain", + "AsyncService", "BaseDomain", - "Domain", "BaseService", - "AsyncService", - "Service" + "Domain", + "Service", ) diff --git a/tests/test_client.py b/tests/test_client.py index 01ba575c..9de93ae8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -9,11 +9,18 @@ from homeassistant_api import WebsocketClient from homeassistant_api.baseclient import BaseClient +HA_URL = os.environ.get("HOMEASSISTANTAPI_URL", "http://localhost:8123/api") +HA_WS_URL = os.environ.get( + "HOMEASSISTANTAPI_WS_URL", + "ws://localhost:8123/api/websocket", +) +HA_TOKEN = os.environ.get("HOMEASSISTANTAPI_TOKEN", "") + def test_custom_session(nimax_session: niquests.Session) -> None: with Client( - os.environ.get("HOMEASSISTANTAPI_URL", "http://localhost:8123/api"), - os.environ.get("HOMEASSISTANTAPI_TOKEN", ""), + HA_URL, + HA_TOKEN, session=nimax_session, ): pass @@ -21,35 +28,36 @@ def test_custom_session(nimax_session: niquests.Session) -> None: def test_default_session(nimax_session: niquests.Session) -> None: # noqa: ARG001 with Client( - os.environ.get("HOMEASSISTANTAPI_URL", "http://localhost:8123/api"), - os.environ.get("HOMEASSISTANTAPI_TOKEN", ""), + HA_URL, + HA_TOKEN, ): pass async def test_custom_async_session(nimax_async_session: niquests.AsyncSession) -> None: async with AsyncClient( - os.environ.get("HOMEASSISTANTAPI_URL", "http://localhost:8123/api"), - os.environ.get("HOMEASSISTANTAPI_TOKEN", ""), + HA_URL, + HA_TOKEN, session=nimax_async_session, ): pass async def test_default_async_session( - nimax_async_session: niquests.AsyncSession, # noqa: ARG001 + nimax_async_session: niquests.AsyncSession, ) -> None: async with AsyncClient( - os.environ.get("HOMEASSISTANTAPI_URL", "http://localhost:8123/api"), - os.environ.get("HOMEASSISTANTAPI_TOKEN", ""), + HA_URL, + HA_TOKEN, + session=nimax_async_session, ): pass def test_websocket_client_ping(nimax_session: niquests.Session) -> None: with WebsocketClient( - os.environ.get("HOMEASSISTANTAPI_WS_URL", "ws://localhost:8123/api/websocket"), - os.environ.get("HOMEASSISTANTAPI_TOKEN", ""), + HA_WS_URL, + HA_TOKEN, session=nimax_session, ) as client: assert client.ping_latency() > 0 @@ -59,8 +67,8 @@ async def test_async_websocket_client_ping( nimax_async_session: niquests.AsyncSession, ) -> None: async with AsyncWebsocketClient( - os.environ.get("HOMEASSISTANTAPI_WS_URL", "ws://localhost:8123/api/websocket"), - os.environ.get("HOMEASSISTANTAPI_TOKEN", ""), + HA_WS_URL, + HA_TOKEN, session=nimax_async_session, ) as client: assert (await client.ping_latency()) > 0 From b4baa0afe22a5d8a68ac7359f121d6c173023c50 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 5 May 2026 22:58:49 -0500 Subject: [PATCH 08/17] Update contributing docs --- docs/CONTRIBUTING.rst | 48 +++++++++++++--- docs/usage.rst | 129 ++++++++++++++++++++++-------------------- 2 files changed, 108 insertions(+), 69 deletions(-) diff --git a/docs/CONTRIBUTING.rst b/docs/CONTRIBUTING.rst index 957378e6..205169d4 100644 --- a/docs/CONTRIBUTING.rst +++ b/docs/CONTRIBUTING.rst @@ -13,8 +13,8 @@ Contribution Ideas If you don't know what you want to contribute yet you should take a look at our :resource:`issues page `. Some other places to start with are: -- **Writing and Rewriting Documentation**---as more code changes happen and this library continues to mature, more features get added which need documenting. -- **Adding examples**--- +- **Writing and Rewriting Documentation**--- As more code changes happen and this library continues to mature, more features get added which need documenting. +- **Adding examples**--- We'd appreciate more self-contained examples that show common use cases. See what other people have been up to all across the home assistant community and if you have an idea for a new feature you should :resource:`create an issue ` or :resource:`fork the repository ` and start contributing. We're always interested in integrating ways to make the library faster, extensible and easier to use. @@ -43,21 +43,54 @@ Step Three: Installing Dependencies ====================================== Firstly, you need to have Python 3.11 or newer installed. -Download the latest Python Version from `here `__. +Download the latest Python Version from `here `__ (or your usual method of installation). Then you need to install :code:`uv`, a fast Python package manager. Checkout the `uv Docs `__. You can install it with :code:`pip` by running :code:`pip install uv`, or see the uv docs for other installation methods. Now you can install the project's dependencies by running :code:`cd HomeAssistantAPI && uv sync` -Step Four: [Optional] Setting Up a Home Assistant Development Environment. +Step Four: [Optional] Setting Up a Home Assistant Server ============================================================================= +Option A. Have a Home Assistant installation running. +------------------------------------------------------ + +If you already have a Home Assistant installation running, you can use it for development. +You'll just need to have the API URL and a Long-Lived Access Token available like you would to use the library normally. + + +Option B. Setup a Home Assistant Development Environment. +---------------------------------------------------------- + If you do not have a Home Assistant installation running already, you can setup a Home Assistant Development environment. -Which is basically a local, unpackaged, Home Assistant Core installation, that runs with just Python (no Docker or Operating System). +This is basically a local, unpackaged, Home Assistant Core installation, that runs with just Python (no Docker or Operating System). You can start and stop the server really easily as it runs just in your -terminal and gives you lots of control over it, making it ideal for testing your changes to Home Assistant API. +terminal and gives you an large affordance of control over it, making it ideal for quickly testing your changes. Follow this great guide `here `__ to do that. -After that you are now ready to make your changes to the codebase! +You'll access the web dashboard to create the Long-Lived Access Token. + + +Option C. Use a Docker-based Development Environment. +----------------------------------------------------- + +If you prefer to use Docker, you can use your own Docker setup to run a Home Assistant development environment. +There is also a Dockerfile and compose file that comes with the repository to make it convenient to spin up a development environment. +To do so, you can run + +.. code-block:: bash + + $ docker-compose up server + +which spins up a container running Home Assistant with port 8123 exposed to your local machine. +You'll need the following environment variables set to use the repository docker setup: + +.. code-block:: bash + + HOMEASSISTANTAPI_URL=http://localhost:8123/api + HOMEASSISTANTAPI_WS_URL=ws://localhost:8123/api/websocket + HOMEASSISTANTAPI_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkMDE4YjQ4YzMyZTE0ODNhYjY2ZWQzOTZmYzg3ZDAyNiIsImlhdCI6MTY3ODU3NDUwMSwiZXhwIjoxOTkzOTM0NTAxfQ.fyhnfwpont4uE0gn46_Ut_pPmyn4QWv0MDaVAei2PPk + + Testing ******** @@ -106,6 +139,7 @@ we enforce code standards. Using the tools, :code:`ruff`, :code:`zuban`, and :code:`pytest`, we make sure that our code quality is top notch and that changes work everywhere. You can those tools manually yourself, but they also run automatically when you open a PR. + Merging Your Contributions ***************************** diff --git a/docs/usage.rst b/docs/usage.rst index cb8f71c5..080d3d7a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -123,6 +123,73 @@ Entities # All of these methods work with the WebsocketClient as well. + +Using the :py:class:`WebsocketClient` +**************************************** + +Using Events (Listening and Firing) +------------------------------------ + +.. code-block:: python + + from homeassistant_api import WebsocketClient + + WS_URL = '' # Example: 'ws://homeassistant.local:8123/api/websocket' + TOKEN = '' + + with WebsocketClient(WS_URL, TOKEN) as ws_client: + with ws_client.listen_events() as events: + for event in events: + print(event) + + # Or if you want to listen for a specific event type until dinner time. + with ws_client.listen_events('state_changed') as events: + for event in events: + print(event) + if event.data.entity_id == 'myalarmclock.dinner_time' and event.data.new_state.state == 'now': + break + + # Or if you want to listen for just 10 events. + with ws_client.listen_events("my_event") as events: + for _, event in zip(range(10), events): + print(event) + + # Alternatively for just one event. + with ws_client.listen_events("my_event") as events: + event = next(events) + print(event) + + # Now to fire an event. + ws_client.fire_event("my_event", my_arg="my_value") + + +Listening for Triggers +------------------------- + +.. code-block:: python + + from homeassistant_api import WebsocketClient + + with WebsocketClient(WS_URL, TOKEN) as ws_client: + with ws_client.listen_trigger() as triggers: # see WebsocketClient.listen_trigger for more info. + for trigger in triggers: + print(trigger) + + # Another more specific example, listening for event triggers. + with ws_client.listen_trigger("event", event_type="my_event") as triggers: + ws_client.fire_event("my_event", my_arg="my_value") + + for trigger in triggers: + print(trigger.variables.my_arg) # This is the value of my_arg from the event fired above. + + # Another one, listening for time triggers. + future = ws_client.get_rendered_template( + "{{ (now() + timedelta(seconds=1)).strftime('%H:%M:%S') }}" + ) + with ws_client.listen_trigger("time", at=future) as triggers: # `at` can be HH:MM or HH:MM:SS + print(next(triggers)) + + Using :py:class:`AsyncClient` ********************************* All four client classes share the same method names. @@ -197,68 +264,6 @@ The :py:class:`AsyncWebsocketClient` works the same way: asyncio.run(main()) -Using Events (Listening and Firing) -***************************************** - -.. code-block:: python - - from homeassistant_api import WebsocketClient - - WS_URL = '' # Example: 'ws://homeassistant.local:8123/api/websocket' - TOKEN = '' - - with WebsocketClient(WS_URL, TOKEN) as ws_client: - with ws_client.listen_events() as events: - for event in events: - print(event) - - # Or if you want to listen for a specific event type until dinner time. - with ws_client.listen_events('state_changed') as events: - for event in events: - print(event) - if event.data.entity_id == 'myalarmclock.dinner_time' and event.data.new_state.state == 'now': - break - - # Or if you want to listen for just 10 events. - with ws_client.listen_events("my_event") as events: - for _, event in zip(range(10), events): - print(event) - - # Alternatively for just one event. - with ws_client.listen_events("my_event") as events: - event = next(events) - print(event) - - # Now to fire an event. - ws_client.fire_event("my_event", my_arg="my_value") - - -Listening for Triggers -************************** - -.. code-block:: python - - from homeassistant_api import WebsocketClient - - with WebsocketClient(WS_URL, TOKEN) as ws_client: - with ws_client.listen_trigger() as triggers: # see WebsocketClient.listen_trigger for more info. - for trigger in triggers: - print(trigger) - - # Another more specific example, listening for event triggers. - with ws_client.listen_trigger("event", event_type="my_event") as triggers: - ws_client.fire_event("my_event", my_arg="my_value") - - for trigger in triggers: - print(trigger.variables.my_arg) # This is the value of my_arg from the event fired above. - - # Another one, listening for time triggers. - future = ws_client.get_rendered_template( - "{{ (now() + timedelta(seconds=1)).strftime('%H:%M:%S') }}" - ) - with ws_client.listen_trigger("time", at=future) as triggers: # `at` can be HH:MM or HH:MM:SS - print(next(triggers)) - What's Next? ############# From 672fc783687ae362a36515e4af8fcc754d67106c Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 5 May 2026 23:00:45 -0500 Subject: [PATCH 09/17] Fix syntax issue --- docs/advanced.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/advanced.rst b/docs/advanced.rst index 53615df9..15b55df5 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -19,6 +19,7 @@ A convenient option is to use the :py:class:`niquests_cache.session.CachedSessio client = Client("", "", session=CachedSession(backend=MemoryBackend(), expire_after=300)) .. code-block:: python + from homeassistant_api import AsyncClient from niquests_cache.session import AsyncCachedSession From 8e917e33e8edc5725d476123139d3d6e7ffceae5 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 6 May 2026 11:21:28 -0500 Subject: [PATCH 10/17] Finish up --- docs/CONTRIBUTING.rst | 4 ++-- docs/api.rst | 2 ++ homeassistant_api/asyncwebsocket.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/CONTRIBUTING.rst b/docs/CONTRIBUTING.rst index 205169d4..ee42184c 100644 --- a/docs/CONTRIBUTING.rst +++ b/docs/CONTRIBUTING.rst @@ -79,7 +79,7 @@ To do so, you can run .. code-block:: bash - $ docker-compose up server + $ docker compose up server which spins up a container running Home Assistant with port 8123 exposed to your local machine. You'll need the following environment variables set to use the repository docker setup: @@ -137,7 +137,7 @@ In order to make sure that our code is easy to read, and navigate as well as to stop stupid mistakes like typos, undefined variables, etc, we enforce code standards. Using the tools, :code:`ruff`, :code:`zuban`, and :code:`pytest`, we make sure that our code quality is top notch and that changes work everywhere. -You can those tools manually yourself, but they also run automatically when you open a PR. +You can run those tools manually yourself, but they also run automatically when you open a Pull Request on :resource:`GitHub `. Merging Your Contributions diff --git a/docs/api.rst b/docs/api.rst index cc730601..b6ea81b8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -20,6 +20,8 @@ Clients .. autoclass:: homeassistant_api.WebsocketClient :members: +.. autoclass:: homeassistant_api.AsyncWebsocketClient + :members: .. autoclass:: homeassistant_api.BaseWebsocketClient :members: diff --git a/homeassistant_api/asyncwebsocket.py b/homeassistant_api/asyncwebsocket.py index 1d7a6d38..d82f0fe7 100644 --- a/homeassistant_api/asyncwebsocket.py +++ b/homeassistant_api/asyncwebsocket.py @@ -472,7 +472,7 @@ async def listen_events( """ Listen for all events of a certain type. - For example, to listen for all events of type `test_event`: + For example, to listen for all events of type :code:`test_event`: .. code-block:: python From 0ef0de146539660bbc7b27a26519c9e2c4e85aa8 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 6 May 2026 12:35:22 -0500 Subject: [PATCH 11/17] Clean --- docs/extensions/resourcelinks.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/extensions/resourcelinks.py b/docs/extensions/resourcelinks.py index 4c285f63..65206fbf 100644 --- a/docs/extensions/resourcelinks.py +++ b/docs/extensions/resourcelinks.py @@ -5,8 +5,11 @@ from typing import Any import sphinx -from docutils import nodes, utils -from docutils.nodes import Node, system_message + +from docutils import nodes +from docutils import utils +from docutils.nodes import Node +from docutils.nodes import system_message from docutils.parsers.rst.states import Inliner from sphinx.application import Sphinx from sphinx.util.nodes import split_explicit_title From 38d8eb830bb93fe3afbb210a6038e68c9936afd9 Mon Sep 17 00:00:00 2001 From: Adam Logan Date: Wed, 6 May 2026 22:15:39 -0700 Subject: [PATCH 12/17] Add non-trivial examples: websocket monitor, history stats, template bulk action --- examples/history_stats.py | 64 +++++++++++++++++++++++++++++ examples/template_bulk_action.py | 42 +++++++++++++++++++ examples/websocket_state_monitor.py | 47 +++++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 examples/history_stats.py create mode 100644 examples/template_bulk_action.py create mode 100644 examples/websocket_state_monitor.py diff --git a/examples/history_stats.py b/examples/history_stats.py new file mode 100644 index 00000000..fbd077b0 --- /dev/null +++ b/examples/history_stats.py @@ -0,0 +1,64 @@ +"""Fetch 24h history for numeric sensors and print min/max/avg per entity. + +Environment variables: + HOMEASSISTANT_API_ENDPOINT e.g. http://localhost:8123/api + HOMEASSISTANT_API_TOKEN Long-lived access token +""" + +import os +from datetime import datetime +from datetime import timedelta +from datetime import timezone + +from homeassistant_api import Client + +url = os.environ["HOMEASSISTANT_API_ENDPOINT"] +token = os.environ["HOMEASSISTANT_API_TOKEN"] + +SENSOR_IDS = [ + "sensor.living_room_temperature", + "sensor.outdoor_temperature", + "sensor.energy_consumption", +] + + +def _to_float(value: str) -> float | None: + try: + return float(value) + except (ValueError, TypeError): + return None + + +def main() -> None: + now = datetime.now(tz=timezone.utc) + yesterday = now - timedelta(hours=24) + + with Client(url, token) as client: + entities = [ + entity + for sensor_id in SENSOR_IDS + if (entity := client.get_entity(entity_id=sensor_id)) is not None + ] + + for history in client.get_entity_histories( + entities=tuple(entities), + start_timestamp=yesterday, + end_timestamp=now, + ): + values = [ + v for s in history.states if (v := _to_float(s.state)) is not None + ] + if not values: + print(f"{history.entity_id}: no numeric data in last 24h") # noqa: T201 + continue + print( # noqa: T201 + f"{history.entity_id}: " + f"min={min(values):.2f} " + f"max={max(values):.2f} " + f"avg={sum(values) / len(values):.2f} " + f"({len(values)} samples)", + ) + + +if __name__ == "__main__": + main() diff --git a/examples/template_bulk_action.py b/examples/template_bulk_action.py new file mode 100644 index 00000000..e9b925df --- /dev/null +++ b/examples/template_bulk_action.py @@ -0,0 +1,42 @@ +"""Use a Jinja2 template to find all lights that are on, then turn them off. + +Demonstrates combining get_rendered_template with trigger_service to perform +bulk operations based on server-side state queries. + +Environment variables: + HOMEASSISTANT_API_ENDPOINT e.g. http://localhost:8123/api + HOMEASSISTANT_API_TOKEN Long-lived access token +""" + +import os + +from homeassistant_api import Client + +url = os.environ["HOMEASSISTANT_API_ENDPOINT"] +token = os.environ["HOMEASSISTANT_API_TOKEN"] + +FIND_ON_LIGHTS = """\ +{{ states.light + | selectattr('state', 'eq', 'on') + | map(attribute='entity_id') + | list + | join(',') }}""" + + +def main() -> None: + with Client(url, token) as client: + rendered = client.get_rendered_template(FIND_ON_LIGHTS) + entity_ids = [e.strip() for e in rendered.split(",") if e.strip()] + + if not entity_ids: + print("No lights are currently on.") # noqa: T201 + return + + print(f"Turning off {len(entity_ids)} light(s)...") # noqa: T201 + for entity_id in entity_ids: + client.trigger_service("light", "turn_off", entity_id=entity_id) + print(f" off: {entity_id}") # noqa: T201 + + +if __name__ == "__main__": + main() diff --git a/examples/websocket_state_monitor.py b/examples/websocket_state_monitor.py new file mode 100644 index 00000000..fec28206 --- /dev/null +++ b/examples/websocket_state_monitor.py @@ -0,0 +1,47 @@ +"""Stream live state_changed events for entities in a given domain. + +Usage: + python websocket_state_monitor.py [domain] + + domain defaults to "light". Press Ctrl+C to stop. + +Environment variables: + HOMEASSISTANT_WS_ENDPOINT e.g. ws://localhost:8123/api/websocket + HOMEASSISTANT_API_TOKEN Long-lived access token +""" + +import os +import sys + +from homeassistant_api import WebsocketClient +from homeassistant_api.models.websocket import FiredEvent + +url = os.environ["HOMEASSISTANT_WS_ENDPOINT"] +token = os.environ["HOMEASSISTANT_API_TOKEN"] + + +def monitor_domain(domain: str) -> None: + with WebsocketClient(url, token) as ws: + print(f"Monitoring '{domain}' state changes. Press Ctrl+C to stop.") # noqa: T201 + with ws.listen_events("state_changed") as events: + for event in events: + if not isinstance(event, FiredEvent): + continue + entity_id: str = event.data.get("entity_id", "") + if not entity_id.startswith(f"{domain}."): + continue + old_state = (event.data.get("old_state") or {}).get( + "state", + "unavailable", + ) + new_state = (event.data.get("new_state") or {}).get( + "state", + "unavailable", + ) + timestamp = event.time_fired.strftime("%H:%M:%S") + print(f"[{timestamp}] {entity_id}: {old_state} → {new_state}") # noqa: T201 + + +if __name__ == "__main__": + domain = sys.argv[1] if len(sys.argv) > 1 else "light" + monitor_domain(domain) From 45b47b96a202839ebb6be285635a1fa8b5786111 Mon Sep 17 00:00:00 2001 From: Nate Date: Fri, 8 May 2026 16:17:44 -0500 Subject: [PATCH 13/17] Migrate to sphinx>=9 --- docs/_static/css/custom.css | 5 +++++ docs/conf.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 3892ef1a..552b2459 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -73,4 +73,9 @@ body { .version { color: #d9d9d9; +} + +table.docutils { + margin-bottom: 1.5em !important; + margin-left: 0 !important; } \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 72daac30..51d6d3c1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ "sphinx.ext.autodoc", "resourcelinks", "sphinx_autodoc_typehints", - "sphinxcontrib.autodoc_pydantic", + "sphinxcontrib.pydantic", "sphinx.ext.intersphinx", "sphinx.ext.autosectionlabel", ] diff --git a/pyproject.toml b/pyproject.toml index 23944a95..ac819750 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,11 +25,11 @@ Repository = "https://github.com/GrandMoff100/HomeAssistantAPI" [dependency-groups] docs = [ - "sphinx>=7,<9", "sphinx-autodoc-typehints>=3,<4", "sphinx-rtd-theme>=3,<4", - "autodoc-pydantic>=2,<3", "sphinx-autobuild>=2025.8.25", + "sphinxcontrib-pydantic>=0.3.0", + "sphinx>=9" ] dev = [ "types-docutils>=0.22", From b9008a44a12e4839a3563f7f148c60449bdbe816 Mon Sep 17 00:00:00 2001 From: Nate Date: Fri, 8 May 2026 16:26:22 -0500 Subject: [PATCH 14/17] Fix sphinx_autodoc_typehints not resolving JsonValue --- docs/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 51d6d3c1..f9bd2447 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -136,3 +136,6 @@ "niquests": ("https://niquests.readthedocs.io/en/stable", None), "niquests-cache": ("https://niquests-cache.readthedocs.io/en/stable/", None), } +autodoc_type_aliases = { + "JsonValue": "typing.Any", +} From cb905d6c2f9761676b1a8827245929ec3e7a71e5 Mon Sep 17 00:00:00 2001 From: Nate Date: Fri, 8 May 2026 18:19:11 -0500 Subject: [PATCH 15/17] Rename old repo urls --- .github/workflows/python-publish.yml | 2 -- .github/workflows/test-suite.yml | 3 +-- README.md | 10 +++++----- docs/conf.py | 4 ++-- docs/quickstart.rst | 4 ++-- homeassistant_api/errors.py | 2 +- pyproject.toml | 4 ++-- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index b147886a..b46d690e 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -9,7 +9,6 @@ on: jobs: build: runs-on: ubuntu-latest - environment: "Python Package Deployment" steps: - uses: actions/checkout@v3 - name: Set up Python @@ -31,7 +30,6 @@ jobs: publish: runs-on: ubuntu-latest - environment: "Python Package Deployment" permissions: id-token: write needs: diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 915ccb3b..54048476 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -6,8 +6,7 @@ on: - "pyproject.toml" pull_request: branches: - - master - - dev + - main paths: - "**.py" - "pyproject.toml" diff --git a/README.md b/README.md index 7d0da8e2..9098ee5a 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # HomeassistantAPI -[![Code Coverage](https://img.shields.io/codecov/c/github/GrandMoff100/HomeAssistantAPI/dev?style=for-the-badge&token=SJFC3HX5R1)](https://codecov.io/gh/GrandMoff100/HomeAssistantAPI) +[![Code Coverage](https://img.shields.io/codecov/c/github/HomeAssistant-API/HomeAssistantAPI/dev?style=for-the-badge&token=SJFC3HX5R1)](https://codecov.io/gh/HomeAssistant-API/HomeAssistantAPI) [![PyPI - Downloads](https://img.shields.io/pypi/dm/HomeAssistant-API?style=for-the-badge)](https://pypistats.org/packages/homeassistant-api) -![GitHub commits since latest release (by date including pre-releases)](https://img.shields.io/github/commits-since/GrandMoff100/HomeassistantAPI/latest/dev?include_prereleases&style=for-the-badge) +![GitHub commits since latest release (by date including pre-releases)](https://img.shields.io/github/commits-since/HomeAssistant-API/HomeAssistantAPI/latest/dev?include_prereleases&style=for-the-badge) [![Read the Docs (version)](https://img.shields.io/readthedocs/homeassistantapi?style=for-the-badge)](https://homeassistantapi.readthedocs.io/en/latest/?badge=latest) -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/GrandMoff100/HomeassistantAPI?style=for-the-badge)](https://github.com/GrandMoff100/HomeassistantAPI/releases) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/HomeAssistant-API/HomeAssistantAPI?style=for-the-badge)](https://github.com/HomeAssistant-API/HomeAssistantAPI/releases) - + ## Python wrapper for Homeassistant's [Websocket API](https://developers.home-assistant.io/docs/api/websocket/) and [REST API](https://developers.home-assistant.io/docs/api/rest/) @@ -65,7 +65,7 @@ you'd want to know is on our readthedocs site [here](https://homeassistantapi.re If there is something missing, open an issue and let us know! Thanks! Go make some cool stuff! Maybe come back and tell us about it in a -[discussion](https://github.com/GrandMoff100/HomeAssistantAPI/discussions)? +[discussion](https://github.com/HomeAssistant-API/HomeAssistantAPI/discussions)? We'd love to hear about how you use our library!! ## License diff --git a/docs/conf.py b/docs/conf.py index f9bd2447..32b29dc8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ project = "Homeassistant API" copyright = "2023-2026, Nathan Larsen" # pylint: disable=redefined-builtin author = "Nathan Larsen" -repo_url = "https://github.com/GrandMoff100/HomeassistantAPI" +repo_url = "https://github.com/HomeAssistant-API/HomeassistantAPI" # The full version, including alpha/beta/rc tags with open("../pyproject.toml") as f: @@ -38,7 +38,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -branch = "dev" if re.match(r".+\.(post|pre)\d+", version) else "v" + version +branch = "main" if re.match(r".+\.(post|pre)\d+", version) else "v" + version extensions = [ "sphinx.ext.autodoc", diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f982b7c1..53258609 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -58,7 +58,7 @@ This installs the latest stable version from :resource:`PyPI ` $ pip install homeassistant_api # To install the latest dev version - $ pip install git+https://github.com/GrandMoff100/HomeassistantAPI + $ pip install git+https://github.com/HomeAssistant-API/HomeAssistantAPI Installing with :code:`git` @@ -70,7 +70,7 @@ To install with :code:`git` we're going to clone the repository and then run :co .. code-block:: bash # Clone with git - git clone https://github.com/GrandMoff100/HomeassistantAPI + git clone https://github.com/HomeAssistant-API/HomeAssistantAPI # CD into your project cd HomeAssistantAPI diff --git a/homeassistant_api/errors.py b/homeassistant_api/errors.py index 4e84c5fd..520d84c7 100644 --- a/homeassistant_api/errors.py +++ b/homeassistant_api/errors.py @@ -68,7 +68,7 @@ def __init__(self, status_code: int, content: str | bytes) -> None: f"Home Assistant returned a response with an error status code {status_code!r}.\n" f"{content!r}\n" "If this happened, " - "please report it at https://github.com/GrandMoff100/HomeAssistantAPI/issues " + "please report it at https://github.com/HomeAssistant-API/HomeAssistantAPI/issues " "with the request status code and the request content. Thanks!", ) diff --git a/pyproject.toml b/pyproject.toml index ac819750..bb8b74bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,8 @@ dependencies = [ [project.urls] Documentation = "https://homeassistantapi.readthedocs.io" -Homepage = "https://github.com/GrandMoff100/HomeAssistantAPI" -Repository = "https://github.com/GrandMoff100/HomeAssistantAPI" +Homepage = "https://github.com/HomeAssistant-API/HomeAssistantAPI" +Repository = "https://github.com/HomeAssistant-API/HomeAssistantAPI" [dependency-groups] docs = [ From e4e9dc5d4c23dc2378eeafca2b4adace7087c8b2 Mon Sep 17 00:00:00 2001 From: Adam Logan Date: Sun, 10 May 2026 16:06:31 -0700 Subject: [PATCH 16/17] Review fixes: duplicate __all__, missing Path imports, broken RST link, grammar, EOF newlines --- docs/CONTRIBUTING.rst | 2 +- docs/advanced.rst | 6 +++--- docs/api.rst | 4 ++-- homeassistant_api/__init__.py | 1 - scripts/run_docs_dev.sh | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/CONTRIBUTING.rst b/docs/CONTRIBUTING.rst index ee42184c..5f63961f 100644 --- a/docs/CONTRIBUTING.rst +++ b/docs/CONTRIBUTING.rst @@ -65,7 +65,7 @@ Option B. Setup a Home Assistant Development Environment. If you do not have a Home Assistant installation running already, you can setup a Home Assistant Development environment. This is basically a local, unpackaged, Home Assistant Core installation, that runs with just Python (no Docker or Operating System). You can start and stop the server really easily as it runs just in your -terminal and gives you an large affordance of control over it, making it ideal for quickly testing your changes. +terminal and gives you control over it, making it ideal for quickly testing your changes. Follow this great guide `here `__ to do that. You'll access the web dashboard to create the Long-Lived Access Token. diff --git a/docs/advanced.rst b/docs/advanced.rst index 15b55df5..e38210f9 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -29,7 +29,7 @@ A convenient option is to use the :py:class:`niquests_cache.session.CachedSessio This creates an in-memory cache that expires after 300 seconds. You can adjust the `expire_after` value to fit your needs or set it to `-1` to disable expiration. -For more information on the available caching options, see the `niquests_cache ` documentation. +For more information on the available caching options, see the `niquests_cache `__ documentation. Persistent Caching @@ -39,7 +39,7 @@ If you want your cache to persist between runs (e.g. to a filesystem), you can p .. code-block:: python - from datetime import timedelta + from pathlib import Path from homeassistant_api import Client from niquests_cache.session import CachedSession @@ -57,7 +57,7 @@ If you want your cache to persist between runs (e.g. to a filesystem), you can p # Or an example for async import asyncio - from datetime import timedelta + from pathlib import Path from homeassistant_api import AsyncClient from niquests_cache.session import AsyncCachedSession diff --git a/docs/api.rst b/docs/api.rst index b6ea81b8..5946fa79 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -78,7 +78,7 @@ Websocket Models .. automodule:: homeassistant_api.models.websocket :members: - + Request Processing ================== @@ -90,4 +90,4 @@ Errors ======= .. automodule:: homeassistant_api.errors - :members: \ No newline at end of file + :members: diff --git a/homeassistant_api/__init__.py b/homeassistant_api/__init__.py index 8227e1c4..5818363b 100644 --- a/homeassistant_api/__init__.py +++ b/homeassistant_api/__init__.py @@ -53,7 +53,6 @@ "ServiceField", "State", "WebsocketClient", - "WebsocketClient", ) from .asyncclient import AsyncClient diff --git a/scripts/run_docs_dev.sh b/scripts/run_docs_dev.sh index f636068f..7847410e 100755 --- a/scripts/run_docs_dev.sh +++ b/scripts/run_docs_dev.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -sphinx-autobuild docs/ build/ --open-browser --port 8000 --watch examples/ --watch homeassistant_api/ \ No newline at end of file +sphinx-autobuild docs/ build/ --open-browser --port 8000 --watch examples/ --watch homeassistant_api/ From 397769531bb64d7b7f891d5ffbc9077dfd7165a3 Mon Sep 17 00:00:00 2001 From: Nate Date: Sun, 10 May 2026 20:33:32 -0500 Subject: [PATCH 17/17] Add back publishing environment --- .github/workflows/python-publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index b46d690e..b147886a 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -9,6 +9,7 @@ on: jobs: build: runs-on: ubuntu-latest + environment: "Python Package Deployment" steps: - uses: actions/checkout@v3 - name: Set up Python @@ -30,6 +31,7 @@ jobs: publish: runs-on: ubuntu-latest + environment: "Python Package Deployment" permissions: id-token: write needs: