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
-[](https://codecov.io/gh/GrandMoff100/HomeAssistantAPI)
+[](https://codecov.io/gh/HomeAssistant-API/HomeAssistantAPI)
[](https://pypistats.org/packages/homeassistant-api)
-
+
[](https://homeassistantapi.readthedocs.io/en/latest/?badge=latest)
-[](https://github.com/GrandMoff100/HomeassistantAPI/releases)
+[](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/CONTRIBUTING.rst b/docs/CONTRIBUTING.rst
index 85a15677..5f63961f 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**--- 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.
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
@@ -38,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 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
********
@@ -95,20 +133,20 @@ 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.
+You can run those tools manually yourself, but they also run automatically when you open a Pull Request on :resource:`GitHub `.
+
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/_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/advanced.rst b/docs/advanced.rst
index 3103adf5..e38210f9 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -5,37 +5,48 @@ 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:`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.
-
.. code-block:: python
- from datetime import timedelta
+ from pathlib import Path
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'), # by default uses sqlite backend
)
with client:
@@ -46,17 +57,15 @@ See the docs for `requests_cache ",
"",
- session=CachedSession(
- cache=FileBackend(
- expire_after=timedelta(minutes=5),
- ),
+ session=AsyncCachedSession(
+ cache_name=Path('.cache') / 'http',
),
)
@@ -68,7 +77,7 @@ See the docs for `requests_cache `.
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/docs/quickstart.rst b/docs/quickstart.rst
index c93b2273..53258609 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -5,8 +5,8 @@ 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.
If you don't want to do that you can setup a Home Assistant container on your laptop or desktop with docker.
@@ -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,29 +26,31 @@ 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 `__
Also if you are building a website and want to integrate Home Assistant you can use a refresh token instead.
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
@@ -56,18 +58,19 @@ Installation with pip is really easy and will install the dependencies this proj
$ 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`
----------------------------------
-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
# Clone with git
- git clone https://github.com/GrandMoff100/HomeassistantAPI
+ git clone https://github.com/HomeAssistant-API/HomeAssistantAPI
# CD into your project
cd HomeAssistantAPI
@@ -83,10 +86,10 @@ 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
-================
-Some examples applications of this project include integrating it into a another library, flask application or just a regular python script.
-Maybe you want to start a project that allows you to use your Home Assistant from your command line but some sassy responses.
+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.
In any event, the possibilities are endless, so go make some cool stuff and share it with us on the :resource:`repository `!
We hope to see your project soon!
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?
#############
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/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)
diff --git a/homeassistant_api/__init__.py b/homeassistant_api/__init__.py
index caeff0e3..5818363b 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",
@@ -55,6 +57,8 @@
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/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
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/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/homeassistant_api/models/domains.py b/homeassistant_api/models/domains.py
index 589ac488..3e2acc47 100644
--- a/homeassistant_api/models/domains.py
+++ b/homeassistant_api/models/domains.py
@@ -24,6 +24,15 @@
from .states import State
+__all__ = (
+ "AsyncDomain",
+ "AsyncService",
+ "BaseDomain",
+ "BaseService",
+ "Domain",
+ "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:
diff --git a/pyproject.toml b/pyproject.toml
index 6fd6de9c..bb8b74bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,14 +20,16 @@ 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 = [
"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",
diff --git a/scripts/run_docs_dev.sh b/scripts/run_docs_dev.sh
index 793eac6d..7847410e 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/
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