├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── issue.md ├── dependabot.yml ├── labels.yml ├── release-drafter.yml └── workflows │ ├── build.yaml │ ├── constraints.txt │ ├── labeler.yml │ └── release-drafter.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── conftest.py ├── examples ├── doc-example-1.py ├── doc-example-2.py ├── doc-example-3.py ├── doc-example-4.py ├── doc-example-5.py └── doc-example-6.py ├── hyperion ├── __init__.py ├── client.py ├── const.py └── py.typed ├── images └── hyperion-logo.png ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg └── tests ├── __init__.py ├── client_test.py └── testdata └── serverinfo_response_1.json /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Create a report to help us improve 4 | --- 5 | 6 | 14 | 15 | ## Version 16 | 17 | 18 | 19 | ## Describe the bug 20 | 21 | A clear and concise description of what the bug is. 22 | 23 | ## Debug log 24 | 25 | ```text 26 | 27 | Add your logs here. 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: pip 8 | directory: "/.github/workflows" 9 | schedule: 10 | interval: daily 11 | - package-ecosystem: pip 12 | directory: "/" 13 | schedule: 14 | interval: daily 15 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Labels names are important as they are used by Release Drafter to decide 3 | # regarding where to record them in changelog or if to skip them. 4 | # 5 | # The repository labels will be automatically configured using this file and 6 | # the GitHub Action https://github.com/marketplace/actions/github-labeler. 7 | - name: breaking 8 | description: Breaking Changes 9 | color: bfd4f2 10 | - name: bug 11 | description: Something isn't working 12 | color: d73a4a 13 | - name: build 14 | description: Build System and Dependencies 15 | color: bfdadc 16 | - name: ci 17 | description: Continuous Integration 18 | color: 4a97d6 19 | - name: dependencies 20 | description: Pull requests that update a dependency file 21 | color: 0366d6 22 | - name: documentation 23 | description: Improvements or additions to documentation 24 | color: 0075ca 25 | - name: duplicate 26 | description: This issue or pull request already exists 27 | color: cfd3d7 28 | - name: enhancement 29 | description: New feature or request 30 | color: a2eeef 31 | - name: github_actions 32 | description: Pull requests that update Github_actions code 33 | color: "000000" 34 | - name: good first issue 35 | description: Good for newcomers 36 | color: 7057ff 37 | - name: help wanted 38 | description: Extra attention is needed 39 | color: 008672 40 | - name: invalid 41 | description: This doesn't seem right 42 | color: e4e669 43 | - name: performance 44 | description: Performance 45 | color: "016175" 46 | - name: python 47 | description: Pull requests that update Python code 48 | color: 2b67c6 49 | - name: question 50 | description: Further information is requested 51 | color: d876e3 52 | - name: refactoring 53 | description: Refactoring 54 | color: ef67c4 55 | - name: removal 56 | description: Removals and Deprecations 57 | color: 9ae7ea 58 | - name: style 59 | description: Style 60 | color: c120e5 61 | - name: testing 62 | description: Testing 63 | color: b1fc6f 64 | - name: wontfix 65 | description: This will not be worked on 66 | color: ffffff 67 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | categories: 3 | - title: ":boom: Breaking Changes" 4 | label: "breaking" 5 | - title: ":rocket: Features" 6 | label: "enhancement" 7 | - title: ":fire: Removals and Deprecations" 8 | label: "removal" 9 | - title: ":beetle: Fixes" 10 | label: "bug" 11 | - title: ":racehorse: Performance" 12 | label: "performance" 13 | - title: ":rotating_light: Testing" 14 | label: "testing" 15 | - title: ":construction_worker: Continuous Integration" 16 | label: "ci" 17 | - title: ":books: Documentation" 18 | label: "documentation" 19 | - title: ":hammer: Refactoring" 20 | label: "refactoring" 21 | - title: ":lipstick: Style" 22 | label: "style" 23 | - title: ":package: Dependencies" 24 | labels: 25 | - "dependencies" 26 | - "build" 27 | template: | 28 | ## Changes 29 | 30 | $CHANGES 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | - dev 10 | pull_request: 11 | schedule: 12 | - cron: "17 6 * * *" 13 | workflow_dispatch: 14 | 15 | jobs: 16 | pre-commit: 17 | runs-on: "ubuntu-latest" 18 | strategy: 19 | matrix: 20 | python-version: [3.8, 3.9] 21 | name: Pre-commit 22 | steps: 23 | - name: Check out the repository 24 | uses: actions/checkout@v3 25 | 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | 31 | - name: Upgrade pip 32 | run: | 33 | pip install --constraint=.github/workflows/constraints.txt pip 34 | pip --version 35 | 36 | - name: Install Python modules 37 | run: | 38 | pip install --constraint=.github/workflows/constraints.txt \ 39 | pre-commit black flake8 reorder-python-imports 40 | 41 | - name: Run pre-commit on all files 42 | run: | 43 | pre-commit run --all-files --show-diff-on-failure --color=always 44 | 45 | tests: 46 | runs-on: "ubuntu-latest" 47 | strategy: 48 | matrix: 49 | python-version: [3.8, 3.9] 50 | name: Run tests 51 | steps: 52 | - name: Check out code from GitHub 53 | uses: "actions/checkout@v3" 54 | - name: Setup Python ${{ matrix.python-version }} 55 | uses: "actions/setup-python@v4" 56 | with: 57 | python-version: ${{ matrix.python-version }} 58 | - name: Install requirements 59 | run: | 60 | pip install --constraint=.github/workflows/constraints.txt pip 61 | pip install -r requirements_dev.txt 62 | - name: Tests suite 63 | run: | 64 | pytest -p no:sugar 65 | - name: Upload coverage to Codecov 66 | uses: codecov/codecov-action@v3.1.0 67 | with: 68 | env_vars: OS,PYTHON 69 | verbose: true 70 | -------------------------------------------------------------------------------- /.github/workflows/constraints.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dermotduffy/hyperion-py/1a51550d5729f1706070b8570ffb8987958a12fe/.github/workflows/constraints.txt -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Manage labels 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | jobs: 10 | labeler: 11 | name: Labeler 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out the repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Run Labeler 18 | uses: crazy-max/ghaction-github-labeler@v4.0.0 19 | with: 20 | skip-delete: true 21 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Draft a release note 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | jobs: 9 | draft_release: 10 | name: Release Drafter 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Run release-drafter 14 | uses: release-drafter/release-drafter@v5.20.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # pytest 2 | .pytest_cache 3 | .cache 4 | 5 | # GITHUB Proposed Python stuff: 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Packages 12 | *.egg 13 | *.egg-info 14 | dist 15 | build 16 | eggs 17 | .eggs 18 | parts 19 | bin 20 | var 21 | sdist 22 | develop-eggs 23 | .installed.cfg 24 | lib 25 | lib64 26 | pip-wheel-metadata 27 | 28 | # Logs 29 | *.log 30 | pip-log.txt 31 | 32 | # Unit test / coverage reports 33 | .coverage 34 | .tox 35 | coverage.xml 36 | nosetests.xml 37 | htmlcov/ 38 | test-reports/ 39 | test-results.xml 40 | test-output.xml 41 | 42 | # Translations 43 | *.mo 44 | 45 | .python-version 46 | 47 | # emacs auto backups 48 | *~ 49 | *# 50 | *.orig 51 | 52 | # venv stuff 53 | pyvenv.cfg 54 | pip-selfcheck.json 55 | venv 56 | .venv 57 | Pipfile* 58 | share/* 59 | Scripts/ 60 | 61 | # vimmy stuff 62 | *.swp 63 | *.swo 64 | tags 65 | ctags.tmp 66 | 67 | # Visual Studio Code 68 | .vscode/* 69 | !.vscode/cSpell.json 70 | !.vscode/extensions.json 71 | !.vscode/tasks.json 72 | .env 73 | 74 | # Built docs 75 | docs/build 76 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v2.34.0 4 | hooks: 5 | - id: pyupgrade 6 | args: [--py37-plus] 7 | - repo: https://github.com/psf/black 8 | rev: 22.3.0 9 | hooks: 10 | - id: black 11 | - repo: https://github.com/codespell-project/codespell 12 | rev: v2.1.0 13 | hooks: 14 | - id: codespell 15 | args: 16 | - --skip="./.*,*.csv,*.json" 17 | - --quiet-level=2 18 | exclude_types: [csv, json] 19 | - repo: https://github.com/PyCQA/flake8 20 | rev: 4.0.1 21 | hooks: 22 | - id: flake8 23 | additional_dependencies: 24 | - flake8-docstrings 25 | - pydocstyle 26 | - repo: https://github.com/pre-commit/pre-commit-hooks 27 | rev: v4.3.0 28 | hooks: 29 | - id: check-executables-have-shebangs 30 | - id: check-json 31 | - repo: https://github.com/adrienverge/yamllint.git 32 | rev: v1.26.3 33 | hooks: 34 | - id: yamllint 35 | - repo: https://github.com/pre-commit/mirrors-mypy 36 | rev: 'v0.961' 37 | hooks: 38 | - id: mypy 39 | additional_dependencies: 40 | # Need to include pytest for fixture decorator type hints. 41 | - pytest==7.1.2 42 | - repo: https://github.com/PyCQA/isort 43 | rev: 5.10.1 44 | hooks: 45 | - id: isort 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dermot Duffy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hyperion logo 6 | 7 | [![PyPi](https://img.shields.io/pypi/v/hyperion-py.svg?style=flat-square)](https://pypi.org/project/hyperion-py/) 8 | [![PyPi](https://img.shields.io/pypi/pyversions/hyperion-py.svg?style=flat-square)](https://pypi.org/project/hyperion-py/) 9 | [![Build Status](https://img.shields.io/github/workflow/status/dermotduffy/hyperion-py/Build?style=flat-square)](https://github.com/dermotduffy/hyperion-py/actions/workflows/build.yaml) 10 | [![Test Coverage](https://img.shields.io/codecov/c/gh/dermotduffy/hyperion-py?style=flat-square)](https://codecov.io/gh/dermotduffy/hyperion-py) 11 | [![License](https://img.shields.io/github/license/dermotduffy/hyperion-py.svg?style=flat-square)](LICENSE) 12 | [![BuyMeCoffee](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=flat-square)](https://www.buymeacoffee.com/dermotdu) 13 | 14 | # Hyperion Library 15 | 16 | Python library for 17 | [Hyperion-NG](https://github.com/hyperion-project/hyperion.ng). See [JSON 18 | API](https://docs.hyperion-project.org/en/json/) for more details about the 19 | inputs and outputs of this library. 20 | 21 | # Installation 22 | 23 | ```bash 24 | $ pip3 install hyperion-py 25 | ``` 26 | 27 | # Usage 28 | 29 | ## Data model philosophy 30 | 31 | Whilst not universally true, this library attempts to precisely represent the 32 | data model, API and parameters as defined in the [Hyperion JSON 33 | documentation](https://docs.hyperion-project.org/en/json/). Thus no attempt is 34 | made (intentionally) to present convenient accessors/calls at a finer level of 35 | granularity than the model already supports. This is to ensure the client has a 36 | decent chance at staying functional regardless of underlying data model changes 37 | from the server, and the responsibility to match the changes to the server's 38 | data model (e.g. new Hyperion server features) belong to the caller. 39 | 40 | ### Constructor Arguments 41 | 42 | The following arguments may be passed to the `HyperionClient` constructor: 43 | 44 | |Argument|Type|Default|Description| 45 | |--------|----|-------|-----------| 46 | |host |`str`||Host or IP to connect to| 47 | |port |`int`|19444|Port to connect to| 48 | |default_callback|`callable`|None|A callable for Hyperion callbacks. See [callbacks](#callbacks)| 49 | |callbacks|`dict`|None|A dictionary of callables keyed by the update name. See [callbacks](#callbacks)| 50 | |token|`str`|None|An authentication token| 51 | |instance|`int`|0|An instance id to switch to upon connection| 52 | |origin|`str`|"hyperion-py"|An arbitrary string describing the calling application| 53 | |timeout_secs|`float`|5.0|The number of seconds to wait for a server response or connection attempt before giving up. See [timeouts](#timeouts)| 54 | |retry_secs|`float`|30.0|The number of seconds between connection attempts| 55 | |raw_connection|`bool`|False|If True, the connect call will establish the network connection but not attempt to authenticate, switch to the required instance or load state. The client must call `async_client_login` to login, `async_client_switch_instance` to switch to the configured instance and `async_get_serverinfo` to load the state manually. This may be useful if the caller wishes to communicate with the server prior to authentication.| 56 | 57 | ### Connection, disconnection and client control calls 58 | 59 | * `async_client_connect()`: Connect the client. 60 | * `async_client_disconnect()`: Disconnect the client. 61 | * `async_client_login()`: Login a connected client. Automatically called by 62 | `async_client_connect()` unless the `raw_connection` constructor argument is True. 63 | * `async_client_switch_instance()`: Switch to the configured instance on the Hyperion 64 | server. Automatically called by `async_client_connect()` unless the `raw_connection` 65 | constructor argument is True. 66 | 67 | ### Native API Calls 68 | 69 | All API calls can be found in 70 | [client.py](https://github.com/dermotduffy/hyperion-py/blob/master/hyperion/client.py). 71 | All async calls start with `async_`. 72 | 73 | |Send request and await response|Send request only|Documentation| 74 | |-------------------------------|-----------------|-------------| 75 | |async_clear|async_send_clear|[Docs](https://docs.hyperion-project.org/en/json/Control.html#clear)| 76 | |async_image_stream_start|async_send_image_stream_start|[Docs](https://docs.hyperion-project.org/en/json/Control.html#live-image-stream)| 77 | |async_image_stream_stop|async_send_image_stream_stop|[Docs](https://docs.hyperion-project.org/en/json/Control.html#live-image-stream)| 78 | |async_is_auth_required|async_send_is_auth_required|[Docs](https://docs.hyperion-project.org/en/json/Authorization.html#authorization-check)| 79 | |async_led_stream_start|async_send_led_stream_start|[Docs](https://docs.hyperion-project.org/en/json/Control.html#live-led-color-stream)| 80 | |async_led_stream_stop|async_send_led_stream_stop|[Docs](https://docs.hyperion-project.org/en/json/Control.html#live-led-color-stream)| 81 | |async_login|async_send_login|[Docs](https://docs.hyperion-project.org/en/json/Authorization.html#login-with-token)| 82 | |async_logout|async_send_logout|[Docs](https://docs.hyperion-project.org/en/json/Authorization.html#logout)| 83 | |async_request_token|async_send_request_token|[Docs](https://docs.hyperion-project.org/en/json/Authorization.html#request-a-token)| 84 | |async_request_token_abort|async_send_request_token_abort|[Docs](https://docs.hyperion-project.org/en/json/Authorization.html#request-a-token)| 85 | |async_get_serverinfo|async_send_get_serverinfo|[Docs](https://docs.hyperion-project.org/en/json/ServerInfo.html#parts)| 86 | |async_set_adjustment|async_send_set_adjustment|[Docs](https://docs.hyperion-project.org/en/json/Control.html#adjustments)| 87 | |async_set_color|async_send_set_color|[Docs](https://docs.hyperion-project.org/en/json/Control.html#set-color)| 88 | |async_set_component|async_send_set_component|[Docs](https://docs.hyperion-project.org/en/json/Control.html#control-components)| 89 | |async_set_effect|async_send_set_effect|[Docs](https://docs.hyperion-project.org/en/json/Control.html#set-effect)| 90 | |async_set_image|async_send_set_image|[Docs](https://docs.hyperion-project.org/en/json/Control.html#set-image)| 91 | |async_set_led_mapping_type|async_send_set_led_mapping_type|[Docs](https://docs.hyperion-project.org/en/json/Control.html#led-mapping)| 92 | |async_set_sourceselect|async_send_set_sourceselect|[Docs](https://docs.hyperion-project.org/en/json/Control.html#source-selection)| 93 | |async_set_videomode|async_send_set_videomode|[Docs](https://docs.hyperion-project.org/en/json/Control.html#video-mode)| 94 | |async_start_instance|async_send_start_instance|[Docs](https://docs.hyperion-project.org/en/json/Control.html#control-instances)| 95 | |async_stop_instance|async_send_stop_instance|[Docs](https://docs.hyperion-project.org/en/json/Control.html#control-instances)| 96 | |async_switch_instance|async_send_switch_instance|[Docs](https://docs.hyperion-project.org/en/json/Control.html#api-instance-handling)| 97 | |async_sysinfo|async_send_sysinfo|[Docs](https://docs.hyperion-project.org/en/json/ServerInfo.html#system-hyperion)| 98 | 99 | Note that the `command` and `subcommand` keys shown in the above linked 100 | documentation will automatically be included in the calls the client sends, and 101 | do not need to be specified. 102 | 103 | ## Client inputs / outputs 104 | 105 | The API parameters and output are all as defined in the [JSON API 106 | documentation](https://docs.hyperion-project.org/en/json/). 107 | 108 | ## Example usage: 109 | 110 | ```python 111 | #!/usr/bin/env python 112 | """Simple Hyperion client read demonstration.""" 113 | 114 | import asyncio 115 | 116 | from hyperion import client, const 117 | 118 | HOST = "hyperion" 119 | 120 | 121 | async def print_brightness() -> None: 122 | """Print Hyperion brightness.""" 123 | 124 | async with client.HyperionClient(HOST) as hyperion_client: 125 | assert hyperion_client 126 | 127 | adjustment = hyperion_client.adjustment 128 | assert adjustment 129 | 130 | print("Brightness: %i%%" % adjustment[0][const.KEY_BRIGHTNESS]) 131 | 132 | 133 | if __name__ == "__main__": 134 | asyncio.get_event_loop().run_until_complete(print_brightness()) 135 | ``` 136 | 137 | ## Running in the background 138 | 139 | A background `asyncio task` runs to process all post-connection inbound data 140 | (e.g. request responses, or subscription updates from state changes on the 141 | server side). This background task must either be started post-connection, or 142 | start (and it will itself establish connection). 143 | 144 | Optionally, this background task can call callbacks back to the user. 145 | 146 | ### Waiting for responses 147 | 148 | If the user makes a call that does not have `_send_` in the name (see table 149 | above), the function call will wait for the response and return it to the 150 | caller. This matching of request & response is done via the `tan` parameter. If 151 | not specified, the client will automatically attach a `tan` integer, and this 152 | will be visible in the returned output data. This matching is necessary to 153 | differentiate between responses due to requests, and "spontaneous data" from 154 | subscription updates. 155 | 156 | #### Example: Waiting for a response 157 | 158 | ```python 159 | #!/usr/bin/env python 160 | """Simple Hyperion client request demonstration.""" 161 | 162 | import asyncio 163 | 164 | from hyperion import client 165 | 166 | HOST = "hyperion" 167 | 168 | 169 | async def print_if_auth_required() -> None: 170 | """Print whether auth is required.""" 171 | 172 | hc = client.HyperionClient(HOST) 173 | await hc.async_client_connect() 174 | 175 | result = await hc.async_is_auth_required() 176 | print("Result: %s" % result) 177 | 178 | await hc.async_client_disconnect() 179 | 180 | 181 | asyncio.get_event_loop().run_until_complete(print_if_auth_required()) 182 | ``` 183 | 184 | Output: 185 | 186 | ``` 187 | Result: {'command': 'authorize-tokenRequired', 'info': {'required': False}, 'success': True, 'tan': 1} 188 | ``` 189 | 190 | #### Example: Sending commands 191 | 192 | A slightly more complex example that sends commands (clears the Hyperion source 193 | select at a given priority, then sets color at that same priority). 194 | 195 | ```python 196 | #!/usr/bin/env python 197 | """Simple Hyperion client request demonstration.""" 198 | 199 | import asyncio 200 | import logging 201 | import sys 202 | 203 | from hyperion import client 204 | 205 | HOST = "hyperion" 206 | PRIORITY = 20 207 | 208 | 209 | async def set_color() -> None: 210 | """Set red color on Hyperion.""" 211 | 212 | async with client.HyperionClient(HOST) as hc: 213 | assert hc 214 | 215 | if not await hc.async_client_connect(): 216 | logging.error("Could not connect to: %s", HOST) 217 | return 218 | 219 | if not client.ResponseOK( 220 | await hc.async_clear(priority=PRIORITY) 221 | ) or not client.ResponseOK( 222 | await hc.async_set_color( 223 | color=[255, 0, 0], priority=PRIORITY, origin=sys.argv[0] 224 | ) 225 | ): 226 | logging.error("Could not clear/set_color on: %s", HOST) 227 | return 228 | 229 | 230 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 231 | asyncio.get_event_loop().run_until_complete(set_color()) 232 | ``` 233 | 234 | #### Example: Starting and switching instances 235 | 236 | The following example will start a stopped instance, wait for it to be ready, 237 | then switch to it. Uses [callbacks](#callbacks), discussed below. 238 | 239 | 240 | ```python 241 | #!/usr/bin/env python 242 | """Simple Hyperion client request demonstration.""" 243 | 244 | from __future__ import annotations 245 | 246 | import asyncio 247 | import logging 248 | import sys 249 | from typing import Any 250 | 251 | from hyperion import client 252 | 253 | HOST = "hyperion" 254 | PRIORITY = 20 255 | 256 | 257 | async def instance_start_and_switch() -> None: 258 | """Wait for an instance to start.""" 259 | 260 | instance_ready = asyncio.Event() 261 | 262 | def instance_update(json: dict[str, Any]) -> None: 263 | for data in json["data"]: 264 | if data["instance"] == 1 and data["running"]: 265 | instance_ready.set() 266 | 267 | async with client.HyperionClient( 268 | HOST, callbacks={"instance-update": instance_update} 269 | ) as hc: 270 | assert hc 271 | 272 | if not client.ResponseOK(await hc.async_start_instance(instance=1)): 273 | logging.error("Could not start instance on: %s", HOST) 274 | return 275 | 276 | # Blocks waiting for the instance to start. 277 | await instance_ready.wait() 278 | 279 | if not client.ResponseOK(await hc.async_switch_instance(instance=1)): 280 | logging.error("Could not switch instance on: %s", HOST) 281 | return 282 | 283 | 284 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 285 | asyncio.get_event_loop().run_until_complete(instance_start_and_switch()) 286 | ``` 287 | 288 | 289 | ### Callbacks 290 | 291 | The client can be configured to callback as the Hyperion server reports new 292 | values. There are two classes of callbacks supported: 293 | 294 | * **default_callback**: This callback will be called when a more specific callback is not specified. 295 | * **callbacks**: A dict of callbacks keyed on the Hyperion subscription 'command' (see [JSON API documentation](https://docs.hyperion-project.org/en/json/)) 296 | 297 | Callbacks can be specified in the `HyperionClient` constructor 298 | (`default_callback=` or `callbacks=` arguments) or after construction via the 299 | `set_callbacks()` and `set_default_callback()` methods. 300 | 301 | As above, the `callbacks` dict is keyed on the relevant Hyperion subscription 302 | `command` (e.g. `components-update`, `priorities-update`). The client also 303 | provides a custom callback with command `client-update` of the following 304 | form: 305 | 306 | ```python 307 | {"command": "client-update", 308 | "connected": True, 309 | "logged-in": True, 310 | "instance": 0, 311 | "loaded-state": True} 312 | ``` 313 | 314 | This can be used to take special action as the client connects or disconnects from the server. 315 | 316 | #### Example: Callbacks 317 | 318 | ```python 319 | #!/usr/bin/env python 320 | """Simple Hyperion client callback demonstration.""" 321 | 322 | from __future__ import annotations 323 | 324 | import asyncio 325 | from typing import Any 326 | 327 | from hyperion import client 328 | 329 | HOST = "hyperion" 330 | 331 | 332 | def callback(json: dict[str, Any]) -> None: 333 | """Sample callback function.""" 334 | 335 | print("Received Hyperion callback: %s" % json) 336 | 337 | 338 | async def show_callback() -> None: 339 | """Show a default callback is called.""" 340 | 341 | async with client.HyperionClient(HOST, default_callback=callback): 342 | pass 343 | 344 | 345 | if __name__ == "__main__": 346 | asyncio.get_event_loop().run_until_complete(show_callback()) 347 | ``` 348 | 349 | Output, showing the progression of connection stages: 350 | 351 | ``` 352 | Received Hyperion callback: {'connected': True, 'logged-in': False, 'instance': None, 'loaded-state': False, 'command': 'client-update'} 353 | Received Hyperion callback: {'connected': True, 'logged-in': True, 'instance': None, 'loaded-state': False, 'command': 'client-update'} 354 | Received Hyperion callback: {'connected': True, 'logged-in': True, 'instance': 0, 'loaded-state': False, 'command': 'client-update'} 355 | Received Hyperion callback: {'command': 'serverinfo', ... } 356 | Received Hyperion callback: {'connected': True, 'logged-in': True, 'instance': 0, 'loaded-state': True, 'command': 'client-update'} 357 | ``` 358 | 359 | ## ThreadedHyperionClient 360 | 361 | A `ThreadedHyperionClient` is also provided as a convenience wrapper to for 362 | non-async code. The `ThreadedHyperionClient` wraps the async calls with 363 | non-async versions (methods are named as shown above, except do not start with 364 | `async_`). 365 | 366 | ### Waiting for the thread to initialize the client 367 | 368 | The thread must be given a chance to initialize the client prior to interaction 369 | with it. This method call will block the caller until the client has been initialized. 370 | 371 | * wait_for_client_init() 372 | 373 | ### Example use of Threaded client 374 | 375 | ```python 376 | #!/usr/bin/env python 377 | """Simple Threaded Hyperion client demonstration.""" 378 | 379 | from hyperion import client, const 380 | 381 | HOST = "hyperion" 382 | 383 | if __name__ == "__main__": 384 | hyperion_client = client.ThreadedHyperionClient(HOST) 385 | 386 | # Start the asyncio loop in a new thread. 387 | hyperion_client.start() 388 | 389 | # Wait for the client to initialize in the new thread. 390 | hyperion_client.wait_for_client_init() 391 | 392 | # Connect the client. 393 | hyperion_client.client_connect() 394 | 395 | print("Brightness: %i%%" % hyperion_client.adjustment[0][const.KEY_BRIGHTNESS]) 396 | 397 | # Disconnect the client. 398 | hyperion_client.client_disconnect() 399 | 400 | # Stop the loop (will stop the thread). 401 | hyperion_client.stop() 402 | 403 | # Join the created thread. 404 | hyperion_client.join() 405 | ``` 406 | 407 | Output: 408 | 409 | ``` 410 | Brightness: 59% 411 | ``` 412 | 413 | ## Exceptions / Errors 414 | 415 | ### Philosophy 416 | 417 | HyperionClient strives not to throw an exception regardless of network 418 | circumstances, reconnection will automatically happen in the background. 419 | Exceptions are only raised (intentionally) for instances of likely programmer 420 | error. 421 | 422 | ### HyperionError 423 | 424 | Not directly raised, but other exceptions inherit from this. 425 | 426 | ### HyperionClientTanNotAvailable 427 | 428 | Exception raised if a `tan` parameter is provided to an API call, but that 429 | `tan` parameter is already being used by another in-progress call. Users 430 | should either not specify `tan` at all (and the client library will 431 | automatically manage it in an incremental fashion), or if specified manually, 432 | it is the caller's responsibility to ensure no two simultaneous calls share a 433 | `tan` (as otherwise the client would not be able to match the call to the 434 | response, and this exception will be raised automatically prior to the call). 435 | 436 | ### "Task was destroyed but it is pending!" 437 | 438 | If a `HyperionClient` object is connected but destroyed prior to disconnection, a warning message may be printed ("Task was destroyed but it is pending!"). To avoid this, ensure to always call `async_client_disconnect` prior to destruction of a connected client. Alternatively use the async context manager: 439 | 440 | ```python 441 | async with client.HyperionClient(TEST_HOST, TEST_PORT) as hc: 442 | if not hc: 443 | return 444 | ... 445 | ``` 446 | 447 | 448 | ## Timeouts 449 | 450 | The client makes liberal use of timeouts, which may be specified at multiple levels: 451 | 452 | * In the client constructor argument `timeout_secs`, used for connection and requests. 453 | * In each request using a `timeout_secs` argument to the individual calls 454 | 455 | Timeout values: 456 | 457 | * `None`: If `None` is used as a timeout, the client will wait forever. 458 | * `0`: If `0` is used as a timeout, the client default (specified in the constructor) will be used. 459 | * `>0.0`: This number of seconds (or partial seconds) will be used. 460 | 461 | By default, all requests will honour the `timeout_secs` specified in the client constructor unless explicitly overridden and defaults to 5 seconds (see [const.py](https://github.com/dermotduffy/hyperion-py/blob/master/hyperion/const.py#L95)). The one exception to this is the `async_send_request_token` which has a much larger default (180 seconds, see [const.py](https://github.com/dermotduffy/hyperion-py/blob/master/hyperion/const.py#L96)) as this request involves the user needing the interact with the Hyperion UI prior to the call being able to return. 462 | 463 | 464 | ## Helpers 465 | 466 | ### ResponseOK 467 | 468 | A handful of convenience callable classes are provided to determine whether 469 | server responses were successful. 470 | 471 | * `ResponseOK`: Whether any Hyperion command response was successful (general). 472 | * `ServerInfoResponseOK`: Whether a `async_get_serverinfo` was successful. 473 | * `LoginResponseOK`: Whether an `async_login` was successful. 474 | * `SwitchInstanceResponseOK`: Whether an `async_switch_instance` command was successful. 475 | 476 | #### Example usage 477 | 478 | ``` 479 | if not client.ResponseOK(await hc.async_clear(priority=PRIORITY)) 480 | ``` 481 | 482 | ### Auth ID 483 | 484 | When requesting an auth token, a 5-character ID can be specified to ensure the 485 | admin user is authorizing the right request from the right origin. By default 486 | the `async_request_token` will randomly generate an ID, but if one is required 487 | to allow the user to confirm a match, it can be explicitly provided. In this case, 488 | this helper method is made available. 489 | 490 | * `generate_random_auth_id`: Generate a random 5-character auth ID for external display and inclusion in a call to `async_request_token`. 491 | 492 | #### Example usage 493 | 494 | ``` 495 | auth_id = hc.generate_random_auth_id() 496 | hc.async_send_login(comment="Trustworthy actor", id=auth_id) 497 | # Show auth_id to the user to allow them to verify the origin of the request, 498 | # then have them visit the Hyperion UI. 499 | ``` 500 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | """Conftest.""" 2 | -------------------------------------------------------------------------------- /examples/doc-example-1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Simple Hyperion client read demonstration.""" 3 | 4 | import asyncio 5 | 6 | from hyperion import client, const 7 | 8 | HOST = "hyperion" 9 | 10 | 11 | async def print_brightness() -> None: 12 | """Print Hyperion brightness.""" 13 | 14 | async with client.HyperionClient(HOST) as hyperion_client: 15 | assert hyperion_client 16 | 17 | adjustment = hyperion_client.adjustment 18 | assert adjustment 19 | 20 | print("Brightness: %i%%" % adjustment[0][const.KEY_BRIGHTNESS]) 21 | 22 | 23 | if __name__ == "__main__": 24 | asyncio.get_event_loop().run_until_complete(print_brightness()) 25 | -------------------------------------------------------------------------------- /examples/doc-example-2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Simple Hyperion client request demonstration.""" 3 | 4 | import asyncio 5 | 6 | from hyperion import client 7 | 8 | HOST = "hyperion" 9 | 10 | 11 | async def print_if_auth_required() -> None: 12 | """Print whether auth is required.""" 13 | 14 | hc = client.HyperionClient(HOST) 15 | await hc.async_client_connect() 16 | 17 | result = await hc.async_is_auth_required() 18 | print("Result: %s" % result) 19 | 20 | await hc.async_client_disconnect() 21 | 22 | 23 | asyncio.get_event_loop().run_until_complete(print_if_auth_required()) 24 | -------------------------------------------------------------------------------- /examples/doc-example-3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Simple Hyperion client callback demonstration.""" 3 | 4 | from __future__ import annotations 5 | 6 | import asyncio 7 | from typing import Any 8 | 9 | from hyperion import client 10 | 11 | HOST = "hyperion" 12 | 13 | 14 | def callback(json: dict[str, Any]) -> None: 15 | """Sample callback function.""" 16 | 17 | print("Received Hyperion callback: %s" % json) 18 | 19 | 20 | async def show_callback() -> None: 21 | """Show a default callback is called.""" 22 | 23 | async with client.HyperionClient(HOST, default_callback=callback): 24 | pass 25 | 26 | 27 | if __name__ == "__main__": 28 | asyncio.get_event_loop().run_until_complete(show_callback()) 29 | -------------------------------------------------------------------------------- /examples/doc-example-4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Simple Threaded Hyperion client demonstration.""" 3 | 4 | from hyperion import client, const 5 | 6 | HOST = "hyperion" 7 | 8 | if __name__ == "__main__": 9 | hyperion_client = client.ThreadedHyperionClient(HOST) 10 | 11 | # Start the asyncio loop in a new thread. 12 | hyperion_client.start() 13 | 14 | # Wait for the client to initialize in the new thread. 15 | hyperion_client.wait_for_client_init() 16 | 17 | # Connect the client. 18 | hyperion_client.client_connect() 19 | 20 | print("Brightness: %i%%" % hyperion_client.adjustment[0][const.KEY_BRIGHTNESS]) 21 | 22 | # Disconnect the client. 23 | hyperion_client.client_disconnect() 24 | 25 | # Stop the loop (will stop the thread). 26 | hyperion_client.stop() 27 | 28 | # Join the created thread. 29 | hyperion_client.join() 30 | -------------------------------------------------------------------------------- /examples/doc-example-5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Simple Hyperion client request demonstration.""" 3 | 4 | import asyncio 5 | import logging 6 | import sys 7 | 8 | from hyperion import client 9 | 10 | HOST = "hyperion" 11 | PRIORITY = 20 12 | 13 | 14 | async def set_color() -> None: 15 | """Set red color on Hyperion.""" 16 | 17 | async with client.HyperionClient(HOST) as hc: 18 | assert hc 19 | 20 | if not await hc.async_client_connect(): 21 | logging.error("Could not connect to: %s", HOST) 22 | return 23 | 24 | if not client.ResponseOK( 25 | await hc.async_clear(priority=PRIORITY) 26 | ) or not client.ResponseOK( 27 | await hc.async_set_color( 28 | color=[255, 0, 0], priority=PRIORITY, origin=sys.argv[0] 29 | ) 30 | ): 31 | logging.error("Could not clear/set_color on: %s", HOST) 32 | return 33 | 34 | 35 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 36 | asyncio.get_event_loop().run_until_complete(set_color()) 37 | -------------------------------------------------------------------------------- /examples/doc-example-6.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Simple Hyperion client request demonstration.""" 3 | 4 | from __future__ import annotations 5 | 6 | import asyncio 7 | import logging 8 | import sys 9 | from typing import Any 10 | 11 | from hyperion import client 12 | 13 | HOST = "hyperion" 14 | PRIORITY = 20 15 | 16 | 17 | async def instance_start_and_switch() -> None: 18 | """Wait for an instance to start.""" 19 | 20 | instance_ready = asyncio.Event() 21 | 22 | def instance_update(json: dict[str, Any]) -> None: 23 | for data in json["data"]: 24 | if data["instance"] == 1 and data["running"]: 25 | instance_ready.set() 26 | 27 | async with client.HyperionClient( 28 | HOST, callbacks={"instance-update": instance_update} 29 | ) as hc: 30 | assert hc 31 | 32 | if not client.ResponseOK(await hc.async_start_instance(instance=1)): 33 | logging.error("Could not start instance on: %s", HOST) 34 | return 35 | 36 | # Blocks waiting for the instance to start. 37 | await instance_ready.wait() 38 | 39 | if not client.ResponseOK(await hc.async_switch_instance(instance=1)): 40 | logging.error("Could not switch instance on: %s", HOST) 41 | return 42 | 43 | 44 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 45 | asyncio.get_event_loop().run_until_complete(instance_start_and_switch()) 46 | -------------------------------------------------------------------------------- /hyperion/__init__.py: -------------------------------------------------------------------------------- 1 | """Hyperion Client package.""" 2 | -------------------------------------------------------------------------------- /hyperion/client.py: -------------------------------------------------------------------------------- 1 | """Client for Hyperion servers.""" 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | import collections 6 | import collections.abc 7 | import copy 8 | import functools 9 | import inspect 10 | import json 11 | import logging 12 | import random 13 | import string 14 | import threading 15 | from types import TracebackType 16 | 17 | # pylint: disable=unused-import 18 | from typing import ( 19 | Any, 20 | Awaitable, 21 | Callable, 22 | Coroutine, 23 | Dict, 24 | Iterable, 25 | Mapping, 26 | Union, 27 | cast, 28 | ) 29 | 30 | from hyperion import const 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | HyperionCallback = Union[ 35 | Callable[[Dict[str, Any]], Awaitable[None]], 36 | Callable[[Dict[str, Any]], None], 37 | ] 38 | 39 | 40 | class HyperionError(Exception): 41 | """Baseclass for all Hyperion exceptions.""" 42 | 43 | 44 | class HyperionClientTanNotAvailable(HyperionError): 45 | """An exception indicating the requested tan is not available.""" 46 | 47 | 48 | class HyperionClientState: 49 | """Class representing the Hyperion client state.""" 50 | 51 | def __init__(self, state: dict[str, Any] | None = None) -> None: 52 | """Initialize state object.""" 53 | self._state: dict[str, Any] = state or {} 54 | self._dirty: bool = False 55 | 56 | @property 57 | def dirty(self) -> bool: 58 | """Return whether or state has been modified.""" 59 | return self._dirty 60 | 61 | @dirty.setter 62 | def dirty(self, val: bool) -> None: 63 | self._dirty = val 64 | 65 | def get(self, key: str) -> Any: 66 | """Retrieve a state element.""" 67 | return self._state.get(key) 68 | 69 | def set(self, key: str, new_value: Any) -> None: 70 | """Set a new state value.""" 71 | old_value = self.get(key) 72 | if old_value != new_value: 73 | self._state[key] = new_value 74 | self._dirty = True 75 | 76 | def update(self, new_values: dict[str, Any]) -> None: 77 | """Update the state with a dict of values.""" 78 | for key in new_values: 79 | self.set(key, new_values[key]) 80 | 81 | def get_all(self) -> dict[str, Any]: 82 | """Get a copy of all the state values.""" 83 | return copy.copy(self._state) 84 | 85 | 86 | class HyperionClient: 87 | """Hyperion Client.""" 88 | 89 | # pylint: disable=too-many-arguments 90 | def __init__( 91 | self, 92 | host: str, 93 | port: int = const.DEFAULT_PORT_JSON, 94 | default_callback: HyperionCallback | Iterable[HyperionCallback] | None = None, 95 | callbacks: Mapping[str, HyperionCallback | Iterable[HyperionCallback]] 96 | | None = None, 97 | token: str | None = None, 98 | instance: int = const.DEFAULT_INSTANCE, 99 | origin: str = const.DEFAULT_ORIGIN, 100 | timeout_secs: float = const.DEFAULT_TIMEOUT_SECS, 101 | retry_secs: int = const.DEFAULT_CONNECTION_RETRY_DELAY_SECS, 102 | raw_connection: bool = False, 103 | ) -> None: 104 | """Initialize client.""" 105 | _LOGGER.debug("HyperionClient initiated with: (%s:%i)", host, port) 106 | 107 | self._callbacks: dict[str, list[HyperionCallback]] = {} 108 | self.set_callbacks(callbacks or {}) 109 | 110 | self._default_callback: list[HyperionCallback] = [] 111 | self.set_default_callback(default_callback or []) 112 | 113 | self._host = host 114 | self._port = port 115 | self._token = token 116 | self._target_instance = instance 117 | self._origin = origin 118 | self._timeout_secs = timeout_secs 119 | self._retry_secs = retry_secs 120 | self._raw_connection = raw_connection 121 | 122 | self._serverinfo: dict[str, Any] | None = None 123 | 124 | self._receive_task: asyncio.Task[None] | None = None 125 | self._maintenance_task: asyncio.Task[None] | None = None 126 | self._maintenance_event: asyncio.Event = asyncio.Event() 127 | 128 | self._reader: asyncio.StreamReader | None = None 129 | self._writer: asyncio.StreamWriter | None = None 130 | 131 | # Start tan @ 1, as the zeroth tan is used by default. 132 | self._tan_cv = asyncio.Condition() 133 | self._tan_counter = 1 134 | self._tan_responses: dict[ 135 | int, dict[str, Any] | None 136 | ] = collections.OrderedDict() 137 | 138 | self._client_state: HyperionClientState = HyperionClientState( 139 | state={ 140 | const.KEY_CONNECTED: False, 141 | const.KEY_LOGGED_IN: False, 142 | const.KEY_INSTANCE: None, 143 | const.KEY_LOADED_STATE: False, 144 | } 145 | ) 146 | 147 | async def __aenter__(self) -> "HyperionClient" | None: 148 | """Enter context manager and connect the client.""" 149 | result = await self.async_client_connect() 150 | return self if result else None 151 | 152 | async def __aexit__( 153 | self, 154 | exc_type: type[BaseException] | None, 155 | exc: BaseException | None, 156 | traceback: TracebackType | None, 157 | ) -> None: 158 | """Leave context manager and disconnect the client.""" 159 | await self.async_client_disconnect() 160 | 161 | async def _client_state_reset(self) -> None: 162 | self._client_state.update( 163 | { 164 | const.KEY_CONNECTED: False, 165 | const.KEY_LOGGED_IN: False, 166 | const.KEY_INSTANCE: None, 167 | const.KEY_LOADED_STATE: False, 168 | } 169 | ) 170 | 171 | # =================== 172 | # || Callbacks || 173 | # =================== 174 | 175 | @classmethod 176 | def _set_or_add_callbacks( 177 | cls, 178 | callbacks: HyperionCallback | Iterable[HyperionCallback] | None, 179 | add: bool, 180 | target: list[HyperionCallback], 181 | ) -> None: 182 | """Set or add a single or list of callbacks.""" 183 | if callbacks is not None and not isinstance( 184 | callbacks, collections.abc.Iterable 185 | ): 186 | callbacks = [callbacks] 187 | if not add: 188 | target.clear() 189 | if callbacks is not None: 190 | target.extend(callbacks) 191 | 192 | @classmethod 193 | def _remove_callbacks( 194 | cls, 195 | callbacks: HyperionCallback | Iterable[HyperionCallback], 196 | target: list[HyperionCallback], 197 | ) -> None: 198 | """Set or add a single or list of callbacks.""" 199 | if not callbacks: 200 | return 201 | if not isinstance(callbacks, collections.abc.Iterable): 202 | callbacks = [callbacks] 203 | 204 | for callback in callbacks: 205 | if callback in target: 206 | target.remove(callback) 207 | 208 | def set_callbacks( 209 | self, 210 | callbacks: Mapping[str, HyperionCallback | Iterable[HyperionCallback]] | None, 211 | ) -> None: 212 | """Set update callbacks.""" 213 | if not callbacks: 214 | self._callbacks = {} 215 | return 216 | for name, value in callbacks.items(): 217 | HyperionClient._set_or_add_callbacks( 218 | value, False, self._callbacks.setdefault(name, []) 219 | ) 220 | 221 | def add_callbacks( 222 | self, 223 | callbacks: Mapping[str, HyperionCallback | Iterable[HyperionCallback]], 224 | ) -> None: 225 | """Add update callbacks.""" 226 | if not callbacks: 227 | return 228 | for name, value in callbacks.items(): 229 | HyperionClient._set_or_add_callbacks( 230 | value, True, self._callbacks.setdefault(name, []) 231 | ) 232 | 233 | def remove_callbacks( 234 | self, 235 | callbacks: Mapping[str, HyperionCallback | Iterable[HyperionCallback]], 236 | ) -> None: 237 | """Add update callbacks.""" 238 | if not callbacks: 239 | return 240 | for name, value in callbacks.items(): 241 | if name not in self._callbacks: 242 | continue 243 | HyperionClient._remove_callbacks(value, self._callbacks[name]) 244 | 245 | def set_default_callback( 246 | self, 247 | default_callback: HyperionCallback | Iterable[HyperionCallback] | None, 248 | ) -> None: 249 | """Set the default callbacks.""" 250 | HyperionClient._set_or_add_callbacks( 251 | default_callback, False, self._default_callback 252 | ) 253 | 254 | def add_default_callback( 255 | self, 256 | default_callback: HyperionCallback | Iterable[HyperionCallback], 257 | ) -> None: 258 | """Set the default callbacks.""" 259 | HyperionClient._set_or_add_callbacks( 260 | default_callback, True, self._default_callback 261 | ) 262 | 263 | def remove_default_callback( 264 | self, 265 | default_callback: HyperionCallback | Iterable[HyperionCallback], 266 | ) -> None: 267 | """Set the default callbacks.""" 268 | HyperionClient._remove_callbacks(default_callback, self._default_callback) 269 | 270 | async def _call_callbacks(self, command: str, arg: dict[str, Any]) -> None: 271 | """Call the relevant callbacks for the given command.""" 272 | callbacks = self._callbacks.get(command, self._default_callback) 273 | for callback in callbacks: 274 | if inspect.iscoroutinefunction(callback): 275 | await cast(Awaitable[None], callback(arg)) 276 | else: 277 | callback(arg) 278 | 279 | # =================== 280 | # || Networking || 281 | # =================== 282 | 283 | @property 284 | def is_connected(self) -> bool: 285 | """Return server availability.""" 286 | return bool(self._client_state.get(const.KEY_CONNECTED)) 287 | 288 | @property 289 | def is_logged_in(self) -> bool: 290 | """Return whether the client is logged in.""" 291 | return bool(self._client_state.get(const.KEY_LOGGED_IN)) 292 | 293 | @property 294 | def instance(self) -> int | None: 295 | """Return server instance.""" 296 | instance: int | None = self._client_state.get(const.KEY_INSTANCE) 297 | return instance 298 | 299 | @property 300 | def target_instance(self) -> int: 301 | """Return server target instance.""" 302 | return self._target_instance 303 | 304 | @property 305 | def has_loaded_state(self) -> bool: 306 | """Return whether the client has loaded state.""" 307 | return bool(self._client_state.get(const.KEY_LOADED_STATE)) 308 | 309 | @property 310 | def client_state(self) -> dict[str, Any]: 311 | """Return client state.""" 312 | return self._client_state.get_all() 313 | 314 | @property 315 | def host(self) -> str: 316 | """Return host ip.""" 317 | return self._host 318 | 319 | @property 320 | def remote_url(self) -> str: 321 | """Return remote control URL.""" 322 | return f"http://{self._host}:{const.DEFAULT_PORT_UI}/#remote" 323 | 324 | async def async_client_connect(self) -> bool: 325 | """Connect to the Hyperion server.""" 326 | 327 | if self._writer: 328 | return True 329 | 330 | future_streams = asyncio.open_connection(self._host, self._port) 331 | try: 332 | self._reader, self._writer = await asyncio.wait_for( 333 | future_streams, timeout=self._timeout_secs 334 | ) 335 | except (asyncio.TimeoutError, ConnectionError, OSError) as exc: 336 | _LOGGER.debug("Could not connect to (%s): %s", self._host_port, repr(exc)) 337 | return False 338 | 339 | _LOGGER.info("Connected to Hyperion server: %s", self._host_port) 340 | 341 | # Start the receive task to process inbound data from the server. 342 | await self._await_or_stop_task(self._receive_task, stop_task=True) 343 | self._receive_task = asyncio.create_task(self._receive_task_loop()) 344 | 345 | await self._client_state_reset() 346 | self._client_state.update( 347 | {const.KEY_CONNECTED: True, const.KEY_INSTANCE: const.DEFAULT_INSTANCE} 348 | ) 349 | await self._call_client_state_callback_if_necessary() 350 | 351 | if not self._raw_connection: 352 | if ( 353 | not self._client_state.get(const.KEY_LOGGED_IN) 354 | and not await self.async_client_login() 355 | ): 356 | await self.async_client_disconnect() 357 | return False 358 | 359 | if ( 360 | not self._client_state.get(const.KEY_INSTANCE) 361 | and not await self.async_client_switch_instance() 362 | ): 363 | await self.async_client_disconnect() 364 | return False 365 | 366 | if not self._client_state.get( 367 | const.KEY_LOADED_STATE 368 | ) and not ServerInfoResponseOK(await self.async_get_serverinfo()): 369 | await self.async_client_disconnect() 370 | return False 371 | 372 | # Start the maintenance task if it does not already exist. 373 | if not self._maintenance_task: 374 | self._maintenance_task = asyncio.create_task(self._maintenance_task_loop()) 375 | 376 | return True 377 | 378 | async def _call_client_state_callback_if_necessary(self) -> None: 379 | """Call the client state callbacks if state has changed.""" 380 | if not self._client_state.dirty: 381 | return 382 | data = HyperionClient._set_data( 383 | self._client_state.get_all(), 384 | hard={ 385 | const.KEY_COMMAND: f"{const.KEY_CLIENT}-{const.KEY_UPDATE}", 386 | }, 387 | ) 388 | await self._call_callbacks(str(data[const.KEY_COMMAND]), data) 389 | self._client_state.dirty = False 390 | 391 | async def async_client_login(self) -> bool: 392 | """Log the client in if a token is provided.""" 393 | if self._token is None: 394 | self._client_state.set(const.KEY_LOGGED_IN, True) 395 | await self._call_client_state_callback_if_necessary() 396 | return True 397 | return bool(LoginResponseOK(await self.async_login(token=self._token))) 398 | 399 | async def async_client_switch_instance(self) -> bool: 400 | """Select an instance the user has specified.""" 401 | if ( 402 | self._client_state.get(const.KEY_INSTANCE) is None 403 | and self._target_instance == const.DEFAULT_INSTANCE 404 | ) or self._client_state.get(const.KEY_INSTANCE) == self._target_instance: 405 | self._client_state.set(const.KEY_INSTANCE, self._target_instance) 406 | await self._call_client_state_callback_if_necessary() 407 | return True 408 | 409 | resp_json = await self.async_switch_instance(instance=self._target_instance) 410 | return ( 411 | resp_json is not None 412 | and bool(SwitchInstanceResponseOK(resp_json)) 413 | and resp_json[const.KEY_INFO][const.KEY_INSTANCE] == self._target_instance 414 | ) 415 | 416 | async def async_client_disconnect(self) -> bool: 417 | """Close streams to the Hyperion server (no reconnect).""" 418 | # Cancel the maintenance task to ensure the connection is not re-established. 419 | await self._await_or_stop_task(self._maintenance_task, stop_task=True) 420 | self._maintenance_task = None 421 | 422 | return await self._async_client_disconnect_internal() 423 | 424 | async def _async_client_disconnect_internal(self) -> bool: 425 | """Close streams to the Hyperion server (may reconnect).""" 426 | if not self._writer: 427 | return True 428 | 429 | error = False 430 | writer = self._writer 431 | self._writer = self._reader = None 432 | try: 433 | writer.close() 434 | await writer.wait_closed() 435 | except ConnectionError as exc: 436 | _LOGGER.warning( 437 | "Could not close connection cleanly for Hyperion (%s): %s", 438 | self._host_port, 439 | repr(exc), 440 | ) 441 | error = True 442 | 443 | await self._client_state_reset() 444 | await self._call_client_state_callback_if_necessary() 445 | 446 | # Tell the maintenance loop it may need to reconnect. 447 | self._maintenance_event.set() 448 | 449 | receive_task = self._receive_task 450 | self._receive_task = None 451 | await self._await_or_stop_task(receive_task, stop_task=True) 452 | return not error 453 | 454 | async def _async_send_json(self, request: dict[str, Any]) -> bool: 455 | """Send JSON to the server.""" 456 | if not self._writer: 457 | return False 458 | 459 | output = json.dumps(request, sort_keys=True).encode("UTF-8") + b"\n" 460 | _LOGGER.debug("Send to server (%s): %s", self._host_port, output) 461 | try: 462 | self._writer.write(output) 463 | await self._writer.drain() 464 | except ConnectionError as exc: 465 | _LOGGER.warning( 466 | "Could not write data for Hyperion (%s): %s", 467 | self._host_port, 468 | repr(exc), 469 | ) 470 | return False 471 | return True 472 | 473 | # pylint: disable=too-many-return-statements 474 | async def _async_safely_read_command( 475 | self, use_timeout: bool = True 476 | ) -> dict[str, Any] | None: 477 | """Safely read a command from the stream.""" 478 | if not self._reader: 479 | return None 480 | 481 | timeout_secs = self._timeout_secs if use_timeout else None 482 | 483 | try: 484 | future_resp = self._reader.readline() 485 | resp = await asyncio.wait_for(future_resp, timeout=timeout_secs) 486 | except ConnectionError: 487 | _LOGGER.warning("Connection to Hyperion lost (%s) ...", self._host_port) 488 | await self._async_client_disconnect_internal() 489 | return None 490 | except asyncio.TimeoutError: 491 | _LOGGER.warning( 492 | "Read from Hyperion timed out (%s), disconnecting ...", self._host_port 493 | ) 494 | await self._async_client_disconnect_internal() 495 | return None 496 | 497 | if not resp: 498 | # If there's no writer, we have disconnected, so skip the error message 499 | # and additional disconnect call. 500 | if self._writer: 501 | _LOGGER.warning("Connection to Hyperion lost (%s) ...", self._host_port) 502 | await self._async_client_disconnect_internal() 503 | return None 504 | 505 | _LOGGER.debug("Read from server (%s): %s", self._host_port, resp) 506 | 507 | try: 508 | resp_json = json.loads(resp) 509 | except json.decoder.JSONDecodeError: 510 | _LOGGER.warning( 511 | "Could not decode JSON from Hyperion (%s), skipping...", 512 | self._host_port, 513 | ) 514 | return None 515 | 516 | try: 517 | resp_json = dict(resp_json) 518 | except ValueError: 519 | _LOGGER.warning( 520 | "Wrong data-type received from Hyperion (%s), skipping...", 521 | self._host_port, 522 | ) 523 | return None 524 | 525 | if const.KEY_COMMAND not in resp_json: 526 | _LOGGER.warning( 527 | "JSON from Hyperion (%s) did not include expected '%s' " 528 | "parameter, skipping...", 529 | self._host_port, 530 | const.KEY_COMMAND, 531 | ) 532 | return None 533 | return cast(Dict[str, Any], resp_json) 534 | 535 | async def _maintenance_task_loop(self) -> None: 536 | try: 537 | while True: 538 | await self._maintenance_event.wait() 539 | if not self._client_state.get(const.KEY_CONNECTED): 540 | if not await self.async_client_connect(): 541 | _LOGGER.info( 542 | "Could not estalish valid connection to Hyperion (%s), " 543 | "retrying in %i seconds...", 544 | self._host_port, 545 | self._retry_secs, 546 | ) 547 | await self._async_client_disconnect_internal() 548 | await asyncio.sleep(const.DEFAULT_CONNECTION_RETRY_DELAY_SECS) 549 | continue 550 | 551 | if self._client_state.get(const.KEY_CONNECTED): 552 | self._maintenance_event.clear() 553 | 554 | except asyncio.CancelledError: # pylint: disable=try-except-raise 555 | # Don't log CancelledError, but do propagate it upwards. 556 | raise 557 | except Exception: 558 | # Make sure exceptions are logged (for testing purposes, as this is 559 | # in a background task). 560 | _LOGGER.exception( 561 | "Exception in Hyperion (%s) background maintenance task", 562 | self._host_port, 563 | ) 564 | raise 565 | 566 | async def _receive_task_loop(self) -> None: 567 | """Run receive task continually.""" 568 | while await self._async_receive_once(): 569 | pass 570 | 571 | async def _await_or_stop_task( 572 | self, task: "asyncio.Task[Any] | None", stop_task: bool = False 573 | ) -> bool: 574 | """Await task, optionally stopping it first. 575 | 576 | Returns True if the task is done. 577 | """ 578 | if task is None: 579 | return False 580 | if stop_task: 581 | task.cancel() 582 | 583 | # Yield to the event loop, so the above cancellation can be processed. 584 | await asyncio.sleep(0) 585 | 586 | if task.done(): 587 | try: 588 | await task 589 | except asyncio.CancelledError: 590 | pass 591 | return True 592 | return False 593 | 594 | async def _handle_changed_instance(self, instance: int) -> None: 595 | """Handle when instance changes (whether this client triggered that or not).""" 596 | if instance == self._client_state.get(const.KEY_INSTANCE): 597 | return 598 | self._target_instance = instance 599 | self._client_state.update( 600 | {const.KEY_INSTANCE: instance, const.KEY_LOADED_STATE: False} 601 | ) 602 | self._update_serverinfo(None) 603 | await self._call_client_state_callback_if_necessary() 604 | 605 | # pylint: disable=too-many-branches 606 | async def _async_receive_once(self) -> bool: 607 | """Manage the bidirectional connection to the server.""" 608 | resp_json = await self._async_safely_read_command(use_timeout=False) 609 | if not resp_json: 610 | return False 611 | command = resp_json[const.KEY_COMMAND] 612 | 613 | if not resp_json.get(const.KEY_SUCCESS, True): 614 | # If it's a failed authorization call, print a specific warning 615 | # message. 616 | if command == const.KEY_AUTHORIZE_LOGIN: 617 | _LOGGER.warning( 618 | "Authorization failed for Hyperion (%s). " 619 | "Check token is valid: %s", 620 | self._host_port, 621 | resp_json, 622 | ) 623 | else: 624 | _LOGGER.warning( 625 | "Failed Hyperion (%s) command: %s", self._host_port, resp_json 626 | ) 627 | elif ( 628 | command == f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}" 629 | and const.KEY_DATA in resp_json 630 | ): 631 | self._update_component(resp_json[const.KEY_DATA]) 632 | elif ( 633 | command == f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}" 634 | and const.KEY_DATA in resp_json 635 | ): 636 | self._update_adjustment(resp_json[const.KEY_DATA]) 637 | elif ( 638 | command == f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}" 639 | and const.KEY_DATA in resp_json 640 | ): 641 | self._update_effects(resp_json[const.KEY_DATA]) 642 | elif command == f"{const.KEY_PRIORITIES}-{const.KEY_UPDATE}": 643 | if const.KEY_PRIORITIES in resp_json.get(const.KEY_DATA, {}): 644 | self._update_priorities(resp_json[const.KEY_DATA][const.KEY_PRIORITIES]) 645 | if const.KEY_PRIORITIES_AUTOSELECT in resp_json.get(const.KEY_DATA, {}): 646 | self._update_priorities_autoselect( 647 | resp_json[const.KEY_DATA][const.KEY_PRIORITIES_AUTOSELECT] 648 | ) 649 | elif ( 650 | command == f"{const.KEY_INSTANCE}-{const.KEY_UPDATE}" 651 | and const.KEY_DATA in resp_json 652 | ): 653 | # If instances are changed, and the current instance is not listed 654 | # in the new instance update, then the client should disconnect. 655 | instances = resp_json[const.KEY_DATA] 656 | 657 | for instance in instances: 658 | if ( 659 | instance.get(const.KEY_INSTANCE) 660 | == self._client_state.get(const.KEY_INSTANCE) 661 | and instance.get(const.KEY_RUNNING) is True 662 | ): 663 | self._update_instances(instances) 664 | break 665 | else: 666 | await self.async_client_disconnect() 667 | elif SwitchInstanceResponseOK(resp_json): 668 | # Upon connection being successfully switched to another instance, 669 | # the client will receive: 670 | # 671 | # { 672 | # "command":"instance-switchTo", 673 | # "info": { 674 | # "instance": 1 675 | # }, 676 | # "success":true, 677 | # "tan":0 678 | # } 679 | # 680 | # This is our cue to fully refresh our serverinfo so our internal 681 | # state is representing the correct instance. 682 | await self._handle_changed_instance( 683 | resp_json[const.KEY_INFO][const.KEY_INSTANCE] 684 | ) 685 | elif ( 686 | command == f"{const.KEY_LED_MAPPING}-{const.KEY_UPDATE}" 687 | and const.KEY_LED_MAPPING_TYPE in resp_json.get(const.KEY_DATA, {}) 688 | ): 689 | self._update_led_mapping_type( 690 | resp_json[const.KEY_DATA][const.KEY_LED_MAPPING_TYPE] 691 | ) 692 | elif ( 693 | command == f"{const.KEY_SESSIONS}-{const.KEY_UPDATE}" 694 | and const.KEY_DATA in resp_json 695 | ): 696 | self._update_sessions(resp_json[const.KEY_DATA]) 697 | elif ( 698 | command == f"{const.KEY_VIDEOMODE}-{const.KEY_UPDATE}" 699 | and const.KEY_VIDEOMODE in resp_json.get(const.KEY_DATA, {}) 700 | ): 701 | self._update_videomode(resp_json[const.KEY_DATA][const.KEY_VIDEOMODE]) 702 | elif ( 703 | command == f"{const.KEY_LEDS}-{const.KEY_UPDATE}" 704 | and const.KEY_LEDS in resp_json.get(const.KEY_DATA, {}) 705 | ): 706 | self._update_leds(resp_json[const.KEY_DATA][const.KEY_LEDS]) 707 | elif command == f"{const.KEY_AUTHORIZE_LOGOUT}": 708 | await self.async_client_disconnect() 709 | elif ServerInfoResponseOK(resp_json): 710 | self._update_serverinfo(resp_json[const.KEY_INFO]) 711 | self._client_state.set(const.KEY_LOADED_STATE, True) 712 | elif LoginResponseOK(resp_json): 713 | self._client_state.set(const.KEY_LOGGED_IN, True) 714 | 715 | await self._call_callbacks(command, resp_json) 716 | await self._call_client_state_callback_if_necessary() 717 | await self._handle_response_for_caller(resp_json) 718 | return True 719 | 720 | async def _handle_response_for_caller(self, resp_json: dict[str, Any]) -> None: 721 | """Handle a server response for a caller.""" 722 | 723 | tan = resp_json.get(const.KEY_TAN) 724 | if tan is not None: 725 | async with self._tan_cv: 726 | if tan in self._tan_responses: 727 | self._tan_responses[tan] = resp_json 728 | self._tan_cv.notify_all() 729 | # Note: The behavior is not perfect here, in cases of an older 730 | # Hyperion server and a malformed request. In that case, the 731 | # server will return tan==0 (regardless of the input tan), and 732 | # so the match here will fail. This will cause the callee to 733 | # time out awaiting a response (or wait forever if not timeout 734 | # is specified). This was addressed in: 735 | # 736 | # https://github.com/hyperion-project/hyperion.ng/issues/1001 . 737 | 738 | # ================== 739 | # || Helper calls || 740 | # ================== 741 | 742 | @property 743 | def _host_port(self) -> str: 744 | """Return a host:port string for this server.""" 745 | return "%s:%i" % (self._host, self._port) 746 | 747 | @classmethod 748 | def _set_data( 749 | cls, 750 | data: dict[Any, Any], 751 | hard: dict[Any, Any] | None = None, 752 | soft: dict[Any, Any] | None = None, 753 | ) -> dict[Any, Any]: 754 | """Override the data in the dictionary selectively.""" 755 | output = soft or {} 756 | output.update(data) 757 | output.update(hard or {}) 758 | return output 759 | 760 | async def _reserve_tan_slot(self, tan: int | None = None) -> int: 761 | """Increment and return the next tan to use.""" 762 | async with self._tan_cv: 763 | if tan is None: 764 | # If tan is not specified, find the next available higher 765 | # value. 766 | while self._tan_counter in self._tan_responses: 767 | self._tan_counter += 1 768 | tan = self._tan_counter 769 | self._tan_counter += 1 770 | if tan in self._tan_responses: 771 | raise HyperionClientTanNotAvailable( 772 | "Requested tan '%i' is not available in Hyperion client (%s)" 773 | % (tan, self._host_port) 774 | ) 775 | self._tan_responses[tan] = None 776 | return tan 777 | 778 | async def _remove_tan_slot(self, tan: int) -> None: 779 | """Remove a tan slot that is no longer required.""" 780 | async with self._tan_cv: 781 | if tan in self._tan_responses: 782 | del self._tan_responses[tan] 783 | 784 | async def _wait_for_tan_response( 785 | self, tan: int, timeout_secs: float 786 | ) -> dict[str, Any] | None: 787 | """Wait for a response to arrive.""" 788 | await self._tan_cv.acquire() 789 | try: 790 | await asyncio.wait_for( 791 | self._tan_cv.wait_for(lambda: self._tan_responses.get(tan) is not None), 792 | timeout=timeout_secs, 793 | ) 794 | return self._tan_responses[tan] 795 | except asyncio.TimeoutError: 796 | pass 797 | finally: 798 | # This should not be necessary, this function should be able to use 799 | # 'async with self._tan_cv', however this does not currently play nice 800 | # with Python 3.7/3.8 when the wait_for is canceled or times out, 801 | # (the condition lock is not reaquired before re-raising the exception). 802 | # See: https://bugs.python.org/issue39032 803 | if self._tan_cv.locked(): 804 | self._tan_cv.release() 805 | return None 806 | 807 | class AwaitResponseWrapper: 808 | """Wrapper an async *send* coroutine and await the response.""" 809 | 810 | def __init__( 811 | self, coro: Callable[..., Awaitable[bool]], timeout_secs: float = 0 812 | ): 813 | """Initialize the wrapper. 814 | 815 | Wait up to timeout_secs for a response. A timeout of 0 816 | will use the client default timeout specified in the constructor. 817 | A timeout of None will wait forever. 818 | """ 819 | self._coro = coro 820 | self._timeout_secs = timeout_secs 821 | 822 | def _extract_timeout_secs( 823 | self, hyperion_client: HyperionClient, data: dict[str, Any] 824 | ) -> float: 825 | """Return the timeout value for a call. 826 | 827 | Modifies input! Removes the timeout key from the inbound data if 828 | present so that it is not passed on to the server. If not present, 829 | returns the wrapper default specified in the wrapper constructor. 830 | """ 831 | # Timeout values: 832 | # * None: Wait forever (default asyncio.wait_for behavior). 833 | # * 0: Use the object default (self._timeout_secs) 834 | # * >0: Wait that long. 835 | if const.KEY_TIMEOUT_SECS in data: 836 | timeout_secs = cast(float, data[const.KEY_TIMEOUT_SECS]) 837 | del data[const.KEY_TIMEOUT_SECS] 838 | return timeout_secs 839 | if self._timeout_secs == 0: 840 | return hyperion_client._timeout_secs # pylint: disable=protected-access 841 | return self._timeout_secs 842 | 843 | async def __call__( 844 | self, hyperion_client: "HyperionClient", *args: Any, **kwargs: Any 845 | ) -> dict[str, Any] | None: 846 | """Call the wrapper.""" 847 | # The receive task should never be executing a call that uses the 848 | # AwaitResponseWrapper (as the response is itself handled by the receive 849 | # task, i.e. partial deadlock). This assertion defends against programmer 850 | # error in development of the client itself. 851 | assert asyncio.current_task() != hyperion_client._receive_task 852 | 853 | tan = await hyperion_client._reserve_tan_slot(kwargs.get(const.KEY_TAN)) 854 | data = hyperion_client._set_data(kwargs, hard={const.KEY_TAN: tan}) 855 | timeout_secs = self._extract_timeout_secs(hyperion_client, data) 856 | 857 | response = None 858 | if await self._coro(hyperion_client, *args, **data): 859 | response = await hyperion_client._wait_for_tan_response( 860 | tan, timeout_secs 861 | ) 862 | await hyperion_client._remove_tan_slot(tan) 863 | return response 864 | 865 | def __get__( 866 | self, instance: HyperionClient, instancetype: type[HyperionClient] 867 | ) -> functools.partial[Coroutine[Any, Any, dict[str, Any] | None]]: 868 | """Return a partial call that uses the correct 'self'.""" 869 | # Need to ensure __call__ receives the 'correct' outer 870 | # 'self', which is 'instance' in this function. 871 | return functools.partial(self.__call__, instance) 872 | 873 | # ============================= 874 | # || Authorization API calls || 875 | # ============================= 876 | 877 | # ================================================================================ 878 | # ** Authorization Check ** 879 | # https://docs.hyperion-project.org/en/json/Authorization.html#authorization-check 880 | # ================================================================================ 881 | 882 | async def async_send_is_auth_required(self, *_: Any, **kwargs: Any) -> bool: 883 | """Determine if authorization is required.""" 884 | data = HyperionClient._set_data( 885 | kwargs, 886 | hard={ 887 | const.KEY_COMMAND: const.KEY_AUTHORIZE, 888 | const.KEY_SUBCOMMAND: const.KEY_TOKEN_REQUIRED, 889 | }, 890 | ) 891 | return await self._async_send_json(data) 892 | 893 | async_is_auth_required = AwaitResponseWrapper(async_send_is_auth_required) 894 | 895 | # ============================================================================= 896 | # ** Login ** 897 | # https://docs.hyperion-project.org/en/json/Authorization.html#login-with-token 898 | # ============================================================================= 899 | 900 | async def async_send_login(self, *_: Any, **kwargs: Any) -> bool: 901 | """Login with token.""" 902 | data = HyperionClient._set_data( 903 | kwargs, 904 | hard={ 905 | const.KEY_COMMAND: const.KEY_AUTHORIZE, 906 | const.KEY_SUBCOMMAND: const.KEY_LOGIN, 907 | }, 908 | ) 909 | return await self._async_send_json(data) 910 | 911 | async_login = AwaitResponseWrapper(async_send_login) 912 | 913 | # ============================================================================= 914 | # ** Logout ** 915 | # https://docs.hyperion-project.org/en/json/Authorization.html#logout 916 | # ============================================================================= 917 | 918 | async def async_send_logout(self, *_: Any, **kwargs: Any) -> bool: 919 | """Logout.""" 920 | data = HyperionClient._set_data( 921 | kwargs, 922 | hard={ 923 | const.KEY_COMMAND: const.KEY_AUTHORIZE, 924 | const.KEY_SUBCOMMAND: const.KEY_LOGOUT, 925 | }, 926 | ) 927 | return await self._async_send_json(data) 928 | 929 | async_logout = AwaitResponseWrapper(async_send_logout) 930 | 931 | # ============================================================================ 932 | # ** Request Token ** 933 | # https://docs.hyperion-project.org/en/json/Authorization.html#request-a-token 934 | # ============================================================================ 935 | 936 | async def async_send_request_token(self, *_: Any, **kwargs: Any) -> bool: 937 | """Request an authorization token. 938 | 939 | The user will accept/deny the token request on the Web UI. 940 | """ 941 | data = HyperionClient._set_data( 942 | kwargs, 943 | hard={ 944 | const.KEY_COMMAND: const.KEY_AUTHORIZE, 945 | const.KEY_SUBCOMMAND: const.KEY_REQUEST_TOKEN, 946 | }, 947 | soft={const.KEY_ID: generate_random_auth_id()}, 948 | ) 949 | return await self._async_send_json(data) 950 | 951 | # This call uses a custom (longer) timeout by default, as the user needs to interact 952 | # with the Hyperion UI before it will return. 953 | async_request_token = AwaitResponseWrapper( 954 | async_send_request_token, timeout_secs=const.DEFAULT_REQUEST_TOKEN_TIMEOUT_SECS 955 | ) 956 | 957 | async def async_send_request_token_abort(self, *_: Any, **kwargs: Any) -> bool: 958 | """Abort a request for an authorization token.""" 959 | data = HyperionClient._set_data( 960 | kwargs, 961 | hard={ 962 | const.KEY_COMMAND: const.KEY_AUTHORIZE, 963 | const.KEY_SUBCOMMAND: const.KEY_REQUEST_TOKEN, 964 | const.KEY_ACCEPT: False, 965 | }, 966 | ) 967 | return await self._async_send_json(data) 968 | 969 | async_request_token_abort = AwaitResponseWrapper(async_send_request_token_abort) 970 | 971 | # ==================== 972 | # || Data API calls || 973 | # ==================== 974 | 975 | # ================ 976 | # ** Adjustment ** 977 | # ================ 978 | 979 | @property 980 | def adjustment(self) -> list[dict[str, Any]] | None: 981 | """Return adjustment.""" 982 | return self._get_serverinfo_value(const.KEY_ADJUSTMENT) 983 | 984 | def _update_adjustment(self, adjustment: list[dict[str, Any]] | None) -> None: 985 | """Update adjustment.""" 986 | if ( 987 | self._serverinfo is None 988 | or not adjustment 989 | or not isinstance(adjustment, list) 990 | ): 991 | return 992 | self._serverinfo[const.KEY_ADJUSTMENT] = adjustment 993 | 994 | async def async_send_set_adjustment(self, *_: Any, **kwargs: Any) -> bool: 995 | """Request that a color be set.""" 996 | data = HyperionClient._set_data( 997 | kwargs, hard={const.KEY_COMMAND: const.KEY_ADJUSTMENT} 998 | ) 999 | return await self._async_send_json(data) 1000 | 1001 | async_set_adjustment = AwaitResponseWrapper(async_send_set_adjustment) 1002 | 1003 | # ===================================================================== 1004 | # ** Clear ** 1005 | # Set: https://docs.hyperion-project.org/en/json/Control.html#clear 1006 | # ===================================================================== 1007 | 1008 | async def async_send_clear(self, *_: Any, **kwargs: Any) -> bool: 1009 | """Request that a priority be cleared.""" 1010 | data = HyperionClient._set_data( 1011 | kwargs, hard={const.KEY_COMMAND: const.KEY_CLEAR} 1012 | ) 1013 | return await self._async_send_json(data) 1014 | 1015 | async_clear = AwaitResponseWrapper(async_send_clear) 1016 | 1017 | # ===================================================================== 1018 | # ** Color ** 1019 | # Set: https://docs.hyperion-project.org/en/json/Control.html#set-color 1020 | # ===================================================================== 1021 | 1022 | async def async_send_set_color(self, *_: Any, **kwargs: Any) -> bool: 1023 | """Request that a color be set.""" 1024 | data = HyperionClient._set_data( 1025 | kwargs, 1026 | hard={const.KEY_COMMAND: const.KEY_COLOR}, 1027 | soft={const.KEY_ORIGIN: self._origin}, 1028 | ) 1029 | return await self._async_send_json(data) 1030 | 1031 | async_set_color = AwaitResponseWrapper(async_send_set_color) 1032 | 1033 | # ================================================================================== 1034 | # ** Component ** 1035 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#components 1036 | # Update: https://docs.hyperion-project.org/en/json/Subscribe.html#component-updates 1037 | # Set: https://docs.hyperion-project.org/en/json/Control.html#control-components 1038 | # ================================================================================== 1039 | 1040 | @property 1041 | def components(self) -> list[dict[str, Any]] | None: 1042 | """Return components.""" 1043 | return self._get_serverinfo_value(const.KEY_COMPONENTS) 1044 | 1045 | def _update_component(self, new_component: dict[str, Any]) -> None: 1046 | """Update full Hyperion state.""" 1047 | if ( 1048 | self._serverinfo is None 1049 | or not isinstance(new_component, dict) 1050 | or const.KEY_NAME not in new_component 1051 | ): 1052 | return 1053 | new_components = self._serverinfo.get(const.KEY_COMPONENTS, []) 1054 | for component in new_components: 1055 | if ( 1056 | const.KEY_NAME not in component 1057 | or component[const.KEY_NAME] != new_component[const.KEY_NAME] 1058 | ): 1059 | continue 1060 | # Update component in place. 1061 | component.clear() 1062 | component.update(new_component) 1063 | break 1064 | else: 1065 | new_components.append(new_component) 1066 | 1067 | async def async_send_set_component(self, *_: Any, **kwargs: Any) -> bool: 1068 | """Request that a color be set.""" 1069 | data = HyperionClient._set_data( 1070 | kwargs, hard={const.KEY_COMMAND: const.KEY_COMPONENTSTATE} 1071 | ) 1072 | return await self._async_send_json(data) 1073 | 1074 | async_set_component = AwaitResponseWrapper(async_send_set_component) 1075 | 1076 | def is_on( 1077 | self, 1078 | components: list[str] | None = None, 1079 | ) -> bool: 1080 | """Determine if components are on.""" 1081 | if components is None: 1082 | components = [const.KEY_COMPONENTID_ALL, const.KEY_COMPONENTID_LEDDEVICE] 1083 | elif not components: 1084 | return False 1085 | 1086 | components_to_state = {} 1087 | for component in self.components or []: 1088 | name = component.get(const.KEY_NAME) 1089 | state = component.get(const.KEY_ENABLED) 1090 | if name is None or state is None: 1091 | continue 1092 | components_to_state[name] = state 1093 | 1094 | for component_target in components: 1095 | if ( 1096 | component_target not in components_to_state 1097 | or not components_to_state[component_target] 1098 | ): 1099 | return False 1100 | return True 1101 | 1102 | # ================================================================================== 1103 | # ** Effects ** 1104 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#effect-list 1105 | # Update: https://docs.hyperion-project.org/en/json/Subscribe.html#effects-updates 1106 | # Set: https://docs.hyperion-project.org/en/json/Control.html#set-effect 1107 | # ================================================================================== 1108 | 1109 | @property 1110 | def effects(self) -> list[dict[str, Any]] | None: 1111 | """Return effects.""" 1112 | return self._get_serverinfo_value(const.KEY_EFFECTS) 1113 | 1114 | def _update_effects(self, effects: list[dict[str, Any]] | None) -> None: 1115 | """Update effects.""" 1116 | if self._serverinfo is None or not isinstance(effects, list): 1117 | return 1118 | self._serverinfo[const.KEY_EFFECTS] = effects 1119 | 1120 | async def async_send_set_effect(self, *_: Any, **kwargs: Any) -> bool: 1121 | """Request that an effect be set.""" 1122 | data = HyperionClient._set_data( 1123 | kwargs, 1124 | hard={const.KEY_COMMAND: const.KEY_EFFECT}, 1125 | soft={const.KEY_ORIGIN: self._origin}, 1126 | ) 1127 | return await self._async_send_json(data) 1128 | 1129 | async_set_effect = AwaitResponseWrapper(async_send_set_effect) 1130 | 1131 | # ================================================================================= 1132 | # ** Image ** 1133 | # Set: https://docs.hyperion-project.org/en/json/Control.html#set-image 1134 | # ================================================================================= 1135 | 1136 | async def async_send_set_image(self, *_: Any, **kwargs: Any) -> bool: 1137 | """Request that an image be set.""" 1138 | data = HyperionClient._set_data( 1139 | kwargs, 1140 | hard={const.KEY_COMMAND: const.KEY_IMAGE}, 1141 | soft={const.KEY_ORIGIN: self._origin}, 1142 | ) 1143 | return await self._async_send_json(data) 1144 | 1145 | async_set_image = AwaitResponseWrapper(async_send_set_image) 1146 | 1147 | # ================================================================================ 1148 | # ** Image Streaming ** 1149 | # Update: https://docs.hyperion-project.org/en/json/Control.html#live-image-stream 1150 | # Set: https://docs.hyperion-project.org/en/json/Control.html#live-image-stream 1151 | # ================================================================================ 1152 | 1153 | async def async_send_image_stream_start(self, *_: Any, **kwargs: Any) -> bool: 1154 | """Request a live image stream to start.""" 1155 | data = HyperionClient._set_data( 1156 | kwargs, 1157 | hard={ 1158 | const.KEY_COMMAND: const.KEY_LEDCOLORS, 1159 | const.KEY_SUBCOMMAND: const.KEY_IMAGE_STREAM_START, 1160 | }, 1161 | ) 1162 | return await self._async_send_json(data) 1163 | 1164 | async_image_stream_start = AwaitResponseWrapper(async_send_image_stream_start) 1165 | 1166 | async def async_send_image_stream_stop(self, *_: Any, **kwargs: Any) -> bool: 1167 | """Request a live image stream to stop.""" 1168 | data = HyperionClient._set_data( 1169 | kwargs, 1170 | hard={ 1171 | const.KEY_COMMAND: const.KEY_LEDCOLORS, 1172 | const.KEY_SUBCOMMAND: const.KEY_IMAGE_STREAM_STOP, 1173 | }, 1174 | ) 1175 | return await self._async_send_json(data) 1176 | 1177 | async_image_stream_stop = AwaitResponseWrapper(async_send_image_stream_stop) 1178 | 1179 | # ================================================================================= 1180 | # ** Instances ** 1181 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#instance 1182 | # Update: https://docs.hyperion-project.org/en/json/Subscribe.html#instance-updates 1183 | # Set: https://docs.hyperion-project.org/en/json/Control.html#control-instances 1184 | # ================================================================================= 1185 | 1186 | @property 1187 | def instances(self) -> list[dict[str, Any]] | None: 1188 | """Return instances.""" 1189 | return self._get_serverinfo_value(const.KEY_INSTANCE) 1190 | 1191 | def _update_instances(self, instances: list[dict[str, Any]] | None) -> None: 1192 | """Update instances.""" 1193 | if self._serverinfo is None or not isinstance(instances, list): 1194 | return 1195 | self._serverinfo[const.KEY_INSTANCE] = instances 1196 | 1197 | async def async_send_start_instance(self, *_: Any, **kwargs: Any) -> bool: 1198 | """Start an instance.""" 1199 | data = HyperionClient._set_data( 1200 | kwargs, 1201 | hard={ 1202 | const.KEY_COMMAND: const.KEY_INSTANCE, 1203 | const.KEY_SUBCOMMAND: const.KEY_START_INSTANCE, 1204 | }, 1205 | ) 1206 | return await self._async_send_json(data) 1207 | 1208 | async_start_instance = AwaitResponseWrapper(async_send_start_instance) 1209 | 1210 | async def async_send_stop_instance(self, *_: Any, **kwargs: Any) -> bool: 1211 | """Stop an instance.""" 1212 | data = HyperionClient._set_data( 1213 | kwargs, 1214 | hard={ 1215 | const.KEY_COMMAND: const.KEY_INSTANCE, 1216 | const.KEY_SUBCOMMAND: const.KEY_STOP_INSTANCE, 1217 | }, 1218 | ) 1219 | return await self._async_send_json(data) 1220 | 1221 | async_stop_instance = AwaitResponseWrapper(async_send_stop_instance) 1222 | 1223 | async def async_send_switch_instance(self, *_: Any, **kwargs: Any) -> bool: 1224 | """Stop an instance.""" 1225 | data = HyperionClient._set_data( 1226 | kwargs, 1227 | hard={ 1228 | const.KEY_COMMAND: const.KEY_INSTANCE, 1229 | const.KEY_SUBCOMMAND: const.KEY_SWITCH_TO, 1230 | }, 1231 | ) 1232 | return await self._async_send_json(data) 1233 | 1234 | async_switch_instance = AwaitResponseWrapper(async_send_switch_instance) 1235 | 1236 | # ============================================================================= 1237 | # ** LEDs ** 1238 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#leds 1239 | # Update: https://docs.hyperion-project.org/en/json/Subscribe.html#leds-updates 1240 | # ============================================================================= 1241 | 1242 | @property 1243 | def leds(self) -> list[dict[str, Any]] | None: 1244 | """Return LEDs.""" 1245 | return self._get_serverinfo_value(const.KEY_LEDS) 1246 | 1247 | def _update_leds(self, leds: list[dict[str, Any]] | None) -> None: 1248 | """Update LEDs.""" 1249 | if self._serverinfo is None or not isinstance(leds, list): 1250 | return 1251 | self._serverinfo[const.KEY_LEDS] = leds 1252 | 1253 | # pylint: disable=line-too-long 1254 | # ================================================================================= 1255 | # ** LED Mapping ** 1256 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#led-mapping 1257 | # Update: https://docs.hyperion-project.org/en/json/Subscribe.html#led-mapping-updates 1258 | # Set: https://docs.hyperion-project.org/en/json/Control.html#led-mapping 1259 | # ================================================================================= 1260 | 1261 | @property 1262 | def led_mapping_type(self) -> str | None: 1263 | """Return LED mapping type.""" 1264 | return self._get_serverinfo_value(const.KEY_LED_MAPPING_TYPE) 1265 | 1266 | def _update_led_mapping_type(self, led_mapping_type: str) -> None: 1267 | """Update LED mapping type.""" 1268 | if self._serverinfo is None or not isinstance(led_mapping_type, str): 1269 | return 1270 | self._serverinfo[const.KEY_LED_MAPPING_TYPE] = led_mapping_type 1271 | 1272 | async def async_send_set_led_mapping_type(self, *_: Any, **kwargs: Any) -> bool: 1273 | """Request the LED mapping type be set.""" 1274 | data = HyperionClient._set_data( 1275 | kwargs, hard={const.KEY_COMMAND: const.KEY_PROCESSING} 1276 | ) 1277 | return await self._async_send_json(data) 1278 | 1279 | async_set_led_mapping_type = AwaitResponseWrapper(async_send_set_led_mapping_type) 1280 | 1281 | # ================================================================================= 1282 | # ** Live LED Streaming ** 1283 | # Update: https://docs.hyperion-project.org/en/json/Control.html#live-led-color-stream 1284 | # Set: https://docs.hyperion-project.org/en/json/Control.html#live-led-color-stream 1285 | # ================================================================================= 1286 | 1287 | async def async_send_led_stream_start(self, *_: Any, **kwargs: Any) -> bool: 1288 | """Request a live led stream to start.""" 1289 | data = HyperionClient._set_data( 1290 | kwargs, 1291 | hard={ 1292 | const.KEY_COMMAND: const.KEY_LEDCOLORS, 1293 | const.KEY_SUBCOMMAND: const.KEY_LED_STREAM_START, 1294 | }, 1295 | ) 1296 | return await self._async_send_json(data) 1297 | 1298 | async_led_stream_start = AwaitResponseWrapper(async_send_led_stream_start) 1299 | 1300 | async def async_send_led_stream_stop(self, *_: Any, **kwargs: Any) -> bool: 1301 | """Request a live led stream to stop.""" 1302 | data = HyperionClient._set_data( 1303 | kwargs, 1304 | hard={ 1305 | const.KEY_COMMAND: const.KEY_LEDCOLORS, 1306 | const.KEY_SUBCOMMAND: const.KEY_LED_STREAM_STOP, 1307 | }, 1308 | ) 1309 | return await self._async_send_json(data) 1310 | 1311 | async_led_stream_stop = AwaitResponseWrapper(async_send_led_stream_stop) 1312 | 1313 | # ================================================================================= 1314 | # ** Priorites ** 1315 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities 1316 | # Update: https://docs.hyperion-project.org/en/json/Subscribe.html#priority-updates 1317 | # ================================================================================= 1318 | 1319 | @property 1320 | def priorities(self) -> list[dict[str, Any]] | None: 1321 | """Return priorites.""" 1322 | return self._get_serverinfo_value(const.KEY_PRIORITIES) 1323 | 1324 | def _update_priorities(self, priorities: list[dict[str, Any]]) -> None: 1325 | """Update priorites.""" 1326 | if self._serverinfo is None or not isinstance(priorities, list): 1327 | return 1328 | self._serverinfo[const.KEY_PRIORITIES] = priorities 1329 | 1330 | @property 1331 | def visible_priority(self) -> dict[str, Any] | None: 1332 | """Return the visible priority, if any.""" 1333 | # The visible priority is supposed to be the first returned by the 1334 | # API, but due to a bug the ordering is incorrect search for it 1335 | # instead, see: 1336 | # https://github.com/hyperion-project/hyperion.ng/issues/964 1337 | for priority in self.priorities or []: 1338 | if priority.get(const.KEY_VISIBLE, False): 1339 | return priority 1340 | return None 1341 | 1342 | # ================================================================================= 1343 | # ** Priorites Autoselect ** 1344 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities-selection-auto-manual 1345 | # Update: https://docs.hyperion-project.org/en/json/Subscribe.html#priority-updates 1346 | # Set: https://docs.hyperion-project.org/en/json/Control.html#source-selection 1347 | # ================================================================================= 1348 | 1349 | @property 1350 | def priorities_autoselect(self) -> bool | None: 1351 | """Return priorites.""" 1352 | return self._get_serverinfo_value(const.KEY_PRIORITIES_AUTOSELECT) 1353 | 1354 | def _update_priorities_autoselect(self, priorities_autoselect: bool) -> None: 1355 | """Update priorites.""" 1356 | if self._serverinfo is None or not isinstance(priorities_autoselect, bool): 1357 | return 1358 | self._serverinfo[const.KEY_PRIORITIES_AUTOSELECT] = priorities_autoselect 1359 | 1360 | async def async_send_set_sourceselect(self, *_: Any, **kwargs: Any) -> bool: 1361 | """Request the sourceselect be set.""" 1362 | data = HyperionClient._set_data( 1363 | kwargs, hard={const.KEY_COMMAND: const.KEY_SOURCESELECT} 1364 | ) 1365 | return await self._async_send_json(data) 1366 | 1367 | async_set_sourceselect = AwaitResponseWrapper(async_send_set_sourceselect) 1368 | 1369 | # ================================================================================ 1370 | # ** Sessions ** 1371 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#sessions 1372 | # Update: https://docs.hyperion-project.org/en/json/Subscribe.html#session-updates 1373 | # ================================================================================ 1374 | 1375 | @property 1376 | def sessions(self) -> list[dict[str, Any]] | None: 1377 | """Return sessions.""" 1378 | return self._get_serverinfo_value(const.KEY_SESSIONS) 1379 | 1380 | def _update_sessions(self, sessions: list[dict[str, Any]]) -> None: 1381 | """Update sessions.""" 1382 | if self._serverinfo is None or not isinstance(sessions, list): 1383 | return 1384 | self._serverinfo[const.KEY_SESSIONS] = sessions 1385 | 1386 | # ===================================================================== 1387 | # ** Serverinfo (full state) ** 1388 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html 1389 | # ===================================================================== 1390 | 1391 | @property 1392 | def serverinfo(self) -> dict[str, Any] | None: 1393 | """Return current serverinfo.""" 1394 | return self._serverinfo 1395 | 1396 | def _update_serverinfo(self, state: dict[str, Any] | None) -> None: 1397 | """Update full Hyperion state.""" 1398 | self._serverinfo = state 1399 | 1400 | def _get_serverinfo_value(self, key: str) -> Any | None: 1401 | """Get a value from serverinfo structure given key.""" 1402 | if not self._serverinfo: 1403 | return None 1404 | return self._serverinfo.get(key) 1405 | 1406 | async def async_send_get_serverinfo(self, *_: Any, **kwargs: Any) -> bool: 1407 | """Server a serverinfo full state/subscription request.""" 1408 | # Request full state ('serverinfo') and subscribe to relevant 1409 | # future updates to keep this object state accurate without the need to 1410 | # poll. 1411 | data = HyperionClient._set_data( 1412 | kwargs, 1413 | hard={ 1414 | const.KEY_COMMAND: const.KEY_SERVERINFO, 1415 | const.KEY_SUBSCRIBE: [ 1416 | f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}", 1417 | f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}", 1418 | f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}", 1419 | f"{const.KEY_LEDS}-{const.KEY_UPDATE}", 1420 | f"{const.KEY_LED_MAPPING}-{const.KEY_UPDATE}", 1421 | f"{const.KEY_INSTANCE}-{const.KEY_UPDATE}", 1422 | f"{const.KEY_PRIORITIES}-{const.KEY_UPDATE}", 1423 | f"{const.KEY_SESSIONS}-{const.KEY_UPDATE}", 1424 | f"{const.KEY_VIDEOMODE}-{const.KEY_UPDATE}", 1425 | ], 1426 | }, 1427 | ) 1428 | return await self._async_send_json(data) 1429 | 1430 | async_get_serverinfo = AwaitResponseWrapper(async_send_get_serverinfo) 1431 | 1432 | # ================================================================================== 1433 | # ** Videomode ** 1434 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#video-mode 1435 | # Update: https://docs.hyperion-project.org/en/json/Subscribe.html#videomode-updates 1436 | # Set: https://docs.hyperion-project.org/en/json/Control.html#video-mode 1437 | # ================================================================================== 1438 | 1439 | @property 1440 | def videomode(self) -> str | None: 1441 | """Return videomode.""" 1442 | return self._get_serverinfo_value(const.KEY_VIDEOMODE) 1443 | 1444 | def _update_videomode(self, videomode: str) -> None: 1445 | """Update videomode.""" 1446 | if self._serverinfo: 1447 | self._serverinfo[const.KEY_VIDEOMODE] = videomode 1448 | 1449 | async def async_send_set_videomode(self, *_: Any, **kwargs: Any) -> bool: 1450 | """Request the LED mapping type be set.""" 1451 | data = HyperionClient._set_data( 1452 | kwargs, hard={const.KEY_COMMAND: const.KEY_VIDEOMODE} 1453 | ) 1454 | return await self._async_send_json(data) 1455 | 1456 | async_set_videomode = AwaitResponseWrapper(async_send_set_videomode) 1457 | 1458 | # ================================================================================== 1459 | # ** Sysinfo ** 1460 | # Full State: https://docs.hyperion-project.org/en/json/ServerInfo.html#system-hyperion 1461 | # Returns system information from the Hyperion instance. 1462 | # ================================================================================== 1463 | 1464 | async def async_send_sysinfo(self, *_: Any, **kwargs: Any) -> bool: 1465 | """Request the sysinfo.""" 1466 | data = HyperionClient._set_data( 1467 | kwargs, hard={const.KEY_COMMAND: const.KEY_SYSINFO} 1468 | ) 1469 | return await self._async_send_json(data) 1470 | 1471 | async_sysinfo = AwaitResponseWrapper(async_send_sysinfo) 1472 | 1473 | async def async_sysinfo_id(self) -> str | None: 1474 | """Return an ID representing this Hyperion server.""" 1475 | sysinfo = await self.async_sysinfo() 1476 | if sysinfo is not None and ResponseOK(sysinfo): 1477 | sysinfo_id = ( 1478 | sysinfo.get(const.KEY_INFO, {}) 1479 | .get(const.KEY_HYPERION, {}) 1480 | .get(const.KEY_ID, None) 1481 | ) 1482 | if not sysinfo_id or not isinstance(sysinfo_id, str): 1483 | return None 1484 | return str(sysinfo_id) 1485 | return None 1486 | 1487 | async def async_sysinfo_version(self) -> str | None: 1488 | """Return the Hyperion server version.""" 1489 | sysinfo = await self.async_sysinfo() 1490 | if sysinfo is not None and ResponseOK(sysinfo): 1491 | sysinfo_version = ( 1492 | sysinfo.get(const.KEY_INFO, {}) 1493 | .get(const.KEY_HYPERION, {}) 1494 | .get(const.KEY_VERSION, None) 1495 | ) 1496 | if not sysinfo_version or not isinstance(sysinfo_version, str): 1497 | return None 1498 | return str(sysinfo_version) 1499 | return None 1500 | 1501 | 1502 | class ThreadedHyperionClient(threading.Thread): 1503 | """Hyperion Client that runs in a dedicated thread.""" 1504 | 1505 | # pylint: disable=too-many-arguments 1506 | def __init__( 1507 | self, 1508 | host: str, 1509 | port: int = const.DEFAULT_PORT_JSON, 1510 | default_callback: HyperionCallback | Iterable[HyperionCallback] | None = None, 1511 | callbacks: dict[str, HyperionCallback | Iterable[HyperionCallback]] 1512 | | None = None, 1513 | token: str | None = None, 1514 | instance: int = const.DEFAULT_INSTANCE, 1515 | origin: str = const.DEFAULT_ORIGIN, 1516 | timeout_secs: float = const.DEFAULT_TIMEOUT_SECS, 1517 | retry_secs: int = const.DEFAULT_CONNECTION_RETRY_DELAY_SECS, 1518 | raw_connection: bool = False, 1519 | ) -> None: 1520 | """Initialize client.""" 1521 | super().__init__() 1522 | self._loop: asyncio.AbstractEventLoop = asyncio.new_event_loop() 1523 | self._hyperion_client: HyperionClient | None = None 1524 | 1525 | self._client_init_call: Callable[[], HyperionClient] = lambda: HyperionClient( 1526 | host, 1527 | port, 1528 | default_callback=default_callback, 1529 | callbacks=callbacks, 1530 | token=token, 1531 | instance=instance, 1532 | origin=origin, 1533 | timeout_secs=timeout_secs, 1534 | retry_secs=retry_secs, 1535 | raw_connection=raw_connection, 1536 | ) 1537 | self._client_init_event = threading.Event() 1538 | 1539 | def wait_for_client_init(self) -> None: 1540 | """Block until the HyperionClient is ready to interact.""" 1541 | self._client_init_event.wait() 1542 | 1543 | async def _async_init_client(self) -> None: 1544 | """Initialize the client.""" 1545 | # Initialize the client in the new thread, using the new event loop. 1546 | # Some asyncio elements of the client (e.g. Conditions / Events) bind 1547 | # to asyncio.get_event_loop() on construction. 1548 | 1549 | self._hyperion_client = self._client_init_call() 1550 | 1551 | for name, value in inspect.getmembers( 1552 | self._hyperion_client, inspect.iscoroutinefunction 1553 | ): 1554 | if name.startswith("async_"): 1555 | new_name = name[len("async_") :] 1556 | self._register_async_call(new_name, value) 1557 | for name, value in inspect.getmembers( 1558 | type(self._hyperion_client), lambda o: isinstance(o, property) 1559 | ): 1560 | self._copy_property(name) 1561 | 1562 | def _copy_property(self, name: str) -> None: 1563 | """Register a property.""" 1564 | setattr( 1565 | type(self), name, property(lambda _: getattr(self._hyperion_client, name)) 1566 | ) 1567 | 1568 | def _register_async_call( 1569 | self, name: str, value: Callable[..., Awaitable[Any]] 1570 | ) -> None: 1571 | """Register a wrapped async call.""" 1572 | setattr( 1573 | self, 1574 | name, 1575 | lambda *args, **kwargs: self._async_wrapper(value, *args, **kwargs), 1576 | ) 1577 | 1578 | def _async_wrapper( 1579 | self, coro: Callable[..., Awaitable[Any]], *args: Any, **kwargs: Any 1580 | ) -> Any: 1581 | """Convert a async call to synchronous by running it in the local event loop.""" 1582 | future = asyncio.run_coroutine_threadsafe(coro(*args, **kwargs), self._loop) 1583 | return future.result() 1584 | 1585 | def __getattr__(self, name: str) -> Any: 1586 | """Override getattr to allow generous mypy treatment for dynamic methods.""" 1587 | return getattr(self, name) 1588 | 1589 | def stop(self) -> None: 1590 | """Stop the asyncio loop and thus the thread.""" 1591 | 1592 | def inner_stop() -> None: 1593 | asyncio.get_event_loop().stop() 1594 | 1595 | self._loop.call_soon_threadsafe(inner_stop) 1596 | 1597 | def run(self) -> None: 1598 | """Run the asyncio loop until stop is called.""" 1599 | asyncio.set_event_loop(self._loop) 1600 | asyncio.get_event_loop().run_until_complete(self._async_init_client()) 1601 | self._client_init_event.set() 1602 | asyncio.get_event_loop().run_forever() 1603 | asyncio.get_event_loop().close() 1604 | 1605 | 1606 | class ResponseOK: 1607 | """Small wrapper class around a server response.""" 1608 | 1609 | def __init__( 1610 | self, 1611 | response: dict[str, Any] | None, 1612 | cmd: str | None = None, 1613 | validators: list[Callable[[dict[str, Any]], bool]] | None = None, 1614 | ): 1615 | """Initialize a Response object.""" 1616 | self._response = response 1617 | self._cmd = cmd 1618 | self._validators = validators or [] 1619 | 1620 | def __bool__(self) -> bool: 1621 | """Determine if the response indicates success.""" 1622 | if not self._response: 1623 | return False 1624 | if not isinstance(self._response, dict): 1625 | return False # type: ignore[unreachable] 1626 | if not self._response.get(const.KEY_SUCCESS, False): 1627 | return False 1628 | if self._cmd is not None and self._response.get(const.KEY_COMMAND) != self._cmd: 1629 | return False 1630 | for validator in self._validators: 1631 | if not validator(self._response): 1632 | return False 1633 | return True 1634 | 1635 | 1636 | class ServerInfoResponseOK(ResponseOK): 1637 | """Wrapper class for ServerInfo responses.""" 1638 | 1639 | def __init__(self, response: dict[str, Any] | None): 1640 | """Initialize the wrapper class.""" 1641 | super().__init__( 1642 | response, 1643 | cmd=const.KEY_SERVERINFO, 1644 | validators=[lambda r: bool(r.get(const.KEY_INFO))], 1645 | ) 1646 | 1647 | 1648 | class LoginResponseOK(ResponseOK): 1649 | """Wrapper class for LoginResponse.""" 1650 | 1651 | def __init__(self, response: dict[str, Any] | None): 1652 | """Initialize the wrapper class.""" 1653 | super().__init__(response, cmd=const.KEY_AUTHORIZE_LOGIN) 1654 | 1655 | 1656 | class SwitchInstanceResponseOK(ResponseOK): 1657 | """Wrapper class for SwitchInstanceResponse.""" 1658 | 1659 | def __init__(self, response: dict[str, Any] | None): 1660 | """Initialize the wrapper class.""" 1661 | super().__init__( 1662 | response, 1663 | cmd=f"{const.KEY_INSTANCE}-{const.KEY_SWITCH_TO}", 1664 | validators=[ 1665 | lambda r: r.get(const.KEY_INFO, {}).get(const.KEY_INSTANCE) is not None 1666 | ], 1667 | ) 1668 | 1669 | 1670 | def generate_random_auth_id() -> str: 1671 | """Generate random authenticate ID.""" 1672 | return "".join( 1673 | random.choice(string.ascii_letters + string.digits) for i in range(0, 5) 1674 | ) 1675 | -------------------------------------------------------------------------------- /hyperion/const.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Hyperion Constants.""" 3 | 4 | KEY_ACCEPT = "accept" 5 | KEY_ACTIVE = "active" 6 | KEY_ADJUSTMENT = "adjustment" 7 | KEY_AUTHORIZE = "authorize" 8 | KEY_AUTHORIZE_LOGIN = "authorize-login" 9 | KEY_AUTHORIZE_LOGOUT = "authorize-logout" 10 | KEY_BRIGHTNESS = "brightness" 11 | KEY_CLEAR = "clear" 12 | KEY_CLIENT = "client" 13 | KEY_COLOR = "color" 14 | KEY_COMMAND = "command" 15 | KEY_COMPONENT = "component" 16 | KEY_COMPONENTSTATE = "componentstate" 17 | KEY_COMPONENTS = "components" 18 | KEY_CONNECTION = "connection" 19 | KEY_CONNECTED = "connected" 20 | KEY_DATA = "data" 21 | KEY_EFFECT = "effect" 22 | KEY_EFFECTS = "effects" 23 | KEY_ENABLED = "enabled" 24 | KEY_FRIENDLY_NAME = "friendly_name" 25 | KEY_HYPERION = "hyperion" 26 | KEY_LED_MAPPING = "imageToLedMapping" 27 | KEY_LED_MAPPING_TYPE = "imageToLedMappingType" 28 | KEY_ID = "id" 29 | KEY_IMAGE = "image" 30 | KEY_IMAGE_STREAM = "imagestream" 31 | KEY_IMAGE_STREAM_START = f"{KEY_IMAGE_STREAM}-start" 32 | KEY_IMAGE_STREAM_STOP = f"{KEY_IMAGE_STREAM}-stop" 33 | KEY_INFO = "info" 34 | KEY_INSTANCE = "instance" 35 | KEY_LEDCOLORS = "ledcolors" 36 | KEY_LED_STREAM_START = "ledstream-start" 37 | KEY_LED_STREAM_STOP = "ledstream-stop" 38 | KEY_LEDS = "leds" 39 | KEY_LED_MAPPING = "imageToLedMapping" 40 | KEY_LOADED_STATE = "loaded-state" 41 | KEY_LOGGED_IN = "logged-in" 42 | KEY_LOGIN = "login" 43 | KEY_LOGOUT = "logout" 44 | KEY_NAME = "name" 45 | KEY_ORIGIN = "origin" 46 | KEY_OWNER = "owner" 47 | KEY_PRIORITY = "priority" 48 | KEY_PRIORITIES = "priorities" 49 | KEY_PRIORITIES_AUTOSELECT = "priorities_autoselect" 50 | KEY_PROCESSING = "processing" 51 | KEY_RGB = "RGB" 52 | KEY_RESULT = "result" 53 | KEY_REQUIRED = "required" 54 | KEY_REQUEST_TOKEN = "requestToken" 55 | KEY_RUNNING = "running" 56 | KEY_SESSIONS = "sessions" 57 | KEY_SET_VIDEOMODE = "videoMode" 58 | KEY_SERVERINFO = "serverinfo" 59 | KEY_SOURCESELECT = "sourceselect" 60 | KEY_START_INSTANCE = "startInstance" 61 | KEY_STATE_LOADED = "startInstance" 62 | KEY_STOP_INSTANCE = "stopInstance" 63 | KEY_SUBCOMMAND = "subcommand" 64 | KEY_SUBSCRIBE = "subscribe" 65 | KEY_SUCCESS = "success" 66 | KEY_SWITCH_TO = "switchTo" 67 | KEY_STATE = "state" 68 | KEY_SYSINFO = "sysinfo" 69 | KEY_TAN = "tan" 70 | KEY_TIMEOUT_SECS = "timeout_secs" 71 | KEY_TOKEN = "token" 72 | KEY_TOKEN_REQUIRED = "tokenRequired" 73 | KEY_UPDATE = "update" 74 | KEY_VERSION = "version" 75 | KEY_VALUE = "value" 76 | KEY_VIDEOMODE = "videomode" 77 | KEY_VISIBLE = "visible" 78 | KEY_VIDEOMODES = ["2D", "3DSBS", "3DTAB"] 79 | 80 | # ComponentIDs from: 81 | # https://docs.hyperion-project.org/en/json/Control.html#components-ids-explained 82 | KEY_COMPONENTID = "componentId" 83 | KEY_COMPONENTID_ALL = "ALL" 84 | KEY_COMPONENTID_COLOR = "COLOR" 85 | KEY_COMPONENTID_EFFECT = "EFFECT" 86 | 87 | KEY_COMPONENTID_SMOOTHING = "SMOOTHING" 88 | KEY_COMPONENTID_BLACKBORDER = "BLACKBORDER" 89 | KEY_COMPONENTID_FORWARDER = "FORWARDER" 90 | KEY_COMPONENTID_BOBLIGHTSERVER = "BOBLIGHTSERVER" 91 | KEY_COMPONENTID_GRABBER = "GRABBER" 92 | KEY_COMPONENTID_AUDIO = "AUDIO" 93 | KEY_COMPONENTID_LEDDEVICE = "LEDDEVICE" 94 | KEY_COMPONENTID_V4L = "V4L" 95 | 96 | KEY_COMPONENTID_EXTERNAL_SOURCES = [ 97 | KEY_COMPONENTID_BOBLIGHTSERVER, 98 | KEY_COMPONENTID_GRABBER, 99 | KEY_COMPONENTID_AUDIO, 100 | KEY_COMPONENTID_V4L, 101 | ] 102 | 103 | # Maps between Hyperion API component names to Hyperion UI names. 104 | KEY_COMPONENTID_TO_NAME = { 105 | KEY_COMPONENTID_ALL: "All", 106 | KEY_COMPONENTID_SMOOTHING: "Smoothing", 107 | KEY_COMPONENTID_BLACKBORDER: "Blackbar Detection", 108 | KEY_COMPONENTID_FORWARDER: "Forwarder", 109 | KEY_COMPONENTID_BOBLIGHTSERVER: "Boblight Server", 110 | KEY_COMPONENTID_GRABBER: "Platform Capture", 111 | KEY_COMPONENTID_LEDDEVICE: "LED Device", 112 | KEY_COMPONENTID_AUDIO: "Audio Capture", 113 | KEY_COMPONENTID_V4L: "USB Capture", 114 | } 115 | KEY_COMPONENTID_FROM_NAME = { 116 | name: component for component, name in KEY_COMPONENTID_TO_NAME.items() 117 | } 118 | 119 | DEFAULT_INSTANCE = 0 120 | DEFAULT_CONNECTION_RETRY_DELAY_SECS = 30 121 | DEFAULT_TIMEOUT_SECS = 5 122 | DEFAULT_REQUEST_TOKEN_TIMEOUT_SECS = 180 123 | DEFAULT_ORIGIN = "hyperion-py" 124 | DEFAULT_PORT_JSON = 19444 125 | DEFAULT_PORT_UI = 8090 126 | -------------------------------------------------------------------------------- /hyperion/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dermotduffy/hyperion-py/1a51550d5729f1706070b8570ffb8987958a12fe/hyperion/py.typed -------------------------------------------------------------------------------- /images/hyperion-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dermotduffy/hyperion-py/1a51550d5729f1706070b8570ffb8987958a12fe/images/hyperion-logo.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aiohttp" 3 | version = "3.8.1" 4 | description = "Async http client/server framework (asyncio)" 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | aiosignal = ">=1.1.2" 11 | async-timeout = ">=4.0.0a3,<5.0" 12 | attrs = ">=17.3.0" 13 | charset-normalizer = ">=2.0,<3.0" 14 | frozenlist = ">=1.1.1" 15 | multidict = ">=4.5,<7.0" 16 | yarl = ">=1.0,<2.0" 17 | 18 | [package.extras] 19 | speedups = ["aiodns", "brotli", "cchardet"] 20 | 21 | [[package]] 22 | name = "aiosignal" 23 | version = "1.2.0" 24 | description = "aiosignal: a list of registered asynchronous callbacks" 25 | category = "dev" 26 | optional = false 27 | python-versions = ">=3.6" 28 | 29 | [package.dependencies] 30 | frozenlist = ">=1.1.0" 31 | 32 | [[package]] 33 | name = "async-timeout" 34 | version = "4.0.2" 35 | description = "Timeout context manager for asyncio programs" 36 | category = "dev" 37 | optional = false 38 | python-versions = ">=3.6" 39 | 40 | [[package]] 41 | name = "atomicwrites" 42 | version = "1.4.0" 43 | description = "Atomic file writes." 44 | category = "dev" 45 | optional = false 46 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 47 | 48 | [[package]] 49 | name = "attrs" 50 | version = "21.4.0" 51 | description = "Classes Without Boilerplate" 52 | category = "dev" 53 | optional = false 54 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 55 | 56 | [package.extras] 57 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 58 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 59 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 60 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 61 | 62 | [[package]] 63 | name = "certifi" 64 | version = "2022.5.18.1" 65 | description = "Python package for providing Mozilla's CA Bundle." 66 | category = "dev" 67 | optional = false 68 | python-versions = ">=3.6" 69 | 70 | [[package]] 71 | name = "charset-normalizer" 72 | version = "2.0.12" 73 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 74 | category = "dev" 75 | optional = false 76 | python-versions = ">=3.5.0" 77 | 78 | [package.extras] 79 | unicode_backport = ["unicodedata2"] 80 | 81 | [[package]] 82 | name = "codecov" 83 | version = "2.1.12" 84 | description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" 85 | category = "dev" 86 | optional = false 87 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 88 | 89 | [package.dependencies] 90 | coverage = "*" 91 | requests = ">=2.7.9" 92 | 93 | [[package]] 94 | name = "colorama" 95 | version = "0.4.4" 96 | description = "Cross-platform colored terminal text." 97 | category = "dev" 98 | optional = false 99 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 100 | 101 | [[package]] 102 | name = "coverage" 103 | version = "6.4.1" 104 | description = "Code coverage measurement for Python" 105 | category = "dev" 106 | optional = false 107 | python-versions = ">=3.7" 108 | 109 | [package.dependencies] 110 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 111 | 112 | [package.extras] 113 | toml = ["tomli"] 114 | 115 | [[package]] 116 | name = "frozenlist" 117 | version = "1.3.0" 118 | description = "A list-like structure which implements collections.abc.MutableSequence" 119 | category = "dev" 120 | optional = false 121 | python-versions = ">=3.7" 122 | 123 | [[package]] 124 | name = "idna" 125 | version = "3.3" 126 | description = "Internationalized Domain Names in Applications (IDNA)" 127 | category = "dev" 128 | optional = false 129 | python-versions = ">=3.5" 130 | 131 | [[package]] 132 | name = "iniconfig" 133 | version = "1.1.1" 134 | description = "iniconfig: brain-dead simple config-ini parsing" 135 | category = "dev" 136 | optional = false 137 | python-versions = "*" 138 | 139 | [[package]] 140 | name = "multidict" 141 | version = "6.0.2" 142 | description = "multidict implementation" 143 | category = "dev" 144 | optional = false 145 | python-versions = ">=3.7" 146 | 147 | [[package]] 148 | name = "packaging" 149 | version = "21.3" 150 | description = "Core utilities for Python packages" 151 | category = "dev" 152 | optional = false 153 | python-versions = ">=3.6" 154 | 155 | [package.dependencies] 156 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 157 | 158 | [[package]] 159 | name = "pluggy" 160 | version = "1.0.0" 161 | description = "plugin and hook calling mechanisms for python" 162 | category = "dev" 163 | optional = false 164 | python-versions = ">=3.6" 165 | 166 | [package.extras] 167 | dev = ["pre-commit", "tox"] 168 | testing = ["pytest", "pytest-benchmark"] 169 | 170 | [[package]] 171 | name = "py" 172 | version = "1.11.0" 173 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 174 | category = "dev" 175 | optional = false 176 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 177 | 178 | [[package]] 179 | name = "pyparsing" 180 | version = "3.0.9" 181 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 182 | category = "dev" 183 | optional = false 184 | python-versions = ">=3.6.8" 185 | 186 | [package.extras] 187 | diagrams = ["railroad-diagrams", "jinja2"] 188 | 189 | [[package]] 190 | name = "pytest" 191 | version = "7.1.2" 192 | description = "pytest: simple powerful testing with Python" 193 | category = "dev" 194 | optional = false 195 | python-versions = ">=3.7" 196 | 197 | [package.dependencies] 198 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 199 | attrs = ">=19.2.0" 200 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 201 | iniconfig = "*" 202 | packaging = "*" 203 | pluggy = ">=0.12,<2.0" 204 | py = ">=1.8.2" 205 | tomli = ">=1.0.0" 206 | 207 | [package.extras] 208 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 209 | 210 | [[package]] 211 | name = "pytest-aiohttp" 212 | version = "1.0.4" 213 | description = "Pytest plugin for aiohttp support" 214 | category = "dev" 215 | optional = false 216 | python-versions = ">=3.7" 217 | 218 | [package.dependencies] 219 | aiohttp = ">=3.8.1" 220 | pytest = ">=6.1.0" 221 | pytest-asyncio = ">=0.17.2" 222 | 223 | [package.extras] 224 | testing = ["coverage (==6.2)", "mypy (==0.931)"] 225 | 226 | [[package]] 227 | name = "pytest-asyncio" 228 | version = "0.18.3" 229 | description = "Pytest support for asyncio" 230 | category = "dev" 231 | optional = false 232 | python-versions = ">=3.7" 233 | 234 | [package.dependencies] 235 | pytest = ">=6.1.0" 236 | 237 | [package.extras] 238 | testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] 239 | 240 | [[package]] 241 | name = "pytest-cov" 242 | version = "3.0.0" 243 | description = "Pytest plugin for measuring coverage." 244 | category = "dev" 245 | optional = false 246 | python-versions = ">=3.6" 247 | 248 | [package.dependencies] 249 | coverage = {version = ">=5.2.1", extras = ["toml"]} 250 | pytest = ">=4.6" 251 | 252 | [package.extras] 253 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 254 | 255 | [[package]] 256 | name = "pytest-timeout" 257 | version = "2.1.0" 258 | description = "pytest plugin to abort hanging tests" 259 | category = "dev" 260 | optional = false 261 | python-versions = ">=3.6" 262 | 263 | [package.dependencies] 264 | pytest = ">=5.0.0" 265 | 266 | [[package]] 267 | name = "requests" 268 | version = "2.28.0" 269 | description = "Python HTTP for Humans." 270 | category = "dev" 271 | optional = false 272 | python-versions = ">=3.7, <4" 273 | 274 | [package.dependencies] 275 | certifi = ">=2017.4.17" 276 | charset-normalizer = ">=2.0.0,<2.1.0" 277 | idna = ">=2.5,<4" 278 | urllib3 = ">=1.21.1,<1.27" 279 | 280 | [package.extras] 281 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 282 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 283 | 284 | [[package]] 285 | name = "tomli" 286 | version = "2.0.1" 287 | description = "A lil' TOML parser" 288 | category = "dev" 289 | optional = false 290 | python-versions = ">=3.7" 291 | 292 | [[package]] 293 | name = "urllib3" 294 | version = "1.26.9" 295 | description = "HTTP library with thread-safe connection pooling, file post, and more." 296 | category = "dev" 297 | optional = false 298 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 299 | 300 | [package.extras] 301 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] 302 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 303 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 304 | 305 | [[package]] 306 | name = "yarl" 307 | version = "1.7.2" 308 | description = "Yet another URL library" 309 | category = "dev" 310 | optional = false 311 | python-versions = ">=3.6" 312 | 313 | [package.dependencies] 314 | idna = ">=2.0" 315 | multidict = ">=4.0" 316 | 317 | [metadata] 318 | lock-version = "1.1" 319 | python-versions = "^3.8 | ^3.9" 320 | content-hash = "1b759ad745c75ca82a19cedeea167f654481cc3cf1378fb4f398802667d69612" 321 | 322 | [metadata.files] 323 | aiohttp = [ 324 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, 325 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, 326 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, 327 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, 328 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, 329 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, 330 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, 331 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, 332 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, 333 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, 334 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, 335 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, 336 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, 337 | {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, 338 | {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, 339 | {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, 340 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, 341 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, 342 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, 343 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, 344 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, 345 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, 346 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, 347 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, 348 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, 349 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, 350 | {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, 351 | {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, 352 | {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, 353 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, 354 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, 355 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, 356 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, 357 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, 358 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, 359 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, 360 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, 361 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, 362 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, 363 | {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, 364 | {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, 365 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, 366 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, 367 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, 368 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, 369 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, 370 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, 371 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, 372 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, 373 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, 374 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, 375 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, 376 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, 377 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, 378 | {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, 379 | {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, 380 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, 381 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, 382 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, 383 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, 384 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, 385 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, 386 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, 387 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, 388 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, 389 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, 390 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, 391 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, 392 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, 393 | {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, 394 | {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, 395 | {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, 396 | ] 397 | aiosignal = [ 398 | {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, 399 | {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, 400 | ] 401 | async-timeout = [ 402 | {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, 403 | {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, 404 | ] 405 | atomicwrites = [ 406 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 407 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 408 | ] 409 | attrs = [ 410 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 411 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 412 | ] 413 | certifi = [ 414 | {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, 415 | {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, 416 | ] 417 | charset-normalizer = [ 418 | {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, 419 | {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, 420 | ] 421 | codecov = [ 422 | {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"}, 423 | {file = "codecov-2.1.12-py3.8.egg", hash = "sha256:782a8e5352f22593cbc5427a35320b99490eb24d9dcfa2155fd99d2b75cfb635"}, 424 | {file = "codecov-2.1.12.tar.gz", hash = "sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1"}, 425 | ] 426 | colorama = [ 427 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 428 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 429 | ] 430 | coverage = [ 431 | {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, 432 | {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, 433 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, 434 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, 435 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, 436 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, 437 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, 438 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, 439 | {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, 440 | {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, 441 | {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, 442 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, 443 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, 444 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, 445 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, 446 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, 447 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, 448 | {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, 449 | {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, 450 | {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, 451 | {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, 452 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, 453 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, 454 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, 455 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, 456 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, 457 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, 458 | {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, 459 | {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, 460 | {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, 461 | {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, 462 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, 463 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, 464 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, 465 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, 466 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, 467 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, 468 | {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, 469 | {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, 470 | {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, 471 | {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, 472 | ] 473 | frozenlist = [ 474 | {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, 475 | {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, 476 | {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, 477 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, 478 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, 479 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, 480 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, 481 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, 482 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, 483 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, 484 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, 485 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, 486 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, 487 | {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, 488 | {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, 489 | {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, 490 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, 491 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, 492 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, 493 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, 494 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, 495 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, 496 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, 497 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, 498 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, 499 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, 500 | {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, 501 | {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, 502 | {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, 503 | {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, 504 | {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, 505 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, 506 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, 507 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, 508 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, 509 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, 510 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, 511 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, 512 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, 513 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, 514 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, 515 | {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, 516 | {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, 517 | {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, 518 | {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, 519 | {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, 520 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, 521 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, 522 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, 523 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, 524 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, 525 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, 526 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, 527 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, 528 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, 529 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, 530 | {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, 531 | {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, 532 | {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, 533 | ] 534 | idna = [ 535 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 536 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 537 | ] 538 | iniconfig = [ 539 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 540 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 541 | ] 542 | multidict = [ 543 | {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, 544 | {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, 545 | {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, 546 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, 547 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, 548 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, 549 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, 550 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, 551 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, 552 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, 553 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, 554 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, 555 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, 556 | {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, 557 | {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, 558 | {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, 559 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, 560 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, 561 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, 562 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, 563 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, 564 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, 565 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, 566 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, 567 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, 568 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, 569 | {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, 570 | {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, 571 | {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, 572 | {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, 573 | {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, 574 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, 575 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, 576 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, 577 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, 578 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, 579 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, 580 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, 581 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, 582 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, 583 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, 584 | {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, 585 | {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, 586 | {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, 587 | {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, 588 | {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, 589 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, 590 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, 591 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, 592 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, 593 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, 594 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, 595 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, 596 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, 597 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, 598 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, 599 | {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, 600 | {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, 601 | {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, 602 | ] 603 | packaging = [ 604 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 605 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 606 | ] 607 | pluggy = [ 608 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 609 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 610 | ] 611 | py = [ 612 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 613 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 614 | ] 615 | pyparsing = [ 616 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 617 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 618 | ] 619 | pytest = [ 620 | {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, 621 | {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, 622 | ] 623 | pytest-aiohttp = [ 624 | {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, 625 | {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, 626 | ] 627 | pytest-asyncio = [ 628 | {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, 629 | {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, 630 | {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, 631 | ] 632 | pytest-cov = [ 633 | {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, 634 | {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, 635 | ] 636 | pytest-timeout = [ 637 | {file = "pytest-timeout-2.1.0.tar.gz", hash = "sha256:c07ca07404c612f8abbe22294b23c368e2e5104b521c1790195561f37e1ac3d9"}, 638 | {file = "pytest_timeout-2.1.0-py3-none-any.whl", hash = "sha256:f6f50101443ce70ad325ceb4473c4255e9d74e3c7cd0ef827309dfa4c0d975c6"}, 639 | ] 640 | requests = [ 641 | {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, 642 | {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, 643 | ] 644 | tomli = [ 645 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 646 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 647 | ] 648 | urllib3 = [ 649 | {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, 650 | {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, 651 | ] 652 | yarl = [ 653 | {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, 654 | {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, 655 | {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, 656 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, 657 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, 658 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, 659 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, 660 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, 661 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, 662 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, 663 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, 664 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, 665 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, 666 | {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, 667 | {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, 668 | {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, 669 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, 670 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, 671 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, 672 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, 673 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, 674 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, 675 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, 676 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, 677 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, 678 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, 679 | {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, 680 | {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, 681 | {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, 682 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, 683 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, 684 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, 685 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, 686 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, 687 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, 688 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, 689 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, 690 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, 691 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, 692 | {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, 693 | {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, 694 | {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, 695 | {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, 696 | {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, 697 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, 698 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, 699 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, 700 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, 701 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, 702 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, 703 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, 704 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, 705 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, 706 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, 707 | {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, 708 | {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, 709 | {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, 710 | {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, 711 | {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, 712 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, 713 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, 714 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, 715 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, 716 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, 717 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, 718 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, 719 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, 720 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, 721 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, 722 | {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, 723 | {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, 724 | {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, 725 | ] 726 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "hyperion-py" 3 | version = "0.7.6" 4 | description = "Hyperion Ambient Lighting Python Package" 5 | authors = ["Dermot Duffy "] 6 | classifiers = [ 7 | "Intended Audience :: Developers", 8 | "Programming Language :: Python :: 3.8", 9 | "Programming Language :: Python :: 3.9", 10 | "License :: OSI Approved :: MIT License", 11 | "Operating System :: OS Independent", 12 | "Topic :: Home Automation", 13 | ] 14 | keywords = [ 15 | "hyperion", 16 | ] 17 | license = "MIT" 18 | repository = "https://github.com/dermotduffy/hyperion-py" 19 | include = ["hyperion/py.typed", "LICENSE"] 20 | readme = "README.md" 21 | packages = [ 22 | { include = "hyperion" }, 23 | ] 24 | 25 | [tool.poetry.dependencies] 26 | python = "^3.8 | ^3.9" 27 | 28 | [tool.poetry.dev-dependencies] 29 | pytest = "^7.0" 30 | pytest-cov = "^3.0.0" 31 | pytest-aiohttp = "^1.0.4" 32 | codecov = "^2.1.12" 33 | pytest-timeout = "^2.1.0" 34 | coverage = "^6.3" 35 | pytest-asyncio = "^0.18.2" 36 | 37 | [build-system] 38 | requires = ["poetry-core>=1.0.0"] 39 | build-backend = "poetry.core.masonry.api" 40 | 41 | [tool.isort] 42 | # https://github.com/PyCQA/isort/wiki/isort-Settings 43 | profile = "black" 44 | # will group `import x` and `from x import` of the same module. 45 | force_sort_within_sections = true 46 | known_first_party = [ 47 | "hyperion", 48 | "tests", 49 | ] 50 | forced_separate = [ 51 | "tests", 52 | ] 53 | combine_as_imports = true 54 | default_section = "THIRDPARTY" 55 | 56 | 57 | [tool.pytest.ini_options] 58 | asyncio_mode = "auto" 59 | addopts = "-qq --timeout=9 --cov=hyperion" 60 | console_output_style = "count" 61 | testpaths = [ 62 | "tests", 63 | ] 64 | markers = [ 65 | "asyncio", 66 | ] 67 | [tool.coverage.run] 68 | branch = false 69 | 70 | [tool.coverage.report] 71 | show_missing = true 72 | fail_under = 95 73 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.10.11; python_version >= "3.7" 2 | aiosignal==1.2.0; python_version >= "3.7" 3 | async-timeout==4.0.2; python_version >= "3.7" 4 | atomicwrites==1.4.0; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0" 5 | attrs==21.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" 6 | certifi==2022.5.18.1; python_version >= "3.7" and python_version < "4" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") 7 | charset-normalizer==2.0.12; python_full_version >= "3.5.0" and python_version >= "3.7" and python_version < "4" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") 8 | codecov==2.1.13; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") 9 | colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0" 10 | coverage==6.4.1; python_version >= "3.7" 11 | frozenlist==1.5.0; python_version >= "3.7" 12 | idna==3.3; python_version >= "3.7" and python_version < "4" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") 13 | iniconfig==1.1.1; python_version >= "3.7" 14 | multidict==6.0.2; python_version >= "3.7" 15 | packaging==21.3; python_version >= "3.7" 16 | pluggy==1.0.0; python_version >= "3.7" 17 | py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" 18 | pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.7" 19 | pytest-aiohttp==1.0.4; python_version >= "3.7" 20 | pytest-asyncio==0.18.3; python_version >= "3.7" 21 | pytest-cov==3.0.0; python_version >= "3.6" 22 | pytest-timeout==2.1.0; python_version >= "3.6" 23 | pytest==7.1.2; python_version >= "3.7" 24 | requests==2.28.0; python_version >= "3.7" and python_version < "4" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") 25 | tomli==2.0.1; python_full_version <= "3.11.0a6" and python_version >= "3.7" 26 | urllib3==1.26.9; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.7" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") 27 | yarl==1.12.0; python_version >= "3.7" 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build 3 | doctests = True 4 | # To work with Black 5 | max-line-length = 88 6 | # E501: line too long 7 | # W503: Line break occurred before a binary operator 8 | # E203: Whitespace before ':' 9 | # D202 No blank lines allowed after function docstring 10 | # W504 line break after binary operator 11 | ignore = 12 | E501, 13 | W503, 14 | E203, 15 | D202, 16 | W504 17 | 18 | [mypy] 19 | warn_unused_configs = true 20 | disallow_any_generics = true 21 | disallow_subclassing_any = true 22 | disallow_untyped_calls = true 23 | disallow_untyped_defs = true 24 | disallow_incomplete_defs = true 25 | check_untyped_defs = true 26 | disallow_untyped_decorators = true 27 | no_implicit_optional = true 28 | warn_redundant_casts = true 29 | warn_unused_ignores = true 30 | warn_return_any = true 31 | no_implicit_reexport = true 32 | ignore_errors = false 33 | warn_unreachable = true 34 | show_error_codes = true 35 | ignore_missing_imports = true 36 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Hyperion test package.""" 2 | -------------------------------------------------------------------------------- /tests/testdata/serverinfo_response_1.json: -------------------------------------------------------------------------------- 1 | {"command":"serverinfo","info":{"activeEffects":[],"activeLedColor":[],"adjustment":[{"backlightColored":false,"backlightThreshold":0,"blue":[0,0,255],"brightness":83,"brightnessCompensation":100,"cyan":[0,255,255],"gammaBlue":1.6,"gammaGreen":2,"gammaRed":1.7,"green":[0,255,0],"id":"default","magenta":[255,0,255],"red":[255,0,0],"white":[255,255,255],"yellow":[255,255,0]}],"components":[{"enabled":true,"name":"ALL"},{"enabled":true,"name":"SMOOTHING"},{"enabled":true,"name":"BLACKBORDER"},{"enabled":false,"name":"FORWARDER"},{"enabled":false,"name":"BOBLIGHTSERVER"},{"enabled":false,"name":"GRABBER"},{"enabled":true,"name":"V4L"},{"enabled":true,"name":"LEDDEVICE"}],"effects":[{"args":{"center_x":0.5,"center_y":0.5,"custom-colors":[[0,0,0],[0,0,0],[255,255,0],[0,0,0],[0,0,0],[255,255,0],[0,0,0],[0,0,0],[255,255,0]],"custom-colors2":[],"enable-second":false,"random-center":false,"reverse":false,"rotation-time":25,"smoothing-custom-settings":null},"file":":/effects//atomic.json","name":"Atomic swirl","script":":/effects//swirl.py"},{"args":{"blobs":5,"color":[0,0,255],"hueChange":60,"reverse":false,"rotationTime":60,"smoothing-custom-settings":null},"file":":/effects//mood-blobs-blue.json","name":"Blue mood blobs","script":":/effects//mood-blobs.py"},{"args":{"color-end":[255,255,255],"color-end-time":250,"color-start":[50,50,50],"color-start-time":50,"fade-in-time":3000,"fade-out-time":1000,"maintain-end-color":true,"repeat-count":0,"smoothing-custom-settings":null},"file":":/effects//breath.json","name":"Breath","script":":/effects//fade.py"},{"args":{"brightness":100,"candles":"all","color":[255,138,0],"sleepTime":0.2,"smoothing-custom-settings":true,"smoothing-time_ms":500,"smoothing-updateFrequency":20},"file":":/effects//candle.json","name":"Candle","script":":/effects//candle.py"},{"args":{"color-end":[238,173,47],"color-end-time":0,"color-start":[136,97,7],"color-start-time":0,"fade-in-time":5000,"fade-out-time":0,"maintain-end-color":true,"repeat-count":1,"smoothing-custom-settings":null},"file":":/effects//cinema-fade-in.json","name":"Cinema brighten lights","script":":/effects//fade.py"},{"args":{"color-end":[238,173,47],"color-end-time":0,"color-start":[136,97,7],"color-start-time":0,"fade-in-time":0,"fade-out-time":5000,"maintain-end-color":true,"repeat-count":1,"smoothing-custom-settings":null},"file":":/effects//cinema-fade-off.json","name":"Cinema dim lights","script":":/effects//fade.py"},{"args":{"baseChange":true,"baseColorChangeRate":2,"baseColorRangeLeft":160,"baseColorRangeRight":320,"blobs":5,"color":[0,0,255],"hueChange":30,"reverse":false,"rotationTime":60,"smoothing-custom-settings":null},"file":":/effects//mood-blobs-cold.json","name":"Cold mood blobs","script":":/effects//mood-blobs.py"},{"args":{"explodeRadius":8,"smoothing-custom-settings":null,"speed":100,"trailLength":5},"file":":/effects//collision.json","name":"Collision","script":":/effects//collision.py"},{"args":{"smoothing-custom-settings":null,"speed":1},"file":":/effects//traces.json","name":"Color traces","script":":/effects//traces.py"},{"args":{"center_x":0.5,"center_x2":0.5,"center_y":0.5,"center_y2":0.5,"custom-colors":[[255,0,0],[20,0,255]],"custom-colors2":[[255,0,0,0],[255,0,0,0],[255,0,0,0],[0,0,0,1],[0,0,0,0],[255,0,0,0],[255,0,0,0],[0,0,0,1]],"enable-second":true,"random-center":false,"random-center2":false,"reverse":false,"reverse2":true,"rotation-time":25,"smoothing-custom-settings":null},"file":":/effects//double-swirl.json","name":"Double swirl","script":":/effects//swirl.py"},{"args":{"fps":25,"image":":fire.gif","reverse":false,"smoothing-custom-settings":null},"file":":/effects//fire.json","name":"Fire","script":":/effects//gif.py"},{"args":{"countries":["de","se"],"smoothing-custom-settings":null,"switch-time":2},"file":":/effects//flag.json","name":"Flags Germany/Sweden","script":":/effects//flag.py"},{"args":{"baseChange":true,"baseColorChangeRate":0.2,"baseColorRangeLeft":0,"baseColorRangeRight":360,"blobs":5,"colorRandom":true,"hueChange":30,"reverse":false,"rotationTime":60,"smoothing-custom-settings":null},"file":":/effects//mood-blobs-full.json","name":"Full color mood blobs","script":":/effects//mood-blobs.py"},{"args":{"blobs":5,"color":[0,255,0],"hueChange":60,"reverse":false,"rotationTime":60,"smoothing-custom-settings":null},"file":":/effects//mood-blobs-green.json","name":"Green mood blobs","script":":/effects//mood-blobs.py"},{"args":{"color":[255,0,0],"fadeFactor":0.7,"smoothing-custom-settings":null,"speed":1},"file":":/effects//knight-rider.json","name":"Knight rider","script":":/effects//knight-rider.py"},{"args":{"sleepTime":0.5,"smoothing-custom-settings":false,"smoothing-time_ms":500,"smoothing-updateFrequency":20,"testleds":"all"},"file":":/effects//ledtest.json","name":"Led Test","script":":/effects//ledtest.py"},{"args":{"background-color":[0,0,0],"hour-color":[0,0,255],"marker-color":[255,255,255],"marker-depth":0,"marker-enabled":false,"marker-width":5,"minute-color":[0,255,0],"second-color":[255,0,0],"show_seconds":true,"smoothing-custom-settings":null},"file":":/effects//light-clock.json","name":"Light clock","script":":/effects//light-clock.py"},{"args":{"fps":25,"image":":lights.gif","reverse":false,"smoothing-custom-settings":null},"file":":/effects//lights.json","name":"Lights","script":":/effects//gif.py"},{"args":{"color-end":[0,0,255],"color-end-time":150,"color-start":[0,0,50],"color-start-time":40,"fade-in-time":200,"fade-out-time":100,"maintain-end-color":false,"repeat-count":3,"smoothing-custom-settings":null},"file":":/effects//notify-blue.json","name":"Notify blue","script":":/effects//fade.py"},{"args":{"margin-pos":2,"rotationTime":4,"smoothing-custom-settings":null},"file":":/effects//pacman.json","name":"Pac-Man","script":":/effects//pacman.py"},{"args":{"sleepTime":0.2,"smoothing-custom-settings":null},"file":":/effects//plasma.json","name":"Plasma","script":":/effects//plasma.py"},{"args":{"color_one":[255,0,0],"color_two":[0,0,255],"colors_count":10,"reverse":false,"rotation-time":1.5,"smoothing-custom-settings":null},"file":":/effects//police-lights-single.json","name":"Police Lights Single","script":":/effects//police.py"},{"args":{"color_one":[255,0,0],"color_two":[0,0,255],"reverse":false,"rotation-time":1,"smoothing-custom-settings":null},"file":":/effects//police-lights-solid.json","name":"Police Lights Solid","script":":/effects//police.py"},{"args":{"brightness":100,"reverse":false,"rotation-time":60,"smoothing-custom-settings":null},"file":":/effects//rainbow-mood.json","name":"Rainbow mood","script":":/effects//rainbow-mood.py"},{"args":{"center_x":0.5,"center_y":0.5,"custom-colors":[],"custom-colors2":[],"enable-second":false,"random-center":false,"reverse":false,"rotation-time":20,"smoothing-custom-settings":true,"smoothing-time_ms":200,"smoothing-updateFrequency":25},"file":":/effects//rainbow-swirl.json","name":"Rainbow swirl","script":":/effects//swirl.py"},{"args":{"center_x":0.5,"center_y":0.5,"custom-colors":[],"custom-colors2":[],"enable-second":false,"random-center":false,"reverse":false,"rotation-time":7,"smoothing-custom-settings":null},"file":":/effects//rainbow-swirl-fast.json","name":"Rainbow swirl fast","script":":/effects//swirl.py"},{"args":{"saturation":1,"smoothing-custom-settings":true,"smoothing-time_ms":200,"smoothing-updateFrequency":20,"speed":750},"file":":/effects//random.json","name":"Random","script":":/effects//random.py"},{"args":{"blobs":5,"color":[255,0,0],"hueChange":60,"reverse":false,"rotationTime":60,"smoothing-custom-settings":null},"file":":/effects//mood-blobs-red.json","name":"Red mood blobs","script":":/effects//mood-blobs.py"},{"args":{"center_x":1.25,"center_y":-0.25,"colors":[[8,0,255],[0,161,255],[0,222,255],[0,153,255],[38,0,255],[0,199,255]],"random-center":false,"reverse":false,"reverse_time":0,"rotation_time":60,"smoothing-custom-settings":true,"smoothing-time_ms":200,"smoothing-updateFrequency":25},"file":":/effects//Seawaves.json","name":"Sea waves","script":":/effects//waves.py"},{"args":{"background-color":[0,0,0],"color":[255,0,0],"percentage":10,"rotation-time":12,"smoothing-custom-settings":null},"file":":/effects//snake.json","name":"Snake","script":":/effects//snake.py"},{"args":{"brightness":100,"color":[255,255,255],"random-color":false,"rotation-time":3,"saturation":100,"sleep-time":0.05,"smoothing-custom-settings":null},"file":":/effects//sparks.json","name":"Sparks","script":":/effects//sparks.py"},{"args":{"color-end":[0,0,0],"color-end-time":100,"color-start":[255,0,0],"color-start-time":100,"fade-in-time":100,"fade-out-time":100,"maintain-end-color":true,"repeat-count":0,"smoothing-custom-settings":null},"file":":/effects//strobe-red.json","name":"Strobe red","script":":/effects//fade.py"},{"args":{"color-end":[0,0,0],"color-end-time":10,"color-start":[255,255,255],"color-start-time":50,"fade-in-time":0,"fade-out-time":100,"maintain-end-color":true,"repeat-count":0,"smoothing-custom-settings":null},"file":":/effects//strobe-white.json","name":"Strobe white","script":":/effects//fade.py"},{"args":{"alarm-color":[255,0,0],"initial-blink":true,"post-color":[255,174,11],"set-post-color":true,"shutdown-enabled":false,"smoothing-custom-settings":null,"speed":1.2},"file":":/effects//shutdown.json","name":"System Shutdown","script":":/effects//shutdown.py"},{"args":{"color":[255,255,255],"height":8,"max_len":7,"min_len":2,"random":false,"smoothing-custom-settings":null,"speed":30,"trails":3},"file":":/effects//trails.json","name":"Trails","script":":/effects//trails.py"},{"args":{"color":[255,255,255],"height":8,"max_len":6,"min_len":2,"random":true,"smoothing-custom-settings":null,"speed":50,"trails":16},"file":":/effects//trails_color.json","name":"Trails color","script":":/effects//trails.py"},{"args":{"baseChange":true,"baseColorChangeRate":2,"baseColorRangeLeft":333,"baseColorRangeRight":151,"blobs":5,"color":[255,0,0],"hueChange":30,"reverse":false,"rotationTime":60,"smoothing-custom-settings":true,"smoothing-time_ms":200,"smoothing-updateFrequency":25},"file":":/effects//mood-blobs-warm.json","name":"Warm mood blobs","script":":/effects//mood-blobs.py"},{"args":{"reverse":false,"smoothing-custom-settings":null},"file":":/effects//waves.json","name":"Waves with Color","script":":/effects//waves.py"},{"args":{"color1":[255,255,255],"color2":[255,0,0],"length":1,"sleepTime":750,"smoothing-custom-settings":null},"file":":/effects//x-mas.json","name":"X-Mas","script":":/effects//x-mas.py"}],"grabbers":{"available":["dispmanx","v4l2","framebuffer","qt"],"v4l2_properties":[{"device":"/dev/video0","framerates":["30","25"],"inputs":[{"inputIndex":0,"inputName":"Camera 1"}],"name":"AV TO USB2.0","resolutions":["720x480","720x576","640x480","320x240","160x120"]},{"device":"/dev/video14","framerates":[],"inputs":[],"name":"bcm2835-isp-capture0","resolutions":[]},{"device":"/dev/video15","framerates":[],"inputs":[],"name":"bcm2835-isp-capture1","resolutions":[]}]},"hostname":"pi-kitchen","imageToLedMappingType":"multicolor_mean","instance":[{"friendly_name":"wled-tv-lights","instance":0,"running":true},{"friendly_name":"test-instance","instance":1,"running":true}],"ledDevices":{"available":["adalight","apa102","apa104","atmo","atmoorb","dmx","fadecandy","file","hyperionusbasp","karate","lightpack","lpd6803","lpd8806","multilightpack","nanoleaf","p9813","paintpack","philipshue","piblaster","rawhid","sedu","sk6812spi","sk6822spi","tinkerforge","tpm2","tpm2net","udpartnet","udpe131","udph801","udpraw","wled","ws2801","ws2812spi","ws281x","yeelight"]},"leds":[{"hmax":0.9888,"hmin":0.9768,"vmax":0.01,"vmin":0},{"hmax":0.9768,"hmin":0.9649,"vmax":0.01,"vmin":0},{"hmax":0.9649,"hmin":0.953,"vmax":0.01,"vmin":0},{"hmax":0.953,"hmin":0.9411,"vmax":0.01,"vmin":0},{"hmax":0.9411,"hmin":0.9291,"vmax":0.01,"vmin":0},{"hmax":0.9291,"hmin":0.9172,"vmax":0.01,"vmin":0},{"hmax":0.9172,"hmin":0.9053,"vmax":0.01,"vmin":0},{"hmax":0.9053,"hmin":0.8934,"vmax":0.01,"vmin":0},{"hmax":0.8934,"hmin":0.8815,"vmax":0.01,"vmin":0},{"hmax":0.8815,"hmin":0.8695,"vmax":0.01,"vmin":0},{"hmax":0.8695,"hmin":0.8576,"vmax":0.01,"vmin":0},{"hmax":0.8576,"hmin":0.8457,"vmax":0.01,"vmin":0},{"hmax":0.8457,"hmin":0.8338,"vmax":0.01,"vmin":0},{"hmax":0.8338,"hmin":0.8219,"vmax":0.01,"vmin":0},{"hmax":0.8219,"hmin":0.8099,"vmax":0.01,"vmin":0},{"hmax":0.8099,"hmin":0.798,"vmax":0.01,"vmin":0},{"hmax":0.798,"hmin":0.7861,"vmax":0.01,"vmin":0},{"hmax":0.7861,"hmin":0.7742,"vmax":0.01,"vmin":0},{"hmax":0.7742,"hmin":0.7623,"vmax":0.01,"vmin":0},{"hmax":0.7623,"hmin":0.7503,"vmax":0.01,"vmin":0},{"hmax":0.7503,"hmin":0.7384,"vmax":0.01,"vmin":0},{"hmax":0.7384,"hmin":0.7265,"vmax":0.01,"vmin":0},{"hmax":0.7265,"hmin":0.7146,"vmax":0.01,"vmin":0},{"hmax":0.7146,"hmin":0.7027,"vmax":0.01,"vmin":0},{"hmax":0.7027,"hmin":0.6907,"vmax":0.01,"vmin":0},{"hmax":0.6907,"hmin":0.6788,"vmax":0.01,"vmin":0},{"hmax":0.6788,"hmin":0.6669,"vmax":0.01,"vmin":0},{"hmax":0.6669,"hmin":0.655,"vmax":0.01,"vmin":0},{"hmax":0.655,"hmin":0.643,"vmax":0.01,"vmin":0},{"hmax":0.643,"hmin":0.6311,"vmax":0.01,"vmin":0},{"hmax":0.6311,"hmin":0.6192,"vmax":0.01,"vmin":0},{"hmax":0.6192,"hmin":0.6073,"vmax":0.01,"vmin":0},{"hmax":0.6073,"hmin":0.5954,"vmax":0.01,"vmin":0},{"hmax":0.5954,"hmin":0.5834,"vmax":0.01,"vmin":0},{"hmax":0.5834,"hmin":0.5715,"vmax":0.01,"vmin":0},{"hmax":0.5715,"hmin":0.5596,"vmax":0.01,"vmin":0},{"hmax":0.5596,"hmin":0.5477,"vmax":0.01,"vmin":0},{"hmax":0.5477,"hmin":0.5358,"vmax":0.01,"vmin":0},{"hmax":0.5358,"hmin":0.5238,"vmax":0.01,"vmin":0},{"hmax":0.5238,"hmin":0.5119,"vmax":0.01,"vmin":0},{"hmax":0.5119,"hmin":0.5,"vmax":0.01,"vmin":0},{"hmax":0.5,"hmin":0.4881,"vmax":0.01,"vmin":0},{"hmax":0.4881,"hmin":0.4762,"vmax":0.01,"vmin":0},{"hmax":0.4762,"hmin":0.4642,"vmax":0.01,"vmin":0},{"hmax":0.4642,"hmin":0.4523,"vmax":0.01,"vmin":0},{"hmax":0.4523,"hmin":0.4404,"vmax":0.01,"vmin":0},{"hmax":0.4404,"hmin":0.4285,"vmax":0.01,"vmin":0},{"hmax":0.4285,"hmin":0.4166,"vmax":0.01,"vmin":0},{"hmax":0.4166,"hmin":0.4046,"vmax":0.01,"vmin":0},{"hmax":0.4046,"hmin":0.3927,"vmax":0.01,"vmin":0},{"hmax":0.3927,"hmin":0.3808,"vmax":0.01,"vmin":0},{"hmax":0.3808,"hmin":0.3689,"vmax":0.01,"vmin":0},{"hmax":0.3689,"hmin":0.357,"vmax":0.01,"vmin":0},{"hmax":0.357,"hmin":0.345,"vmax":0.01,"vmin":0},{"hmax":0.345,"hmin":0.3331,"vmax":0.01,"vmin":0},{"hmax":0.3331,"hmin":0.3212,"vmax":0.01,"vmin":0},{"hmax":0.3212,"hmin":0.3093,"vmax":0.01,"vmin":0},{"hmax":0.3093,"hmin":0.2973,"vmax":0.01,"vmin":0},{"hmax":0.2973,"hmin":0.2854,"vmax":0.01,"vmin":0},{"hmax":0.2854,"hmin":0.2735,"vmax":0.01,"vmin":0},{"hmax":0.2735,"hmin":0.2616,"vmax":0.01,"vmin":0},{"hmax":0.2616,"hmin":0.2497,"vmax":0.01,"vmin":0},{"hmax":0.2497,"hmin":0.2377,"vmax":0.01,"vmin":0},{"hmax":0.2377,"hmin":0.2258,"vmax":0.01,"vmin":0},{"hmax":0.2258,"hmin":0.2139,"vmax":0.01,"vmin":0},{"hmax":0.2139,"hmin":0.202,"vmax":0.01,"vmin":0},{"hmax":0.202,"hmin":0.1901,"vmax":0.01,"vmin":0},{"hmax":0.1901,"hmin":0.1781,"vmax":0.01,"vmin":0},{"hmax":0.1781,"hmin":0.1662,"vmax":0.01,"vmin":0},{"hmax":0.1662,"hmin":0.1543,"vmax":0.01,"vmin":0},{"hmax":0.1543,"hmin":0.1424,"vmax":0.01,"vmin":0},{"hmax":0.1424,"hmin":0.1305,"vmax":0.01,"vmin":0},{"hmax":0.1305,"hmin":0.1185,"vmax":0.01,"vmin":0},{"hmax":0.1185,"hmin":0.1066,"vmax":0.01,"vmin":0},{"hmax":0.1066,"hmin":0.0947,"vmax":0.01,"vmin":0},{"hmax":0.0947,"hmin":0.0828,"vmax":0.01,"vmin":0},{"hmax":0.0828,"hmin":0.0709,"vmax":0.01,"vmin":0},{"hmax":0.0709,"hmin":0.0589,"vmax":0.01,"vmin":0},{"hmax":0.0589,"hmin":0.047,"vmax":0.01,"vmin":0},{"hmax":0.047,"hmin":0.0351,"vmax":0.01,"vmin":0},{"hmax":0.0351,"hmin":0.0232,"vmax":0.01,"vmin":0},{"hmax":0.0232,"hmin":0.0113,"vmax":0.01,"vmin":0},{"hmax":0.01,"hmin":0,"vmax":0.0413,"vmin":0.02},{"hmax":0.01,"hmin":0,"vmax":0.0627,"vmin":0.0413},{"hmax":0.01,"hmin":0,"vmax":0.084,"vmin":0.0627},{"hmax":0.01,"hmin":0,"vmax":0.1053,"vmin":0.084},{"hmax":0.01,"hmin":0,"vmax":0.1267,"vmin":0.1053},{"hmax":0.01,"hmin":0,"vmax":0.148,"vmin":0.1267},{"hmax":0.01,"hmin":0,"vmax":0.1693,"vmin":0.148},{"hmax":0.01,"hmin":0,"vmax":0.1907,"vmin":0.1693},{"hmax":0.01,"hmin":0,"vmax":0.212,"vmin":0.1907},{"hmax":0.01,"hmin":0,"vmax":0.2333,"vmin":0.212},{"hmax":0.01,"hmin":0,"vmax":0.2547,"vmin":0.2333},{"hmax":0.01,"hmin":0,"vmax":0.276,"vmin":0.2547},{"hmax":0.01,"hmin":0,"vmax":0.2973,"vmin":0.276},{"hmax":0.01,"hmin":0,"vmax":0.3187,"vmin":0.2973},{"hmax":0.01,"hmin":0,"vmax":0.34,"vmin":0.3187},{"hmax":0.01,"hmin":0,"vmax":0.3613,"vmin":0.34},{"hmax":0.01,"hmin":0,"vmax":0.3827,"vmin":0.3613},{"hmax":0.01,"hmin":0,"vmax":0.404,"vmin":0.3827},{"hmax":0.01,"hmin":0,"vmax":0.4253,"vmin":0.404},{"hmax":0.01,"hmin":0,"vmax":0.4467,"vmin":0.4253},{"hmax":0.01,"hmin":0,"vmax":0.468,"vmin":0.4467},{"hmax":0.01,"hmin":0,"vmax":0.4893,"vmin":0.468},{"hmax":0.01,"hmin":0,"vmax":0.5107,"vmin":0.4893},{"hmax":0.01,"hmin":0,"vmax":0.532,"vmin":0.5107},{"hmax":0.01,"hmin":0,"vmax":0.5533,"vmin":0.532},{"hmax":0.01,"hmin":0,"vmax":0.5747,"vmin":0.5533},{"hmax":0.01,"hmin":0,"vmax":0.596,"vmin":0.5747},{"hmax":0.01,"hmin":0,"vmax":0.6173,"vmin":0.596},{"hmax":0.01,"hmin":0,"vmax":0.6387,"vmin":0.6173},{"hmax":0.01,"hmin":0,"vmax":0.66,"vmin":0.6387},{"hmax":0.01,"hmin":0,"vmax":0.6813,"vmin":0.66},{"hmax":0.01,"hmin":0,"vmax":0.7027,"vmin":0.6813},{"hmax":0.01,"hmin":0,"vmax":0.724,"vmin":0.7027},{"hmax":0.01,"hmin":0,"vmax":0.7453,"vmin":0.724},{"hmax":0.01,"hmin":0,"vmax":0.7667,"vmin":0.7453},{"hmax":0.01,"hmin":0,"vmax":0.788,"vmin":0.7667},{"hmax":0.01,"hmin":0,"vmax":0.8093,"vmin":0.788},{"hmax":0.01,"hmin":0,"vmax":0.8307,"vmin":0.8093},{"hmax":0.01,"hmin":0,"vmax":0.852,"vmin":0.8307},{"hmax":0.01,"hmin":0,"vmax":0.8733,"vmin":0.852},{"hmax":0.01,"hmin":0,"vmax":0.8947,"vmin":0.8733},{"hmax":0.01,"hmin":0,"vmax":0.916,"vmin":0.8947},{"hmax":0.01,"hmin":0,"vmax":0.9373,"vmin":0.916},{"hmax":0.01,"hmin":0,"vmax":0.9587,"vmin":0.9373},{"hmax":0.01,"hmin":0,"vmax":0.98,"vmin":0.9587},{"hmax":0.0232,"hmin":0.0113,"vmax":1,"vmin":0.99},{"hmax":0.0351,"hmin":0.0232,"vmax":1,"vmin":0.99},{"hmax":0.047,"hmin":0.0351,"vmax":1,"vmin":0.99},{"hmax":0.0589,"hmin":0.047,"vmax":1,"vmin":0.99},{"hmax":0.0709,"hmin":0.0589,"vmax":1,"vmin":0.99},{"hmax":0.0828,"hmin":0.0709,"vmax":1,"vmin":0.99},{"hmax":0.0947,"hmin":0.0828,"vmax":1,"vmin":0.99},{"hmax":0.1066,"hmin":0.0947,"vmax":1,"vmin":0.99},{"hmax":0.1185,"hmin":0.1066,"vmax":1,"vmin":0.99},{"hmax":0.1305,"hmin":0.1185,"vmax":1,"vmin":0.99},{"hmax":0.1424,"hmin":0.1305,"vmax":1,"vmin":0.99},{"hmax":0.1543,"hmin":0.1424,"vmax":1,"vmin":0.99},{"hmax":0.1662,"hmin":0.1543,"vmax":1,"vmin":0.99},{"hmax":0.1781,"hmin":0.1662,"vmax":1,"vmin":0.99},{"hmax":0.1901,"hmin":0.1781,"vmax":1,"vmin":0.99},{"hmax":0.202,"hmin":0.1901,"vmax":1,"vmin":0.99},{"hmax":0.2139,"hmin":0.202,"vmax":1,"vmin":0.99},{"hmax":0.2258,"hmin":0.2139,"vmax":1,"vmin":0.99},{"hmax":0.2377,"hmin":0.2258,"vmax":1,"vmin":0.99},{"hmax":0.2497,"hmin":0.2377,"vmax":1,"vmin":0.99},{"hmax":0.2616,"hmin":0.2497,"vmax":1,"vmin":0.99},{"hmax":0.2735,"hmin":0.2616,"vmax":1,"vmin":0.99},{"hmax":0.2854,"hmin":0.2735,"vmax":1,"vmin":0.99},{"hmax":0.2973,"hmin":0.2854,"vmax":1,"vmin":0.99},{"hmax":0.3093,"hmin":0.2973,"vmax":1,"vmin":0.99},{"hmax":0.3212,"hmin":0.3093,"vmax":1,"vmin":0.99},{"hmax":0.3331,"hmin":0.3212,"vmax":1,"vmin":0.99},{"hmax":0.345,"hmin":0.3331,"vmax":1,"vmin":0.99},{"hmax":0.357,"hmin":0.345,"vmax":1,"vmin":0.99},{"hmax":0.3689,"hmin":0.357,"vmax":1,"vmin":0.99},{"hmax":0.3808,"hmin":0.3689,"vmax":1,"vmin":0.99},{"hmax":0.3927,"hmin":0.3808,"vmax":1,"vmin":0.99},{"hmax":0.4046,"hmin":0.3927,"vmax":1,"vmin":0.99},{"hmax":0.4166,"hmin":0.4046,"vmax":1,"vmin":0.99},{"hmax":0.4285,"hmin":0.4166,"vmax":1,"vmin":0.99},{"hmax":0.4404,"hmin":0.4285,"vmax":1,"vmin":0.99},{"hmax":0.4523,"hmin":0.4404,"vmax":1,"vmin":0.99},{"hmax":0.4642,"hmin":0.4523,"vmax":1,"vmin":0.99},{"hmax":0.4762,"hmin":0.4642,"vmax":1,"vmin":0.99},{"hmax":0.4881,"hmin":0.4762,"vmax":1,"vmin":0.99},{"hmax":0.5,"hmin":0.4881,"vmax":1,"vmin":0.99},{"hmax":0.5119,"hmin":0.5,"vmax":1,"vmin":0.99},{"hmax":0.5238,"hmin":0.5119,"vmax":1,"vmin":0.99},{"hmax":0.5358,"hmin":0.5238,"vmax":1,"vmin":0.99},{"hmax":0.5477,"hmin":0.5358,"vmax":1,"vmin":0.99},{"hmax":0.5596,"hmin":0.5477,"vmax":1,"vmin":0.99},{"hmax":0.5715,"hmin":0.5596,"vmax":1,"vmin":0.99},{"hmax":0.5834,"hmin":0.5715,"vmax":1,"vmin":0.99},{"hmax":0.5954,"hmin":0.5834,"vmax":1,"vmin":0.99},{"hmax":0.6073,"hmin":0.5954,"vmax":1,"vmin":0.99},{"hmax":0.6192,"hmin":0.6073,"vmax":1,"vmin":0.99},{"hmax":0.6311,"hmin":0.6192,"vmax":1,"vmin":0.99},{"hmax":0.643,"hmin":0.6311,"vmax":1,"vmin":0.99},{"hmax":0.655,"hmin":0.643,"vmax":1,"vmin":0.99},{"hmax":0.6669,"hmin":0.655,"vmax":1,"vmin":0.99},{"hmax":0.6788,"hmin":0.6669,"vmax":1,"vmin":0.99},{"hmax":0.6907,"hmin":0.6788,"vmax":1,"vmin":0.99},{"hmax":0.7027,"hmin":0.6907,"vmax":1,"vmin":0.99},{"hmax":0.7146,"hmin":0.7027,"vmax":1,"vmin":0.99},{"hmax":0.7265,"hmin":0.7146,"vmax":1,"vmin":0.99},{"hmax":0.7384,"hmin":0.7265,"vmax":1,"vmin":0.99},{"hmax":0.7503,"hmin":0.7384,"vmax":1,"vmin":0.99},{"hmax":0.7623,"hmin":0.7503,"vmax":1,"vmin":0.99},{"hmax":0.7742,"hmin":0.7623,"vmax":1,"vmin":0.99},{"hmax":0.7861,"hmin":0.7742,"vmax":1,"vmin":0.99},{"hmax":0.798,"hmin":0.7861,"vmax":1,"vmin":0.99},{"hmax":0.8099,"hmin":0.798,"vmax":1,"vmin":0.99},{"hmax":0.8219,"hmin":0.8099,"vmax":1,"vmin":0.99},{"hmax":0.8338,"hmin":0.8219,"vmax":1,"vmin":0.99},{"hmax":0.8457,"hmin":0.8338,"vmax":1,"vmin":0.99},{"hmax":0.8576,"hmin":0.8457,"vmax":1,"vmin":0.99},{"hmax":0.8695,"hmin":0.8576,"vmax":1,"vmin":0.99},{"hmax":0.8815,"hmin":0.8695,"vmax":1,"vmin":0.99},{"hmax":0.8934,"hmin":0.8815,"vmax":1,"vmin":0.99},{"hmax":0.9053,"hmin":0.8934,"vmax":1,"vmin":0.99},{"hmax":0.9172,"hmin":0.9053,"vmax":1,"vmin":0.99},{"hmax":0.9291,"hmin":0.9172,"vmax":1,"vmin":0.99},{"hmax":0.9411,"hmin":0.9291,"vmax":1,"vmin":0.99},{"hmax":0.953,"hmin":0.9411,"vmax":1,"vmin":0.99},{"hmax":0.9649,"hmin":0.953,"vmax":1,"vmin":0.99},{"hmax":0.9768,"hmin":0.9649,"vmax":1,"vmin":0.99},{"hmax":0.9888,"hmin":0.9768,"vmax":1,"vmin":0.99},{"hmax":1,"hmin":0.99,"vmax":0.98,"vmin":0.9587},{"hmax":1,"hmin":0.99,"vmax":0.9587,"vmin":0.9373},{"hmax":1,"hmin":0.99,"vmax":0.9373,"vmin":0.916},{"hmax":1,"hmin":0.99,"vmax":0.916,"vmin":0.8947},{"hmax":1,"hmin":0.99,"vmax":0.8947,"vmin":0.8733},{"hmax":1,"hmin":0.99,"vmax":0.8733,"vmin":0.852},{"hmax":1,"hmin":0.99,"vmax":0.852,"vmin":0.8307},{"hmax":1,"hmin":0.99,"vmax":0.8307,"vmin":0.8093},{"hmax":1,"hmin":0.99,"vmax":0.8093,"vmin":0.788},{"hmax":1,"hmin":0.99,"vmax":0.788,"vmin":0.7667},{"hmax":1,"hmin":0.99,"vmax":0.7667,"vmin":0.7453},{"hmax":1,"hmin":0.99,"vmax":0.7453,"vmin":0.724},{"hmax":1,"hmin":0.99,"vmax":0.724,"vmin":0.7027},{"hmax":1,"hmin":0.99,"vmax":0.7027,"vmin":0.6813},{"hmax":1,"hmin":0.99,"vmax":0.6813,"vmin":0.66},{"hmax":1,"hmin":0.99,"vmax":0.66,"vmin":0.6387},{"hmax":1,"hmin":0.99,"vmax":0.6387,"vmin":0.6173},{"hmax":1,"hmin":0.99,"vmax":0.6173,"vmin":0.596},{"hmax":1,"hmin":0.99,"vmax":0.596,"vmin":0.5747},{"hmax":1,"hmin":0.99,"vmax":0.5747,"vmin":0.5533},{"hmax":1,"hmin":0.99,"vmax":0.5533,"vmin":0.532},{"hmax":1,"hmin":0.99,"vmax":0.532,"vmin":0.5107},{"hmax":1,"hmin":0.99,"vmax":0.5107,"vmin":0.4893},{"hmax":1,"hmin":0.99,"vmax":0.4893,"vmin":0.468},{"hmax":1,"hmin":0.99,"vmax":0.468,"vmin":0.4467},{"hmax":1,"hmin":0.99,"vmax":0.4467,"vmin":0.4253},{"hmax":1,"hmin":0.99,"vmax":0.4253,"vmin":0.404},{"hmax":1,"hmin":0.99,"vmax":0.404,"vmin":0.3827},{"hmax":1,"hmin":0.99,"vmax":0.3827,"vmin":0.3613},{"hmax":1,"hmin":0.99,"vmax":0.3613,"vmin":0.34},{"hmax":1,"hmin":0.99,"vmax":0.34,"vmin":0.3187},{"hmax":1,"hmin":0.99,"vmax":0.3187,"vmin":0.2973},{"hmax":1,"hmin":0.99,"vmax":0.2973,"vmin":0.276},{"hmax":1,"hmin":0.99,"vmax":0.276,"vmin":0.2547},{"hmax":1,"hmin":0.99,"vmax":0.2547,"vmin":0.2333},{"hmax":1,"hmin":0.99,"vmax":0.2333,"vmin":0.212},{"hmax":1,"hmin":0.99,"vmax":0.212,"vmin":0.1907},{"hmax":1,"hmin":0.99,"vmax":0.1907,"vmin":0.1693},{"hmax":1,"hmin":0.99,"vmax":0.1693,"vmin":0.148},{"hmax":1,"hmin":0.99,"vmax":0.148,"vmin":0.1267},{"hmax":1,"hmin":0.99,"vmax":0.1267,"vmin":0.1053},{"hmax":1,"hmin":0.99,"vmax":0.1053,"vmin":0.084},{"hmax":1,"hmin":0.99,"vmax":0.084,"vmin":0.0627},{"hmax":1,"hmin":0.99,"vmax":0.0627,"vmin":0.0413},{"hmax":1,"hmin":0.99,"vmax":0.0413,"vmin":0.02}],"priorities":[{"active":true,"componentId":"V4L","origin":"System","owner":"V4L2:/dev/video1","priority":300,"visible":false},{"active":true,"componentId":"V4L","origin":"System","owner":"V4L2:/dev/video0","priority":240,"visible":true}],"priorities_autoselect":true,"sessions":[],"transform":[{"blacklevel":[0,0,0],"gamma":[2.5,2.5,2.5],"id":"default","luminanceGain":1,"luminanceMinimum":0,"saturationGain":1,"saturationLGain":1,"threshold":[0,0,0],"valueGain":1,"whitelevel":[1,1,1]}],"videomode":"2D"},"success":true,"tan":1} 2 | --------------------------------------------------------------------------------