diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9d4dc5a3..e45157da 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,12 +13,12 @@ name: "CodeQL" on: push: - branches: [ dev ] + branches: [dev] pull_request: # The branches below must be a subset of the branches above - branches: [ dev ] + branches: [dev] schedule: - - cron: '17 7 * * 1' + - cron: "17 7 * * 1" jobs: analyze: @@ -32,39 +32,39 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python' ] + language: ["python"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - - name: Checkout repository - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v2 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 62353925..cda181f8 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -24,4 +24,7 @@ jobs: env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: poetry publish --build -username $TWINE_USERNAME --password TWINE_PASSWORD + run: | + poetry publish --build \ + --username $TWINE_USERNAME \ + --password TWINE_PASSWORD diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f74a64..f6c24d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,44 @@ # Changelog -**v2.0.0** -- Added Data Models -- Added Documentation -- Added functions for all endpoints +## v3.0.0 -**v2.1.0** -- Added Event support +- Added Rigorous CI/CD tools, i.e. `black`, `isort`, `mypy`, `pre-commit`, `pylint`, `flake8`. +- Renamed `AsyncClient` methods with `async_` convention. +- `Client` and `AsyncClient` can be initialized without confirming the API's status. +- `Client` and `AsyncClient` are now both context managers that function the exact same. +- Both clients now share previously redundant model conversion methods. +- Reversed CHANGELOG order (most recent first). -**v2.2.0** -- Implemented async support with `homeassistant_api._async.AsyncClient` +## v2.4.0.post2 -**v2.3.0** -- Bug fixes (see closed issues between releases) -- Added global request kwargs parameter to Client objects (see [docs](https://homeassistantapi.readthedocs.io/en/latest/api.html)) +- Fixed wrong check in malformed_id function + +## v2.4.0.post1 + +- Replaced `text/plain` with `application/octet-stream` in docs and processing module. +- Added message content to UnrecognizedStatusCodeError to help with user debugging + +## v2.4.0 -**v2.4.0** - Bug fixes (see closed issues between releases) - Added a processing framework for hooking into mimetype processing - Fixed some issues with some ``AsyncClient`` methods -**v2.4.0.post1** -- Replaced `text/plain` with `application/octet-stream` in docs and processing module. -- Added message content to UnrecognizedStatusCodeError to help with user debugging +## v2.3.0 -**v2.4.0.post2** -- Fixed wrong check in malformed_id function +- Bug fixes (see closed issues between releases) +- Added global request kwargs parameter to Client objects (see [docs](homeassistantapi.rtfd.io/en/latest/api.html#homeassistant_api.Client)) + +## v2.2.0 + +- Implemented async support with `homeassistant_api._async.AsyncClient` + +## v2.1.0 + +- Added Event support + +## v2.0.0 + +- Added Data Models +- Added Documentation +- Added functions for all endpoints diff --git a/README.md b/README.md index 73b579fc..028c1c90 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![GitHub release (latest by date)](https://img.shields.io/github/v/release/GrandMoff100/HomeassistantAPI?style=for-the-badge) ![GitHub release (latest by date)](https://img.shields.io/github/downloads/GrandMoff100/HomeassistantAPI/latest/total?style=for-the-badge) -![Homeassistant Logo](https://github.com/GrandMoff100/HomeAssistantAPI/blob/7edb4e6298d37bda19c08b807613c6d351788491/docs/images/homeassistant-logo.png?raw=true) +![Home AssistantLogo](https://github.com/GrandMoff100/HomeAssistantAPI/blob/7edb4e6298d37bda19c08b807613c6d351788491/docs/images/homeassistant-logo.png?raw=true) Python Wrapper for Homeassistant's [REST API](https://developers.home-assistant.io/docs/api/rest/) diff --git a/docs/processing.rst b/docs/advanced.rst similarity index 60% rename from docs/processing.rst rename to docs/advanced.rst index dc0dfdc2..2570f18b 100644 --- a/docs/processing.rst +++ b/docs/advanced.rst @@ -1,6 +1,14 @@ +******************* +Advanced Section +******************* + +Persistent Caching +******************** + + Response Processing ********************** -Homeassistant API uses functions called processors. +Home Assistant API uses functions called processors. These functions take a Response object as a parameter and return the python data type associated with the content-type header. How To Register Response Processors (Converters) @@ -15,16 +23,16 @@ To register a response processor you need to import the Processing class and the from homeassistant_api.processing import process_json - @Processing.processor("application/octet-stream", override=True) + @Processing.processor("application/octet-stream") def text_processor(response): return response.text.lower() - @Processing.async_processor("text/csv") + @Processing.processor("text/csv") async def async_text_processor(response): text = await response.text() return [line.split(",") for line in text.splitlines()] - @Processing.processor("application/json", override=True) + @Processing.processor("application/json") def json_processor(response): print("I processed a json response!) return process_json(response) @@ -34,14 +42,15 @@ To register a response processor you need to import the Processing class and the print(client.get_entities()) -In this example. +In this example. The first processor (a function wrapped with the processor decorator) is going to be called when we receive a response that has that as its :code:`Content-Type` header. -Because :code:`homeassistant_api` provides processors for :code:`application/octet-stream` and :code:`application/json` by default, -we need to tell :code:`homeassistant_api` to override the default processor with :code:`override=True`. +:code:`homeassistant_api` provides processors for :code:`application/octet-stream` and :code:`application/json` by default, +But :code:`@Processing.processor` gives the most recently registered processor the highest precedence when choosing a processor for a response. +So our processor here will be chosen over the default processors. -The second processor is an async processor that only gets called when AsyncClient receives a response that has :code:`text/csv` as its :code:`Content-Type` header. -If you wanted to override :code:`homeassistant_api`'s default json processing using the :code:`json` module with a different way to process json data. -Such as using instead, the :code:`ujson` module (which is faster but more limiting). +The second processor is an async processor that only gets called when Client receives an async response that has :code:`text/csv` as its :code:`Content-Type` header. +If you wanted, you could not use :code:`homeassistant_api`'s default json processing using the :code:`json` module, +and use instead the :code:`ujson` module (which is faster but more restrictive). The third processor function implements the default processor function for the :code:`application/json` mimetype after printing a string. If you wanted to run some intermediate processing. diff --git a/docs/api.rst b/docs/api.rst index 4c503cdb..8283b6c0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,17 +1,10 @@ Code Reference *************** -.. currentmodule:: homeassistant_api - - Clients -------- -.. autoclass:: Client - :members: - :inherited-members: - -.. autoclass:: homeassistant_api._async.AsyncClient +.. autoclass:: homeassistant_api.Client :members: :inherited-members: @@ -19,67 +12,48 @@ Clients Data Models ------------ -.. autoclass:: Domain - :members: +.. automodule:: homeassistant_api.models -.. autoclass:: Service - :members: + .. autoclass:: Domain -.. autoclass:: Group - :members: + .. autoclass:: Service -.. autoclass:: Entity - :members: + .. autoclass:: Group -.. autoclass:: State - :members: + .. autoclass:: Entity -.. autoclass:: Event - :members: + .. autoclass:: History + .. autoclass:: State -.. autoclass:: homeassistant_api._async.AsyncDomain - :members: + .. autoclass:: Event -.. autoclass:: homeassistant_api._async.AsyncService - :members: -.. autoclass:: homeassistant_api._async.AsyncGroup - :members: -.. autoclass:: homeassistant_api._async.AsyncEntity - :members: +.. automodule:: homeassistant_api._async.models -.. autoclass:: homeassistant_api._async.AsyncEvent - :members: + .. autoclass:: AsyncDomain + .. autoclass:: AsyncService -Processing ------------ + .. autoclass:: AsyncGroup + .. autoclass:: AsyncEntity -.. autoclass:: Processing - :members: + .. autoclass:: AsyncEvent -Exceptions +Processing ----------- -.. autoclass:: homeassistant_api.errors.RequestError - -.. autoclass:: homeassistant_api.errors.MalformedDataError -.. autoclass:: homeassistant_api.errors.MalformedInputError - -.. autoclass:: homeassistant_api.errors.APIConfigurationError - -.. autoclass:: homeassistant_api.errors.ParameterMissingError - -.. autoclass:: homeassistant_api.errors.UnexpectedStatusCodeError +.. autoclass:: homeassistant_api.processing.Processing + :members: -.. autoclass:: homeassistant_api.errors.UnauthorizedError -.. autoclass:: homeassistant_api.errors.EndpointNotFoundError +Exceptions +----------- -.. autoclass:: homeassistant_api.errors.MethodNotAllowedError +.. automodule:: homeassistant_api.errors + :members: diff --git a/docs/conf.py b/docs/conf.py index edfff7d3..6d24d6f4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,6 +50,7 @@ "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", } # Add any paths that contain templates here, relative to this directory. diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 00000000..1d9af6db --- /dev/null +++ b/docs/development.rst @@ -0,0 +1,82 @@ +.. _development_page: + +***************** +Development +***************** + +This page is where development related things are. +See below. + +Contribution Ideas +********************* + +If you don't know what you want to contribute yet you should take a look at the TODO.md in the :resource:`repository `. +We're always interested in integrating ways to make the library faster, extensible and easier to use. + +Setting up your Development Environment +***************************************** + +So now that you know what you want to contribute it is time to setup a development environment to make your changes in. + +Step One: Fork the Repository +=============================== + +Click `here `_ to fork the repository. Then click your username. + +Step Two: Clone the Repository Locally +======================================= + +Next run in your terminal. + +.. code-block:: bash + + $ git clone https://github.com//HomeAssistantAPI + +Step Three: Installing Dependencies +====================================== + +Firstly, you need to have Python 3.7 or newer with Pip installed. Download the latest Python Version from `here `_. +Then you need to install the very popular Python Package Manager, :code:`poetry`. +Checkout the `Poetry docs `_. +You can install that with :code:`pip` by running :code:`pip install poetry`. +Now you can install the project's dependencies by running :code:`cd HomeAssistantAPI && poetry install` + +Step Four: [Optional] Setting Up 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). +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. +Follow this great guide `here `_ to do that. +After that you are now ready to make your changes to the codebase! + +Testing +******** +In order to test your changes you need to have an API URL, and a Long Lived Access Token. +Follow the :ref:`Quickstart Section ` for getting those. +If you setup the Development Environment then your API URL will most likely be something along the lines of :code:`https://localhost:8123/api`. +Then you can test your changes by passing the API URL, and Long Lived Access Token to the :code:`Client` object. + +.. _styling: + +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. +Using the tools, :code:`flake8`, :code:`pylint`, :code:`mypy`, :code:`black`, :code:`isort`, we make make sure that our code quality is top notch. +You can those tools manually your self, or you can run them all at once on your changes by running :code:`pre-commit run`. +Your code will also be checked again when you open a PR on the original repository. + +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. +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 ;)). +Then after that your changes will get merged and will be available in the next release! + diff --git a/docs/faq.rst b/docs/faq.rst deleted file mode 100644 index 2e20afa6..00000000 --- a/docs/faq.rst +++ /dev/null @@ -1,7 +0,0 @@ -FAQs -***** - -We want to add some Frequently Asked Questions but frankly questions haven't been asked frequently enough. -Until then this page will be left empty. :( - -If you have questions open a discussion or issue on the :resource:`repository `! :) diff --git a/docs/index.rst b/docs/index.rst index df0cfb55..629c678f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,8 @@ Welcome to Homeassistant API! ============================= -Homeassistant API is a pythonic module that interacts with `Homeassistant's REST API integration `_ +Homeassistant API is a pythonic module that interacts with `Homeassistant's REST API integration `_. +You can use it to remotely control your Home Assistant like getting entity states, triggering services, etc. Index ---------- @@ -20,23 +21,22 @@ Index Home quickstart usage - faq - credits + development Features ---------- -- Full consumption of the Homeassistant REST API endpoints -- Convenient classes that represent data from the API -- Asynchronous support for integration in async applications or libraries -- Modular design for intuitive readability +- Full consumption of the Home Assistant REST API endpoints. +- Convenient Pydantic Models for data validation. +- Syncrononous and Asynchronous support for integrating with all applications and/or libraries. +- Modular design for intuitive readability. +- Request caching for more efficient repeative requests. Getting Started ------------------- -Is this your first time using the library? This is the place to get started! - +Is this your first time using the library? Start with our :ref:`Quickstart Section ` Example --------- @@ -44,20 +44,30 @@ Example .. literalinclude:: ../examples/basic.py :language: python -Many more examples are available in the :resource:`repository `. Feel feel to open a pull request and add your own! See Contributing Guidelines +Want to look at more? +Many more examples are available in the :resource:`repository `. +We encourage you to open a pull request and add your own after you get to know the library! +See the :ref:`Contributing Section `. Code Reference --------------- -View the documentation for each class and function :doc:`here `. +View the documentation for each class and method :doc:`here `. + +.. _contributing_section: -Contributing -------------- +Contributing Guidelines +-------------------------- -We very warmly welcome contributions. -If you have an idea or some code you want to add to the project please fork :resource:`the repository `, make your changes, and open a pull request. -Most likely your changes will get merged if your code passes flake8 without any errors, and adds some functionality to the project. -We'd love to incorporate your unique ideas and perspective! +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 ` https://github.com/FoxNerdSaysMoo` for contributions to some of the awesome theme styling on these docs! +.. + We would love to give a special shoutout to `FoxNerdSaysMoo ` for contributions to some of the awesome theme styling on these docs! diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 01405e9d..4decb10a 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,3 +1,5 @@ +.. _quickstart: + *********** Quickstart *********** @@ -7,28 +9,31 @@ Prerequisites Homeassistant --------------- -Before using this library, you need to have Homeassistant OS 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 Homeassistant container on your laptop or desktop with docker. -See `here `__ for how to install the installation right for you. - - +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. +See `here `_ for how to install the installation right for you. -Configuring the API in Homeassistant -====================================== +Configuring the REST API Server in Homeassistant +======================================================= Enable the :code:`api` integration in Homeassistant ------------------------------------------------------ -This library requires that you enable the :code:`api` integration on your Homeassistant if you are familiar with setting up integrations. -The :code:`api` integration is also enabled when you enable the :code:`default_config` integration. +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. +But if by chance you have disabled :code:`default_config` you need to enable :code:`api`. +Which requires the :code:`http` integration as well. +(Again most likely already enabled on most installations of Home Assistant.) +If you are not sure if it is enabled or not, chances are if your frontend is enabled, so is your API Server. +.. _access_token_setup: 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 input to your script later. -A good guide on how to do that is `here `__ +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 `_ -Exposing Homeassistant to the Web +Exposing Home Assistant to the Web -------------------------------------- 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`. @@ -45,31 +50,31 @@ Installation with pip is really easy and will install the dependencies this proj .. code-block:: bash - # To install the latest stable version from Pypi + # To install the latest stable version from PyPI $ pip install homeassistant_api - # To install the latest dev version - $ pip install git+https://github.com/GrandMoff100/HomeassistantAPI/tree/dev - - # Example of installing a pre-release - $ pip install homeassistant_api==2.0.0a1 + # To install the latest dev version (you'll need to use poetry because pip, by itself, does not understand poetry dependencies.) + $ poetry add git+https://github.com/GrandMoff100/HomeassistantAPI Installing with :code:`git` ---------------------------------- -To install with git we're going to clone the repository and then run setup.py like so. +To install with git we're going to clone the repository and then run :code:`$ poetry install` like so. .. code-block:: bash # Clone with git git clone https://github.com/GrandMoff100/HomeassistantAPI - - # Switch current working directory to the repository - cd HomeassistantAPI - # Run setup.py - python setup.py install + # CD into your project + cd + + # Install poetry + python -m pip install poetry + + # Run poetry install + python -m poetry install ~/HomeAssistantAPI Then you should be all set to start using the project! If run into any problems open an issue on our github :resource:`issue tracker ` @@ -77,7 +82,8 @@ Then you should be all set to start using the project! If run into any problems Example Usages ================ -Some examples applications of this project include integrating it into a console executable, flask application or just a regular python script. -You can start a project that allows you to use this from the command line. -Or add it to a discord bot to manage your homeassistant from inside discord (you might want to use AsyncClient if you do) -In any event, the possibilities are endless, so go make some cool stuff and share it with us! +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. +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 582c26e2..ec4f6031 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -4,12 +4,13 @@ Usage The Basics... -************** +################# This library is centered around the :code:`Client` class. -Once you have have your api base url and Long Lived Access Token from homeassistant we can start to do stuff. -The rest of this guide assumes you have the :code:`Client` (or :code:`AsyncClient`) saved to a :code:`client` variable like this. -Most of these examples require some integrations to be setup inside homeassistant for the examples to actually work. +Once you have have your api base url and Long Lived Access Token from Home Assistant we can start to do stuff. +The rest of this guide assumes you have the :code:`Client` saved to a :code:`client` variable. +Most of these examples require some integrations to be setup inside Home Assistant for the examples to actually work. +The most commonly used features of this library include triggering services and getting and modifying entity states. .. code-block:: python @@ -21,104 +22,122 @@ Most of these examples require some integrations to be setup inside homeassistan URL = '' TOKEN = '' + # Assigns the Client object to a variable and checks if it's running. client = Client(URL, TOKEN) + service = client.get_domain("light") # Gets the light service domain from Home Assistant + + service.turn_on(entity_id="light.my_living_room_light") + # Triggers the light.turn_on service on the entity `light.my_living_room_light` -Client -======== -The most commonly used features of this library include triggering services and getting and modifying entity states. -Services ---------- .. code-block:: python + :linenos: + + from datetime import datetime + from homeassistant_api import Client - domains = client.get_domains() - # {'homeassistant': , 'notify': } + # You can also initialize Client before you use it. + + client = Client("https://myhomeassistant.duckdns.org:8123/api", "mylongtokenpasswordthingyfoobar") + + # If you want to use caching you can use client as a context manager like so + with client: + while True: + sun = client.get_entity(entity_id="sun.sun") + state = sun.get_state() # Because requests are cached we reduce bandwidth usage :D + # Cache expires every 30 seconds by default. + if state.state == "below_horizon": + difference = datetime.now() - state.last_updated + print("Sun set", difference.seconds, "seconds ago.") + break + +Services +********** + +.. code-block:: python - service = domains.cover.services.close_cover # Works the same as domains['cover'].services['open_cover'] - # + light = client.get_domain("light") - changed_states = client.trigger_service('cover', 'close_cover', entity_id='cover.garage_door') - # Alternatively (using fetched service from above) - changed_states = service.trigger(entity_id='cover.garage_door') - # [] + print(light.services) + # {'turn_on': Service(service_id='turn_on', name='Turn on', description='Turn on one or more lights and adjust properties of the light, even when they are turned on already.\n', ... + changed_states = light.toggle(entity_id="light.light_bulb_1") Entities ---------- +************* .. code-block:: python entity_groups = client.get_entities() - # {'person': , 'zone': , ...} + # {'person': , 'zone': , ...} door = client.get_entity(entity_id='cover.garage_door') # "> states = client.get_states() - # [, ,...] + # [, ,...] state = client.get_state('sun.sun') - # + # new_state = client.set_state(state='my ToaTallY Whatever vAlUe 12t87932', group_id='my_favorite_colors', entity_slug='number_one') - # + # # Alternatively you can set state from the entity class itself from homeassistant_api import State # If you are wondering where door came from its about 15 lines up. - door.state.state = 'My new state' - door.state.attributes['open_height'] = '23' - door.set_state(door.state) - # - + door.set_state(State(state="My new state", attributes={"open_height": "5ft"})) + # -AsyncClient -============= -Before you run this code you need to install the :code:`homeassistant_api[async]` (it just installs :code:`aiohttp`). -Here is the async counterpart to the usage above. -Except how to run async code in the console without starting an eventloop yourself you ask? You can install :code:`aioconsole` and then run :code:`$ apython` +Using Client with :code:`async`/:code:`await` +************************************************* +Are you wondering if you can use :code:`homeassistant_api` using Python's :code:`async`/:code:`await` syntax? +Good news! You can! Services ------------- +************ .. code-block:: python - from homeassistant_api._async import AsyncClient + import asyncio + from homeassistant_api import Client + + # Initialize client like usual + client = Client(url, token) - client = AsyncClient(url, token) + async def main(): - domains = await client.get_domains() - # {'homeassistant': , 'notify': } + domains = await client.async_get_domains() + print(domains) + # {'homeassistant': , 'notify': } - service = domains.cover.services.close_cover # Works the same as domains['cover'].services['open_cover'] - # + cover = await client.async_get_domain("cover") - changed_states = client.trigger_service('cover', 'close_cover', entity_id='cover.garage_door') - # Alternatively (using fetched service from above) - changed_states = service.trigger(entity_id='cover.garage_door') - # [] + changed_states = await cover.close_cover(entity_id='cover.garage_door') + # [] + asyncio.get_event_loop().run_until_complete(main()) Entities ------------ +********* .. code-block:: python - entity_groups = await client.get_entities() + entity_groups = await client.async_get_entities() # {'person': , 'zone': , ...} - door = await client.get_entity(entity_id='cover.garage_door') + door = await client.async_get_entity(entity_id='cover.garage_door') # "> - states = await client.get_states() + states = await client.async_get_states() # [, ,...] - state = await client.get_state('sun.sun') + state = await client.async_get_state('sun.sun') # - new_state = await client.set_state(state='my ToaTallY Whatever vAlUe 12t87932', group_id='my_favorite_colors', entity_slug='number_one') + new_state = await client.async_set_state(state='my ToaTallY Whatever vAlUe 12t87932', group_id='my_favorite_colors', entity_slug='number_one') # # Alternatively you can set state from the entity class itself @@ -127,12 +146,12 @@ Entities # If you are wondering where door came from its about 15 lines up. door.state.state = 'My new state' door.state.attributes['open_height'] = '23' - await door.set_state(door.state) + await door.async_set_state(door.state) # What's Next? -************* +############# Browse below to learn more about what you can do with :code:`homeassistant_api`. @@ -140,5 +159,5 @@ Browse below to learn more about what you can do with :code:`homeassistant_api`. .. toctree:: :maxdepth: 2 - processing - api \ No newline at end of file + api + advanced diff --git a/examples/async_get_entities.py b/examples/async_get_entities.py index 86071b43..c9bf0b89 100644 --- a/examples/async_get_entities.py +++ b/examples/async_get_entities.py @@ -10,7 +10,7 @@ async def main(): # Initialize main object client = Client(url, token) - # Uses async context manager to ping the server. + # Uses async context manager to ping the server and initialize caching. async with client: # All async methods are prefixed with `async_`. data = await client.async_get_entities() diff --git a/examples/basic.py b/examples/basic.py index 652cd5f4..9a985bef 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -1,12 +1,20 @@ +import os + from homeassistant_api import Client -api_url = "" # Something like http://localhost:8123/api -token = "" # Used to aunthenticate yourself with homeassistant +api_url = "https://larsen-hassio.duckdns.org:8123/api" # Something like http://localhost:8123/api +token = os.getenv( + "HOMEASSISTANT_TOKEN" +) # Used to aunthenticate yourself with homeassistant # See the documentation on how to obtain a Long Lived Access Token -with Client(api_url, token) as client: # Initializes main object and pings the server. - # Gets all services under the light domain. - light = client.get_domain("light") +assert token is not None + +with Client( + api_url, + token, +) as client: # Create Client object and check that its running. + cover = client.get_domain("cover") - # Tells homeassistant to trigger the turn_on service on the given entity_id - light.turn_on.trigger(entity_id="light.front_room") + # Tells Home Assistant to trigger the toggle service on the given entity_id + cover.toggle(entity_id="cover.garage_door") diff --git a/examples/myq_grarage_door.py b/examples/myq_grarage_door.py index 584a72e8..8ae9f0ec 100644 --- a/examples/myq_grarage_door.py +++ b/examples/myq_grarage_door.py @@ -5,13 +5,14 @@ api_url = os.getenv("API_URL") token = os.getenv("TOKEN") + if api_url is not None and token is not None: # Intitializes the main Client client = Client(api_url, token) # Verifies the extistence of the specified server and opens efficient ClientSessions. with client: # Gets the cover service domain - cover = client.get_domain("cover") - + light = client.get_domain("light") + assert light is not None # Triggers the service with a specific garage door - cover.open_garage(entity_id="cover.my_garage_door") + print(light.toggle(entity_id="light.light_bulb_1")) diff --git a/homeassistant_api/__init__.py b/homeassistant_api/__init__.py index c0b7780b..25733fc6 100644 --- a/homeassistant_api/__init__.py +++ b/homeassistant_api/__init__.py @@ -1,4 +1,5 @@ """Imports all library stuff for convenience.""" +from ._async import AsyncDomain, AsyncEntity, AsyncEvent, AsyncGroup, AsyncService from .client import Client from .errors import ( APIConfigurationError, @@ -14,3 +15,11 @@ ) from .models import Domain, Entity, Event, Group, History, Service, State from .processing import Processing + +Domain.update_forward_refs(**locals()) +Entity.update_forward_refs(**locals()) +Event.update_forward_refs(**locals()) +Group.update_forward_refs(**locals()) +History.update_forward_refs(**locals()) +Service.update_forward_refs(**locals()) +State.update_forward_refs(**locals()) diff --git a/homeassistant_api/_async/asyncclient.py b/homeassistant_api/_async/asyncclient.py index 0b7c0971..529d562e 100644 --- a/homeassistant_api/_async/asyncclient.py +++ b/homeassistant_api/_async/asyncclient.py @@ -1,9 +1,10 @@ -"""Module for interacting with homeassistant asyncronously.""" +"""Module for interacting with Home Assistant asyncronously.""" import asyncio from datetime import datetime from os.path import join from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union, cast +import aiohttp from aiohttp_client_cache import CachedSession from ..const import DATE_FMT @@ -24,16 +25,16 @@ class RawAsyncClient(RawWrapper, JsonProcessingMixin): :param global_request_kwargs: A dictionary or dict-like object of kwargs to pass to :func:`requests.request` or :meth:`aiohttp.ClientSession.request`. Optional. """ # pylint: disable=line-too-long - _session: Optional[CachedSession] = None + _async_session: Optional[CachedSession] = None async def __aenter__(self): - self._session = CachedSession(expire_after=30) - await self._session.__aenter__() + self._async_session = CachedSession(expire_after=30) + await self._async_session.__aenter__() await self.async_check_api_running() return self async def __aexit__(self, cls, obj, traceback): - await self._session.__aexit__(cls, obj, traceback) + await self._async_session.__aexit__(cls, obj, traceback) # Very important request function async def async_request( @@ -47,18 +48,26 @@ async def async_request( try: if self.global_request_kwargs is not None: kwargs.update(self.global_request_kwargs) - assert self._session is not None - resp = await self._session.request( + if self._async_session is not None: + return await self.async_response_logic( + await self._async_session.request( + method, + self.endpoint(path), + headers=self.prepare_headers(headers), + **kwargs, + ) + ) + async with aiohttp.request( method, self.endpoint(path), headers=self.prepare_headers(headers), **kwargs, - ) + ) as resp: + return await self.async_response_logic(resp) except asyncio.exceptions.TimeoutError as err: raise RequestError( - f'Homeassistant did not respond in time (timeout: {kwargs.get("timeout", 300)} sec)' + f'Home Assistant did not respond in time (timeout: {kwargs.get("timeout", 300)} sec)' ) from err - return await self.async_response_logic(resp) @staticmethod async def async_response_logic(response): @@ -68,7 +77,7 @@ async def async_response_logic(response): # API information methods async def async_api_error_log(self) -> str: """Returns the server error log as a string""" - return cast(str, await self.async_request("error_log", return_text=True)) + return cast(str, await self.async_request("error_log")) async def async_api_config(self) -> Dict[str, Any]: """Returns the yaml configuration of homeassistant""" @@ -116,7 +125,7 @@ async def async_get_entity_histories( yield History.parse_obj({"states": states}) async def async_get_rendered_template(self, template: str): - """Renders a given Jinja2 template string with homeassistant context data.""" + """Renders a given Jinja2 template string with Home Assistant context data.""" return await self.async_request( "template", json=dict(template=template), @@ -130,7 +139,7 @@ async def async_get_discovery_info(self) -> Dict[str, Any]: # API check methods async def async_check_api_config(self) -> bool: - """Asks homeassistant to validate its configuration file""" + """Asks Home Assistant to validate its configuration file""" res = await self.async_request("config/core/check_config", method="POST") res = cast(Dict[Any, Any], res) valid = {"valid": True, "invalid": False}.get( @@ -145,7 +154,7 @@ async def async_check_api_config(self) -> bool: return valid async def async_check_api_running(self) -> bool: - """Asks homeassistant if its running""" + """Asks Home Assistant if its running""" res = cast(Dict[Any, Any], await self.async_request("")) if res.get("message", None) == "API running.": return True @@ -202,7 +211,7 @@ async def async_trigger_service( service: str, **service_data, ) -> List[State]: - """Tells homeassistant to trigger a service, returns stats changed while being called""" + """Tells Home Assistant to trigger a service, returns stats changed while being called""" data = await self.async_request( join("services", domain, service), method="POST", diff --git a/homeassistant_api/_async/models/__init__.py b/homeassistant_api/_async/models/__init__.py index 0a28ffb2..e15e7ef4 100644 --- a/homeassistant_api/_async/models/__init__.py +++ b/homeassistant_api/_async/models/__init__.py @@ -1,4 +1,4 @@ -"""Imports async Models for convenience.""" +"""The async Models for the entire library.""" from .domains import AsyncDomain, AsyncService from .entity import AsyncEntity, AsyncGroup from .events import AsyncEvent diff --git a/homeassistant_api/_async/models/entity.py b/homeassistant_api/_async/models/entity.py index c03e48f2..f5db913d 100644 --- a/homeassistant_api/_async/models/entity.py +++ b/homeassistant_api/_async/models/entity.py @@ -2,9 +2,7 @@ from os.path import join from typing import TYPE_CHECKING, Any, Dict, Optional, cast -from pydantic import BaseModel - -from ...models import History, State +from ...models import BaseModel, History, State if TYPE_CHECKING: from homeassistant_api import Client @@ -36,11 +34,7 @@ class AsyncEntity(BaseModel): group: AsyncGroup async def async_get_state(self) -> State: - """Returns the state last fetched from the api.""" - return self.state - - async def async_fetch_state(self) -> State: - """Asks homeassistant for the state of the entity and sets it locally""" + """Asks Home Assistant for the state of the entity and sets it locally""" state_data = await self.group.client.async_request( join("states", self.entity_id) ) @@ -50,7 +44,7 @@ async def async_fetch_state(self) -> State: return self.state async def async_set_state(self, state: State) -> State: - """Tells homeassistant to set the given State object.""" + """Tells Home Assistant to set the given State object.""" return await self.group.client.async_set_state( self.entity_id, group=self.group.group_id, diff --git a/homeassistant_api/_async/models/events.py b/homeassistant_api/_async/models/events.py index 9656b424..81e3aeed 100644 --- a/homeassistant_api/_async/models/events.py +++ b/homeassistant_api/_async/models/events.py @@ -1,7 +1,7 @@ """Event Model File""" from typing import TYPE_CHECKING, Dict, cast -from pydantic import BaseModel +from ...models import BaseModel if TYPE_CHECKING: from homeassistant_api import Client @@ -9,7 +9,7 @@ class AsyncEvent(BaseModel): """ - Event class for Homeassistant Event Triggers + Event class for Home Assistant Event Triggers For attribute information see the Data Science docs on Event models. https://data.home-assistant.io/docs/events diff --git a/homeassistant_api/client.py b/homeassistant_api/client.py index db9159ec..b40d8ef0 100644 --- a/homeassistant_api/client.py +++ b/homeassistant_api/client.py @@ -3,5 +3,5 @@ from .rawclient import RawClient -class Client(RawClient, RawAsyncClient): # type: ignore[misc] +class Client(RawClient, RawAsyncClient): """The all-in-one class to interact with Home Assistant!""" diff --git a/homeassistant_api/errors.py b/homeassistant_api/errors.py index 5f27b262..3682ffc3 100644 --- a/homeassistant_api/errors.py +++ b/homeassistant_api/errors.py @@ -29,11 +29,11 @@ class ParameterMissingError(HomeassistantAPIError): class UnexpectedStatusCodeError(HomeassistantAPIError): - """Error raised when Homeassistant returns a response with status code that was unexpected.""" + """Error raised when Home Assistant returns a response with status code that was unexpected.""" def __init__(self, code: int, content): super().__init__( - f"Homeassistant return response with an unrecognized status code {code!r}.\n{content}" + f"Home Assistant return response with an unrecognized status code {code!r}.\n{content}" ) diff --git a/homeassistant_api/models/__init__.py b/homeassistant_api/models/__init__.py index f1164072..6d2a7934 100644 --- a/homeassistant_api/models/__init__.py +++ b/homeassistant_api/models/__init__.py @@ -1,4 +1,5 @@ -"""Imports Model objects for convenience.""" +"""The Model objects for the entire library.""" +from .base import BaseModel from .domains import Domain, Service from .entity import Entity, Group from .events import Event diff --git a/homeassistant_api/models/base.py b/homeassistant_api/models/base.py new file mode 100644 index 00000000..bca15cbd --- /dev/null +++ b/homeassistant_api/models/base.py @@ -0,0 +1,14 @@ +"""Module for Global Base Model Configuration inheritance.""" + +from pydantic import BaseModel as PydanticBaseModel + + +class BaseModel(PydanticBaseModel): + """Base model that all Library Models inherit from.""" + + class Config: + """Pydantic config class for all library models.""" + + arbitrary_types_allowed = True + smart_union = True + validate_assignment = True diff --git a/homeassistant_api/models/domains.py b/homeassistant_api/models/domains.py index c280bff8..ef64b120 100644 --- a/homeassistant_api/models/domains.py +++ b/homeassistant_api/models/domains.py @@ -1,9 +1,7 @@ """File for Service and Domain data models""" +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple -from typing import TYPE_CHECKING, Dict, Optional, Tuple - -from pydantic import BaseModel - +from .base import BaseModel from .states import State if TYPE_CHECKING: @@ -20,7 +18,13 @@ class Domain(BaseModel): def add_service(self, service_id: str, **data) -> None: """Registers services into a domain to be used or accessed""" self.services.update( - {service_id: Service(service_id=service_id, domain=self, **data)} + { + service_id: Service( + service_id=service_id, + domain=self, + **data, + ) + } ) def get_service(self, service_id: str): @@ -36,15 +40,25 @@ def __getattr__(self, attr: str): return super().__getattribute__(attr) +class ServiceField(BaseModel): + """Model for service parameters/fields.""" + + description: str + example: Any + selector: Optional[Dict[str, Any]] = None + name: Optional[str] = None + required: Optional[bool] = None + + class Service(BaseModel): """Model representing services from homeassistant""" service_id: str domain: Domain name: Optional[str] = None - description: Optional[Dict[str, str]] = None - fields: Optional[Dict[str, str]] = None - target: Optional[Dict[str, str]] = None + description: Optional[str] = None + fields: Optional[Dict[str, ServiceField]] = None + target: Optional[Dict[str, dict]] = None def trigger(self, **service_data) -> Tuple[State, ...]: """Triggers the service associated with this object.""" diff --git a/homeassistant_api/models/entity.py b/homeassistant_api/models/entity.py index d3ee0c14..a1c85748 100644 --- a/homeassistant_api/models/entity.py +++ b/homeassistant_api/models/entity.py @@ -3,8 +3,7 @@ from os.path import join from typing import TYPE_CHECKING, Any, Dict, Optional, cast -from pydantic import BaseModel - +from .base import BaseModel from .history import History from .states import State @@ -47,11 +46,7 @@ class Entity(BaseModel): group: Group def get_state(self) -> State: - """Returns the state last fetched from the api.""" - return self.state - - def fetch_state(self) -> State: - """Asks homeassistant for the state of the entity and sets it locally""" + """Asks Home Assistant for the state of the entity and caches it locally""" state_data = self.group.client.request(join("states", self.entity_id)) self.state = self.group.client.process_state_json( cast(Dict[str, Any], state_data) @@ -60,7 +55,7 @@ def fetch_state(self) -> State: def set_state(self, state: State) -> State: """ - Tells homeassistant to set the given State object. + Tells Home Assistant to set the given State object. (You can construct the state object yourself.) """ state_data = self.group.client.request( diff --git a/homeassistant_api/models/events.py b/homeassistant_api/models/events.py index 3f5c3342..67f1a590 100644 --- a/homeassistant_api/models/events.py +++ b/homeassistant_api/models/events.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Dict, cast -from pydantic import BaseModel +from .base import BaseModel if TYPE_CHECKING: from homeassistant_api import Client @@ -10,7 +10,7 @@ class Event(BaseModel): """ - Event class for Homeassistant Event Triggers + Event class for Home Assistant Event Triggers For attribute information see the Data Science docs on Event models https://data.home-assistant.io/docs/events diff --git a/homeassistant_api/models/history.py b/homeassistant_api/models/history.py index 762e8a04..6baa3751 100644 --- a/homeassistant_api/models/history.py +++ b/homeassistant_api/models/history.py @@ -1,13 +1,12 @@ """Module for the History model.""" from typing import Tuple -from pydantic import BaseModel - +from .base import BaseModel from .states import State class History(BaseModel): - """Model representing past states of an entity.""" + """Model representing past :code:`State`'s of an entity.""" states: Tuple[State, ...] @@ -17,7 +16,7 @@ def __init__(self, *args, **kwargs): @property def entity_id(self) -> str: - """Returns the shared entity_id of states.""" + """Returns the shared :code:`entity_id` of states.""" entity_ids = [state.entity_id for state in self.states] result, *others = set(entity_ids) assert len(others) == 0 diff --git a/homeassistant_api/models/states.py b/homeassistant_api/models/states.py index 2b89697c..a502981c 100644 --- a/homeassistant_api/models/states.py +++ b/homeassistant_api/models/states.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Any, Dict, Optional -from pydantic import BaseModel +from .base import BaseModel class State(BaseModel): @@ -13,4 +13,4 @@ class State(BaseModel): attributes: Dict[str, Any] = {} last_changed: Optional[datetime] = None last_updated: Optional[datetime] = None - context: Dict[str, str] = {} + context: Dict[str, Optional[str]] = {} diff --git a/homeassistant_api/processing.py b/homeassistant_api/processing.py index 043b4655..6683c971 100644 --- a/homeassistant_api/processing.py +++ b/homeassistant_api/processing.py @@ -7,7 +7,6 @@ import simplejson from aiohttp import ClientResponse from aiohttp_client_cache.response import CachedResponse as AsyncCachedResponse -from pydantic import BaseModel from requests import Response from requests_cache import CachedResponse @@ -20,6 +19,7 @@ UnauthorizedError, UnexpectedStatusCodeError, ) +from .models import BaseModel class Processing(BaseModel): @@ -28,11 +28,6 @@ class Processing(BaseModel): response: Union[Response, CachedResponse, ClientResponse, AsyncCachedResponse] _processors: Dict[str, Tuple[Callable, ...]] = {} - class Config: # pylint: disable=too-few-public-methods - """A pydantic config class.""" - - arbitrary_types_allowed: bool = True - @staticmethod def processor(mimetype: str): """A decorator used to register a response converter function.""" @@ -40,7 +35,9 @@ def processor(mimetype: str): def register_processor(processor): if mimetype not in Processing._processors: Processing._processors[mimetype] = tuple() - Processing._processors[mimetype] += (processor,) + Processing._processors[mimetype] = (processor,) + Processing._processors[ + mimetype + ] return processor return register_processor @@ -101,7 +98,7 @@ def process_json(response): return response.json() except (json.decoder.JSONDecodeError, simplejson.decoder.JSONDecodeError) as err: raise MalformedDataError( - f"Homeassistant responded with non-json response: {repr(response.text)}" + f"Home Assistant responded with non-json response: {repr(response.text)}" ) from err @@ -118,7 +115,7 @@ async def async_process_json(response): return await response.json() except (json.decoder.JSONDecodeError, simplejson.decoder.JSONDecodeError) as err: raise MalformedDataError( - f"Homeassistant responded with non-json response: {repr(await response.text())}" + f"Home Assistant responded with non-json response: {repr(await response.text())}" ) from err diff --git a/homeassistant_api/rawapi.py b/homeassistant_api/rawapi.py index 795c00cf..42e82be3 100644 --- a/homeassistant_api/rawapi.py +++ b/homeassistant_api/rawapi.py @@ -21,14 +21,23 @@ def __init__( api_url: str, token: str, global_request_kwargs: Optional[Dict[str, str]] = None, + cache_backend=None, + cache_expire_after: Optional[int] = None, ) -> None: - self.api_url = api_url - self.token = token if global_request_kwargs is None: global_request_kwargs = {} - self.global_request_kwargs = global_request_kwargs if not self.api_url.endswith("/"): self.api_url += "/" + if cache_backend is None: + cache_backend = "memory" + if cache_expire_after is None: + cache_expire_after = 30 + + self.api_url = api_url + self.token = token + self.global_request_kwargs = global_request_kwargs + self.cache_backend = cache_backend + self.cache_expire_after = cache_expire_after def endpoint(self, path: str) -> str: """Joins the api base url with a local path to an absolute url""" diff --git a/homeassistant_api/rawclient.py b/homeassistant_api/rawclient.py index 942dfc3f..cbcefca6 100644 --- a/homeassistant_api/rawclient.py +++ b/homeassistant_api/rawclient.py @@ -27,7 +27,10 @@ class RawClient(RawWrapper, JsonProcessingMixin): _session: Optional[CachedSession] = None def __enter__(self): - self._session = CachedSession(expire_after=30, backend="memory") + self._session = CachedSession( + expire_after=self.cache_expire_after, + backend=self.cache_backend, + ) self._session.__enter__() self.check_api_running() self.check_api_config() @@ -48,16 +51,23 @@ def request( try: if self.global_request_kwargs is not None: kwargs.update(self.global_request_kwargs) - assert self._session is not None - resp = self._session.request( - method, - self.endpoint(path), - headers=self.prepare_headers(headers), - **kwargs, - ) + if self._session is not None: + resp = self._session.request( + method, + self.endpoint(path), + headers=self.prepare_headers(headers), + **kwargs, + ) + else: + resp = requests.request( + method, + self.endpoint(path), + headers=self.prepare_headers(headers), + **kwargs, + ) except requests.exceptions.Timeout as err: raise RequestError( - f'Homeassistant did not respond in time (timeout: {kwargs.get("timeout", 300)} sec)' + f'Home Assistant did not respond in time (timeout: {kwargs.get("timeout", 300)} sec)' ) from err return self.response_logic(resp) @@ -113,7 +123,7 @@ def get_entity_histories(self, *args, **kwargs) -> Generator[History, None, None def get_rendered_template(self, template: str) -> str: """ - Renders a Jinja2 template with homeassistant context data. + Renders a Jinja2 template with Home Assistant context data. See https://developers.home-assistant.io/docs/api/rest/. """ return cast( @@ -133,7 +143,7 @@ def get_discovery_info(self) -> Dict[str, Any]: # API check methods def check_api_config(self) -> bool: - """Asks homeassistant to validate its configuration file""" + """Asks Home Assistant to validate its configuration file""" res = cast(dict, self.request("config/core/check_config", method="POST")) valid = {"valid": True, "invalid": False}.get(res["result"], False) if valid is False: @@ -141,7 +151,7 @@ def check_api_config(self) -> bool: return valid def check_api_running(self) -> bool: - """Asks homeassistant if its running""" + """Asks Home Assistant if its running""" res = self.request("") if cast(dict, res).get("message", None) == "API running.": return True @@ -192,8 +202,13 @@ def get_domains(self) -> Tuple[Domain, ...]: ) return tuple(services) - def get_domain(self, domain: str) -> Domain: + def get_domain(self, domain_id: str) -> Optional[Domain]: """Fetchers all services under a particular domain.""" + domains = self.get_domains() + for domain in domains: + if domain.domain_id == domain_id: + return domain + return None def trigger_service( self, @@ -201,7 +216,7 @@ def trigger_service( service: str, **service_data, ) -> Tuple[State, ...]: - """Tells homeassistant to trigger a service, returns stats changed while being called""" + """Tells Home Assistant to trigger a service, returns stats changed while being called""" data = self.request( join("services", domain, service), method="POST", diff --git a/pyproject.toml b/pyproject.toml index 2cb8c6d1..e59cd61b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "Home Assistant API" -version = "2.4.1" +version = "3.0.0" description = "Python Wrapper for Homeassistant's REST API" authors = ["GrandMoff100 "] license = "GPL-3.0-or-later" @@ -44,11 +44,14 @@ profile = "black" disable = [ "invalid-name", "duplicate-code", - "no-member" + "no-member", + "too-few-public-methods", + "too-many-arguments" ] [tool.pylint.master] extension-pkg-whitelist = ["pydantic"] +ignore-paths = ["examples"] [tool.bandit] skips = ["B105"] diff --git a/scripts/run_docs_dev b/scripts/run_docs_dev new file mode 100755 index 00000000..d4f1ad00 --- /dev/null +++ b/scripts/run_docs_dev @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +rm -rf build +sphinx-build docs build +cd build +python -m http.server +cd ../ \ No newline at end of file