├── .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 |
6 |
7 | [](https://pypi.org/project/hyperion-py/)
8 | [](https://pypi.org/project/hyperion-py/)
9 | [](https://github.com/dermotduffy/hyperion-py/actions/workflows/build.yaml)
10 | [](https://codecov.io/gh/dermotduffy/hyperion-py)
11 | [](LICENSE)
12 | [](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 |
--------------------------------------------------------------------------------