updates
This commit is contained in:
parent
9b39f1af50
commit
c74c45998c
8
deps/bin/eq3cli
vendored
Executable file
8
deps/bin/eq3cli
vendored
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/local/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from eq3bt.eq3cli import cli
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(cli())
|
225
deps/lib/python3.10/site-packages/CHANGELOG
vendored
Normal file
225
deps/lib/python3.10/site-packages/CHANGELOG
vendored
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
0.2 (2022-07-13)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- Add publish to pypi workflow (#54) [Teemu R]
|
||||||
|
|
||||||
|
- Add bleak backend and make it default (#53) [Teemu R]
|
||||||
|
|
||||||
|
- Wrap backend exceptions inside BackendException (#52) [Teemu R]
|
||||||
|
|
||||||
|
- Add mac property to thermostat class (#51) [Teemu R]
|
||||||
|
|
||||||
|
- Update README, pyproject.toml (#49) [Teemu R]
|
||||||
|
|
||||||
|
- Support gattlib as an alternative btle library (#48) [Helmut Grohne]
|
||||||
|
|
||||||
|
- Use poetry, add pre-commit hooks & mass format to modern standards,
|
||||||
|
add CI (#47) [Teemu R]
|
||||||
|
|
||||||
|
|
||||||
|
0.1.12 (2021-11-13)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Add bt interface selection (#44) [Hummel95]
|
||||||
|
|
||||||
|
0.1.11 (2019-05-27)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Decoding presets in status messages (#33) [Matthias Erll]
|
||||||
|
|
||||||
|
- Adding device serial number and firmware (#31) [Matthias Erll]
|
||||||
|
|
||||||
|
- Context.invoke() -> Context.forward() (#28) [Till]
|
||||||
|
|
||||||
|
- Require python 3.4 or newer in setup.py, closes #23. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
|
||||||
|
0.1.10 (2018-11-09))
|
||||||
|
------------------------
|
||||||
|
- Context.invoke() -> Context.forward() (#28) [Till]
|
||||||
|
- Require python 3.4 or newer in setup.py, closes #23. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
|
||||||
|
0.1.9 (2018-02-18)
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- Update to the new construct API (#20) [Arkadiusz Bulski]
|
||||||
|
|
||||||
|
|
||||||
|
0.1.8 (2018-01-20)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Update to work with the newest construct release, bump version. [Teemu
|
||||||
|
Rytilahti]
|
||||||
|
|
||||||
|
- Update schedule example, fixes #15. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Do not suppress exceptions from bluepy, but log them to debug logger
|
||||||
|
and raise exceptions for users to handle. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Install flake8 and pylint, which are required for the travis build.
|
||||||
|
[Teemu Rytilahti]
|
||||||
|
|
||||||
|
0.1.7 (2017-10-06)
|
||||||
|
------------------------
|
||||||
|
- Fixed setting schedule not working (#9) [horsitis
|
||||||
|
|
||||||
|
0.1.6 (2017-04-01)
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- Version 0.1.6. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Use debug logging for the first round of connection error. [Teemu
|
||||||
|
Rytilahti]
|
||||||
|
|
||||||
|
- Disallow running with python versions older than 3.4. [Teemu
|
||||||
|
Rytilahti]
|
||||||
|
|
||||||
|
The library _may_ still be python2 compatible though for now,
|
||||||
|
but this is unsupported and should not be relied on.
|
||||||
|
|
||||||
|
- On/Off/Manual mode fixes (#6) [Janne Grunau]
|
||||||
|
|
||||||
|
* Handle On/Off mode correctly
|
||||||
|
|
||||||
|
* Set temperature in [EQ3BT_MIN_TEMP, EQ3BT_MAX_TEMP] for manual mode
|
||||||
|
|
||||||
|
* simplify mode setter function
|
||||||
|
|
||||||
|
- Be less noisy on connection errors. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Require and validate mac address at the cli (#4) [Klemens Schölhorn]
|
||||||
|
|
||||||
|
- Add missing structures.py. this was already in pypi package, so no
|
||||||
|
harm done. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
|
||||||
|
0.1.5 (2017-01-28)
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- Version 0.1.5. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Fix manual on/off handling, cleanup the code for next release. [Teemu
|
||||||
|
Rytilahti]
|
||||||
|
|
||||||
|
- Make Thermostat testable. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Use less magic constants and more structures, fix manual mode setting.
|
||||||
|
[Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Fix setup.py typo. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Eq3cli: add away command. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Restructuring with construct for more readable code. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
* add set_away(away_ends, temperature) for enabling and disabling away mode
|
||||||
|
|
||||||
|
- Add hound-ci config. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
|
||||||
|
0.1.4 (2017-01-15)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Version 0.1.4. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Add away_end property. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Add changelog. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
0.1.3 (2017-01-15)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Make eq3bt a module. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Update README. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Add scheduling and offset functionality. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Connection: pretty print messaging in hex. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Setup.py: fix console script location. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
0.1.2 (2017-01-14)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fix packaging, add click dependency, bump to 0.1.2. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Bump bluepy requirement to 1.0.5. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
0.1 (2017-01-14)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- Restructure bluepy_devices to python-eq3bt. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
* Complete restructure of the library. All unnecessary and problematic parts are dropped.
|
||||||
|
* General cleaning up, making flake8 and pylint happy.
|
||||||
|
* Updated and documented cli tool, named eq3cli
|
||||||
|
|
||||||
|
- Add contextmanager for connection to simplify connecting and
|
||||||
|
disconnecting. Calling writes on device will build and tear down the
|
||||||
|
connection automatically. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Eq3btsmart: do not try to connect on init, allows adding the component
|
||||||
|
to homeassistant even if the device is not connectable at the moment.
|
||||||
|
[Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Add eq3cli tool. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
Included command-line tool can be used to control the device.
|
||||||
|
All current functionality is available through it, check updated README.md for usage.
|
||||||
|
|
||||||
|
- Add logger to ease debugging. [Teemu Rytilahti]
|
||||||
|
|
||||||
|
- Increase version to 0.3.0 for the enhaced eq3btsmart support. [Janne
|
||||||
|
Grunau]
|
||||||
|
|
||||||
|
- Eq3btsmart: and support for the comfort and eco temperature presets.
|
||||||
|
[Janne Grunau]
|
||||||
|
|
||||||
|
- Eq3btsmart: add a property for the low battery warning. [Janne Grunau]
|
||||||
|
|
||||||
|
- Eq3btsmart: add support for the thermostat's operating lock. [Janne
|
||||||
|
Grunau]
|
||||||
|
|
||||||
|
- Eq3btsmart: add window open mode configuration. [Janne Grunau]
|
||||||
|
|
||||||
|
- Eq3btsmart: and property to check window open state. [Janne Grunau]
|
||||||
|
|
||||||
|
- Eq3btsmart: report valve state. [Janne Grunau]
|
||||||
|
|
||||||
|
- Eq3btsmart: control the supported modes of the thermostat. [Janne
|
||||||
|
Grunau]
|
||||||
|
|
||||||
|
The away mode is not really useful yet.
|
||||||
|
|
||||||
|
- Eq3btsmart: verify that temperatures are in min/max range. [Janne
|
||||||
|
Grunau]
|
||||||
|
|
||||||
|
- Eq3btsmart: fix the minimal and maximal temperatures. [Janne Grunau]
|
||||||
|
|
||||||
|
4.5 and 30 degree celsius have special meanings and can't be set
|
||||||
|
in 'auto' mode. 4.5 means off (valve closed even if the temperature
|
||||||
|
below 4.5 degress). 30 means on (valve permanently open even if the
|
||||||
|
temperature exceeds 30 degrees).
|
||||||
|
|
||||||
|
- Eq3btsmart: the update request needs to include the full time. [Janne
|
||||||
|
Grunau]
|
||||||
|
|
||||||
|
Otherwise the thermostat can set a random time. Also fixes the format of
|
||||||
|
the set time request.
|
||||||
|
|
||||||
|
- Initial update in eq3btsmart.py. [Markus Peter]
|
||||||
|
|
||||||
|
- +travis. [Markus Peter]
|
||||||
|
|
||||||
|
- Update README.md. [Markus Peter]
|
||||||
|
|
||||||
|
- Create README.md. [Markus Peter]
|
||||||
|
|
||||||
|
- Initial Commit Version 0.2.0. [Markus Peter]
|
||||||
|
|
||||||
|
- Initial commit. [Markus Peter]
|
1
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/INSTALLER
vendored
Normal file
1
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/INSTALLER
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
pip
|
202
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/LICENSE
vendored
Normal file
202
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/LICENSE
vendored
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
239
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/METADATA
vendored
Normal file
239
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/METADATA
vendored
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: aiofiles
|
||||||
|
Version: 0.8.0
|
||||||
|
Summary: File support for asyncio.
|
||||||
|
License: Apache-2.0
|
||||||
|
Author: Tin Tvrtkovic
|
||||||
|
Author-email: tinchester@gmail.com
|
||||||
|
Requires-Python: >=3.6,<4.0
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
|
||||||
|
aiofiles: file support for asyncio
|
||||||
|
==================================
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/aiofiles.svg
|
||||||
|
:target: https://pypi.python.org/pypi/aiofiles
|
||||||
|
|
||||||
|
.. image:: https://travis-ci.org/Tinche/aiofiles.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/Tinche/aiofiles
|
||||||
|
|
||||||
|
.. image:: https://codecov.io/gh/Tinche/aiofiles/branch/master/graph/badge.svg
|
||||||
|
:target: https://codecov.io/gh/Tinche/aiofiles
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/aiofiles.svg
|
||||||
|
:target: https://github.com/Tinche/aiofiles
|
||||||
|
:alt: Supported Python versions
|
||||||
|
|
||||||
|
**aiofiles** is an Apache2 licensed library, written in Python, for handling local
|
||||||
|
disk files in asyncio applications.
|
||||||
|
|
||||||
|
Ordinary local file IO is blocking, and cannot easily and portably made
|
||||||
|
asynchronous. This means doing file IO may interfere with asyncio applications,
|
||||||
|
which shouldn't block the executing thread. aiofiles helps with this by
|
||||||
|
introducing asynchronous versions of files that support delegating operations to
|
||||||
|
a separate thread pool.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async with aiofiles.open('filename', mode='r') as f:
|
||||||
|
contents = await f.read()
|
||||||
|
print(contents)
|
||||||
|
'My file contents'
|
||||||
|
|
||||||
|
Asynchronous iteration is also supported.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async with aiofiles.open('filename') as f:
|
||||||
|
async for line in f:
|
||||||
|
...
|
||||||
|
|
||||||
|
Asynchronous interface to tempfile module.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async with aiofiles.tempfile.TemporaryFile('wb') as f:
|
||||||
|
await f.write(b'Hello, World!')
|
||||||
|
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- a file API very similar to Python's standard, blocking API
|
||||||
|
- support for buffered and unbuffered binary files, and buffered text files
|
||||||
|
- support for ``async``/``await`` (:PEP:`492`) constructs
|
||||||
|
- async interface to tempfile module
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
To install aiofiles, simply:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip install aiofiles
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Files are opened using the ``aiofiles.open()`` coroutine, which in addition to
|
||||||
|
mirroring the builtin ``open`` accepts optional ``loop`` and ``executor``
|
||||||
|
arguments. If ``loop`` is absent, the default loop will be used, as per the
|
||||||
|
set asyncio policy. If ``executor`` is not specified, the default event loop
|
||||||
|
executor will be used.
|
||||||
|
|
||||||
|
In case of success, an asynchronous file object is returned with an
|
||||||
|
API identical to an ordinary file, except the following methods are coroutines
|
||||||
|
and delegate to an executor:
|
||||||
|
|
||||||
|
* ``close``
|
||||||
|
* ``flush``
|
||||||
|
* ``isatty``
|
||||||
|
* ``read``
|
||||||
|
* ``readall``
|
||||||
|
* ``read1``
|
||||||
|
* ``readinto``
|
||||||
|
* ``readline``
|
||||||
|
* ``readlines``
|
||||||
|
* ``seek``
|
||||||
|
* ``seekable``
|
||||||
|
* ``tell``
|
||||||
|
* ``truncate``
|
||||||
|
* ``writable``
|
||||||
|
* ``write``
|
||||||
|
* ``writelines``
|
||||||
|
|
||||||
|
In case of failure, one of the usual exceptions will be raised.
|
||||||
|
|
||||||
|
The ``aiofiles.os`` module contains executor-enabled coroutine versions of
|
||||||
|
several useful ``os`` functions that deal with files:
|
||||||
|
|
||||||
|
* ``stat``
|
||||||
|
* ``sendfile``
|
||||||
|
* ``rename``
|
||||||
|
* ``replace``
|
||||||
|
* ``remove``
|
||||||
|
* ``mkdir``
|
||||||
|
* ``makedirs``
|
||||||
|
* ``rmdir``
|
||||||
|
* ``removedirs``
|
||||||
|
* ``path.exists``
|
||||||
|
* ``path.isfile``
|
||||||
|
* ``path.isdir``
|
||||||
|
* ``path.getsize``
|
||||||
|
* ``path.getatime``
|
||||||
|
* ``path.getctime``
|
||||||
|
* ``path.samefile``
|
||||||
|
* ``path.sameopenfile``
|
||||||
|
|
||||||
|
Tempfile
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
**aiofiles.tempfile** implements the following interfaces:
|
||||||
|
|
||||||
|
- TemporaryFile
|
||||||
|
- NamedTemporaryFile
|
||||||
|
- SpooledTemporaryFile
|
||||||
|
- TemporaryDirectory
|
||||||
|
|
||||||
|
Results return wrapped with a context manager allowing use with async with and async for.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async with aiofiles.tempfile.NamedTemporaryFile('wb+') as f:
|
||||||
|
await f.write(b'Line1\n Line2')
|
||||||
|
await f.seek(0)
|
||||||
|
async for line in f:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
async with aiofiles.tempfile.TemporaryDirectory() as d:
|
||||||
|
filename = os.path.join(d, "file.ext")
|
||||||
|
|
||||||
|
|
||||||
|
Writing tests for aiofiles
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Real file IO can be mocked by patching ``aiofiles.threadpool.sync_open``
|
||||||
|
as desired. The return type also needs to be registered with the
|
||||||
|
``aiofiles.threadpool.wrap`` dispatcher:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
aiofiles.threadpool.wrap.register(mock.MagicMock)(
|
||||||
|
lambda *args, **kwargs: threadpool.AsyncBufferedIOBase(*args, **kwargs))
|
||||||
|
|
||||||
|
async def test_stuff():
|
||||||
|
data = 'data'
|
||||||
|
mock_file = mock.MagicMock()
|
||||||
|
|
||||||
|
with mock.patch('aiofiles.threadpool.sync_open', return_value=mock_file) as mock_open:
|
||||||
|
async with aiofiles.open('filename', 'w') as f:
|
||||||
|
await f.write(data)
|
||||||
|
|
||||||
|
mock_file.write.assert_called_once_with(data)
|
||||||
|
|
||||||
|
History
|
||||||
|
~~~~~~~
|
||||||
|
0.8.0 (2021-11-27)
|
||||||
|
``````````````````
|
||||||
|
* aiofiles is now tested on Python 3.10.
|
||||||
|
* Added ``aiofiles.os.replace``.
|
||||||
|
`#107 <https://github.com/Tinche/aiofiles/pull/107>`_
|
||||||
|
* Added ``aiofiles.os.{makedirs, removedirs}``.
|
||||||
|
* Added ``aiofiles.os.path.{exists, isfile, isdir, getsize, getatime, getctime, samefile, sameopenfile}``.
|
||||||
|
`#63 <https://github.com/Tinche/aiofiles/pull/63>`_
|
||||||
|
* Added `suffix`, `prefix`, `dir` args to ``aiofiles.tempfile.TemporaryDirectory``.
|
||||||
|
`#116 <https://github.com/Tinche/aiofiles/pull/116>`_
|
||||||
|
|
||||||
|
0.7.0 (2021-05-17)
|
||||||
|
``````````````````
|
||||||
|
- Added the ``aiofiles.tempfile`` module for async temporary files.
|
||||||
|
`#56 <https://github.com/Tinche/aiofiles/pull/56>`_
|
||||||
|
- Switched to Poetry and GitHub actions.
|
||||||
|
- Dropped 3.5 support.
|
||||||
|
|
||||||
|
0.6.0 (2020-10-27)
|
||||||
|
``````````````````
|
||||||
|
- `aiofiles` is now tested on ppc64le.
|
||||||
|
- Added `name` and `mode` properties to async file objects.
|
||||||
|
`#82 <https://github.com/Tinche/aiofiles/pull/82>`_
|
||||||
|
- Fixed a DeprecationWarning internally.
|
||||||
|
`#75 <https://github.com/Tinche/aiofiles/pull/75>`_
|
||||||
|
- Python 3.9 support and tests.
|
||||||
|
|
||||||
|
0.5.0 (2020-04-12)
|
||||||
|
``````````````````
|
||||||
|
- Python 3.8 support. Code base modernization (using ``async/await`` instead of ``asyncio.coroutine``/``yield from``).
|
||||||
|
- Added ``aiofiles.os.remove``, ``aiofiles.os.rename``, ``aiofiles.os.mkdir``, ``aiofiles.os.rmdir``.
|
||||||
|
`#62 <https://github.com/Tinche/aiofiles/pull/62>`_
|
||||||
|
|
||||||
|
|
||||||
|
0.4.0 (2018-08-11)
|
||||||
|
``````````````````
|
||||||
|
- Python 3.7 support.
|
||||||
|
- Removed Python 3.3/3.4 support. If you use these versions, stick to aiofiles 0.3.x.
|
||||||
|
|
||||||
|
0.3.2 (2017-09-23)
|
||||||
|
``````````````````
|
||||||
|
- The LICENSE is now included in the sdist.
|
||||||
|
`#31 <https://github.com/Tinche/aiofiles/pull/31>`_
|
||||||
|
|
||||||
|
0.3.1 (2017-03-10)
|
||||||
|
``````````````````
|
||||||
|
|
||||||
|
- Introduced a changelog.
|
||||||
|
- ``aiofiles.os.sendfile`` will now work if the standard ``os`` module contains a ``sendfile`` function.
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
Contributions are very welcome. Tests can be run with ``tox``, please ensure
|
||||||
|
the coverage at least stays the same before you submit a pull request.
|
||||||
|
|
26
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/RECORD
vendored
Normal file
26
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/RECORD
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
aiofiles-0.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
aiofiles-0.8.0.dist-info/LICENSE,sha256=y16Ofl9KOYjhBjwULGDcLfdWBfTEZRXnduOspt-XbhQ,11325
|
||||||
|
aiofiles-0.8.0.dist-info/METADATA,sha256=9WSIVaJhAbMi93mj4ir3932H5Je3JxAcUvH5hgyD0qU,7022
|
||||||
|
aiofiles-0.8.0.dist-info/RECORD,,
|
||||||
|
aiofiles-0.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
aiofiles-0.8.0.dist-info/WHEEL,sha256=FMw6u7Kp3jrdDDtJRZ3hkPV-9adLQT8pbM4iifKUGfw,85
|
||||||
|
aiofiles/__init__.py,sha256=6fPaAw6PXV8rszheTedkYcoLHC3KbbPH--eMODJ_-IE,136
|
||||||
|
aiofiles/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
aiofiles/__pycache__/base.cpython-310.pyc,,
|
||||||
|
aiofiles/__pycache__/os.cpython-310.pyc,,
|
||||||
|
aiofiles/__pycache__/ospath.cpython-310.pyc,,
|
||||||
|
aiofiles/base.py,sha256=_ntjFm4olO13y9JEQchLudmgPJ4mAFug5MRi6Efk7g0,2114
|
||||||
|
aiofiles/os.py,sha256=A8vZ0paqH14JaepvEdfSCv6BAFoV4D1LVmnKfkEG5tQ,719
|
||||||
|
aiofiles/ospath.py,sha256=cIyDMoGVHTqKoKFXfXVzuIROJQQRmeNqVuNuc9QgPY0,387
|
||||||
|
aiofiles/tempfile/__init__.py,sha256=y6siScSCxNkydsoAN0OlxSwvgqpk1XoVTvJ0gTvAmTk,7234
|
||||||
|
aiofiles/tempfile/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
aiofiles/tempfile/__pycache__/temptypes.cpython-310.pyc,,
|
||||||
|
aiofiles/tempfile/temptypes.py,sha256=ZwWLAV3eiWWW0QoRDPHN2U7XPf0DUQFYPRid7o4Sk9s,2169
|
||||||
|
aiofiles/threadpool/__init__.py,sha256=JaRiXZRaVvx32RvrUXwgZxAByrzqdmDcxi6t_P2iP6w,2273
|
||||||
|
aiofiles/threadpool/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
aiofiles/threadpool/__pycache__/binary.cpython-310.pyc,,
|
||||||
|
aiofiles/threadpool/__pycache__/text.cpython-310.pyc,,
|
||||||
|
aiofiles/threadpool/__pycache__/utils.cpython-310.pyc,,
|
||||||
|
aiofiles/threadpool/binary.py,sha256=tRdJnH6ragF5Kr13oIBPJrljgTl3hWSOaHSXfHESRBk,1167
|
||||||
|
aiofiles/threadpool/text.py,sha256=dNweKCxpwRgfqA6XCiWYDDddOTa0lbzH-Fh5o3rho-8,665
|
||||||
|
aiofiles/threadpool/utils.py,sha256=fcqvRBrcIk2qP-rOdm92zDHyCfgD6itS621V6oOLJwk,1912
|
0
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/REQUESTED
vendored
Normal file
0
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/REQUESTED
vendored
Normal file
4
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/WHEEL
vendored
Normal file
4
deps/lib/python3.10/site-packages/aiofiles-0.8.0.dist-info/WHEEL
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: poetry 1.1.0a6
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
5
deps/lib/python3.10/site-packages/aiofiles/__init__.py
vendored
Normal file
5
deps/lib/python3.10/site-packages/aiofiles/__init__.py
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Utilities for asyncio-friendly file handling."""
|
||||||
|
from .threadpool import open
|
||||||
|
from . import tempfile
|
||||||
|
|
||||||
|
__all__ = ["open", "tempfile"]
|
BIN
deps/lib/python3.10/site-packages/aiofiles/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/aiofiles/__pycache__/base.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/__pycache__/base.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/aiofiles/__pycache__/os.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/__pycache__/os.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/aiofiles/__pycache__/ospath.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/__pycache__/ospath.cpython-310.pyc
vendored
Normal file
Binary file not shown.
91
deps/lib/python3.10/site-packages/aiofiles/base.py
vendored
Normal file
91
deps/lib/python3.10/site-packages/aiofiles/base.py
vendored
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"""Various base classes."""
|
||||||
|
from types import coroutine
|
||||||
|
from collections.abc import Coroutine
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncBase:
|
||||||
|
def __init__(self, file, loop, executor):
|
||||||
|
self._file = file
|
||||||
|
self._loop = loop
|
||||||
|
self._executor = executor
|
||||||
|
|
||||||
|
def __aiter__(self):
|
||||||
|
"""We are our own iterator."""
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return super().__repr__() + " wrapping " + repr(self._file)
|
||||||
|
|
||||||
|
async def __anext__(self):
|
||||||
|
"""Simulate normal file iteration."""
|
||||||
|
line = await self.readline()
|
||||||
|
if line:
|
||||||
|
return line
|
||||||
|
else:
|
||||||
|
raise StopAsyncIteration
|
||||||
|
|
||||||
|
|
||||||
|
class _ContextManager(Coroutine):
|
||||||
|
__slots__ = ("_coro", "_obj")
|
||||||
|
|
||||||
|
def __init__(self, coro):
|
||||||
|
self._coro = coro
|
||||||
|
self._obj = None
|
||||||
|
|
||||||
|
def send(self, value):
|
||||||
|
return self._coro.send(value)
|
||||||
|
|
||||||
|
def throw(self, typ, val=None, tb=None):
|
||||||
|
if val is None:
|
||||||
|
return self._coro.throw(typ)
|
||||||
|
elif tb is None:
|
||||||
|
return self._coro.throw(typ, val)
|
||||||
|
else:
|
||||||
|
return self._coro.throw(typ, val, tb)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return self._coro.close()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gi_frame(self):
|
||||||
|
return self._coro.gi_frame
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gi_running(self):
|
||||||
|
return self._coro.gi_running
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gi_code(self):
|
||||||
|
return self._coro.gi_code
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
return self.send(None)
|
||||||
|
|
||||||
|
@coroutine
|
||||||
|
def __iter__(self):
|
||||||
|
resp = yield from self._coro
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def __await__(self):
|
||||||
|
resp = yield from self._coro
|
||||||
|
return resp
|
||||||
|
|
||||||
|
async def __anext__(self):
|
||||||
|
resp = await self._coro
|
||||||
|
return resp
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self._obj = await self._coro
|
||||||
|
return self._obj
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
self._obj.close()
|
||||||
|
self._obj = None
|
||||||
|
|
||||||
|
|
||||||
|
class AiofilesContextManager(_ContextManager):
|
||||||
|
"""An adjusted async context manager for aiofiles."""
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self._obj.close()
|
||||||
|
self._obj = None
|
31
deps/lib/python3.10/site-packages/aiofiles/os.py
vendored
Normal file
31
deps/lib/python3.10/site-packages/aiofiles/os.py
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""Async executor versions of file functions from the os module."""
|
||||||
|
import asyncio
|
||||||
|
from functools import partial, wraps
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def wrap(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def run(*args, loop=None, executor=None, **kwargs):
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
pfunc = partial(func, *args, **kwargs)
|
||||||
|
return await loop.run_in_executor(executor, pfunc)
|
||||||
|
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
from . import ospath as path
|
||||||
|
|
||||||
|
|
||||||
|
stat = wrap(os.stat)
|
||||||
|
rename = wrap(os.rename)
|
||||||
|
replace = wrap(os.replace)
|
||||||
|
remove = wrap(os.remove)
|
||||||
|
mkdir = wrap(os.mkdir)
|
||||||
|
makedirs = wrap(os.makedirs)
|
||||||
|
rmdir = wrap(os.rmdir)
|
||||||
|
removedirs = wrap(os.removedirs)
|
||||||
|
|
||||||
|
if hasattr(os, "sendfile"):
|
||||||
|
sendfile = wrap(os.sendfile)
|
14
deps/lib/python3.10/site-packages/aiofiles/ospath.py
vendored
Normal file
14
deps/lib/python3.10/site-packages/aiofiles/ospath.py
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""Async executor versions of file functions from the os.path module."""
|
||||||
|
|
||||||
|
from .os import wrap
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
exists = wrap(path.exists)
|
||||||
|
isfile = wrap(path.isfile)
|
||||||
|
isdir = wrap(path.isdir)
|
||||||
|
getsize = wrap(path.getsize)
|
||||||
|
getmtime = wrap(path.getmtime)
|
||||||
|
getatime = wrap(path.getatime)
|
||||||
|
getctime = wrap(path.getctime)
|
||||||
|
samefile = wrap(path.samefile)
|
||||||
|
sameopenfile = wrap(path.sameopenfile)
|
263
deps/lib/python3.10/site-packages/aiofiles/tempfile/__init__.py
vendored
Normal file
263
deps/lib/python3.10/site-packages/aiofiles/tempfile/__init__.py
vendored
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
# Imports
|
||||||
|
import asyncio
|
||||||
|
from tempfile import (
|
||||||
|
TemporaryFile as syncTemporaryFile,
|
||||||
|
NamedTemporaryFile as syncNamedTemporaryFile,
|
||||||
|
SpooledTemporaryFile as syncSpooledTemporaryFile,
|
||||||
|
TemporaryDirectory as syncTemporaryDirectory,
|
||||||
|
_TemporaryFileWrapper as syncTemporaryFileWrapper,
|
||||||
|
)
|
||||||
|
from io import FileIO, TextIOBase, BufferedReader, BufferedWriter, BufferedRandom
|
||||||
|
from functools import partial, singledispatch
|
||||||
|
from ..base import AiofilesContextManager
|
||||||
|
from ..threadpool.text import AsyncTextIOWrapper
|
||||||
|
from ..threadpool.binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO
|
||||||
|
from .temptypes import AsyncSpooledTemporaryFile, AsyncTemporaryDirectory
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"NamedTemporaryFile",
|
||||||
|
"TemporaryFile",
|
||||||
|
"SpooledTemporaryFile",
|
||||||
|
"TemporaryDirectory",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Public methods for async open and return of temp file/directory
|
||||||
|
# objects with async interface
|
||||||
|
# ================================================================
|
||||||
|
def NamedTemporaryFile(
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
delete=True,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Async open a named temporary file"""
|
||||||
|
return AiofilesContextManager(
|
||||||
|
_temporary_file(
|
||||||
|
named=True,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
delete=delete,
|
||||||
|
loop=loop,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def TemporaryFile(
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Async open an unnamed temporary file"""
|
||||||
|
return AiofilesContextManager(
|
||||||
|
_temporary_file(
|
||||||
|
named=False,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
loop=loop,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def SpooledTemporaryFile(
|
||||||
|
max_size=0,
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Async open a spooled temporary file"""
|
||||||
|
return AiofilesContextManager(
|
||||||
|
_spooled_temporary_file(
|
||||||
|
max_size=max_size,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
loop=loop,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def TemporaryDirectory(suffix=None, prefix=None, dir=None, loop=None, executor=None):
|
||||||
|
"""Async open a temporary directory"""
|
||||||
|
return AiofilesContextManagerTempDir(
|
||||||
|
_temporary_directory(
|
||||||
|
suffix=suffix, prefix=prefix, dir=dir, loop=loop, executor=executor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Internal coroutines to open new temp files/directories
|
||||||
|
# =========================================================
|
||||||
|
async def _temporary_file(
|
||||||
|
named=True,
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
delete=True,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
max_size=0,
|
||||||
|
):
|
||||||
|
"""Async method to open a temporary file with async interface"""
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
if named:
|
||||||
|
cb = partial(
|
||||||
|
syncNamedTemporaryFile,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
delete=delete,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cb = partial(
|
||||||
|
syncTemporaryFile,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
f = await loop.run_in_executor(executor, cb)
|
||||||
|
|
||||||
|
# Wrap based on type of underlying IO object
|
||||||
|
if type(f) is syncTemporaryFileWrapper:
|
||||||
|
# _TemporaryFileWrapper was used (named files)
|
||||||
|
result = wrap(f.file, f, loop=loop, executor=executor)
|
||||||
|
# add delete property
|
||||||
|
result.delete = f.delete
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
# IO object was returned directly without wrapper
|
||||||
|
return wrap(f, f, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
async def _spooled_temporary_file(
|
||||||
|
max_size=0,
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Open a spooled temporary file with async interface"""
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
cb = partial(
|
||||||
|
syncSpooledTemporaryFile,
|
||||||
|
max_size=max_size,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
f = await loop.run_in_executor(executor, cb)
|
||||||
|
|
||||||
|
# Single interface provided by SpooledTemporaryFile for all modes
|
||||||
|
return AsyncSpooledTemporaryFile(f, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
async def _temporary_directory(
|
||||||
|
suffix=None, prefix=None, dir=None, loop=None, executor=None
|
||||||
|
):
|
||||||
|
"""Async method to open a temporary directory with async interface"""
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
cb = partial(syncTemporaryDirectory, suffix, prefix, dir)
|
||||||
|
f = await loop.run_in_executor(executor, cb)
|
||||||
|
|
||||||
|
return AsyncTemporaryDirectory(f, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
class AiofilesContextManagerTempDir(AiofilesContextManager):
|
||||||
|
"""With returns the directory location, not the object (matching sync lib)"""
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self._obj = await self._coro
|
||||||
|
return self._obj.name
|
||||||
|
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def wrap(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
"""Wrap the object with interface based on type of underlying IO"""
|
||||||
|
raise TypeError("Unsupported IO type: {}".format(base_io_obj))
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(TextIOBase)
|
||||||
|
def _(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(BufferedWriter)
|
||||||
|
def _(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(BufferedReader)
|
||||||
|
@wrap.register(BufferedRandom)
|
||||||
|
def _(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
return AsyncBufferedReader(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(FileIO)
|
||||||
|
def _(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
return AsyncFileIO(file, loop=loop, executor=executor)
|
BIN
deps/lib/python3.10/site-packages/aiofiles/tempfile/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/tempfile/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/aiofiles/tempfile/__pycache__/temptypes.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/tempfile/__pycache__/temptypes.cpython-310.pyc
vendored
Normal file
Binary file not shown.
74
deps/lib/python3.10/site-packages/aiofiles/tempfile/temptypes.py
vendored
Normal file
74
deps/lib/python3.10/site-packages/aiofiles/tempfile/temptypes.py
vendored
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
"""Async wrappers for spooled temp files and temp directory objects"""
|
||||||
|
|
||||||
|
# Imports
|
||||||
|
import asyncio
|
||||||
|
from types import coroutine
|
||||||
|
|
||||||
|
from ..base import AsyncBase
|
||||||
|
from ..threadpool.utils import (
|
||||||
|
delegate_to_executor,
|
||||||
|
proxy_property_directly,
|
||||||
|
cond_delegate_to_executor,
|
||||||
|
)
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor("fileno", "rollover")
|
||||||
|
@cond_delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"newlines",
|
||||||
|
"read",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
)
|
||||||
|
@proxy_property_directly("closed", "encoding", "mode", "name", "softspace")
|
||||||
|
class AsyncSpooledTemporaryFile(AsyncBase):
|
||||||
|
"""Async wrapper for SpooledTemporaryFile class"""
|
||||||
|
|
||||||
|
async def _check(self):
|
||||||
|
if self._file._rolled:
|
||||||
|
return
|
||||||
|
max_size = self._file._max_size
|
||||||
|
if max_size and self._file.tell() > max_size:
|
||||||
|
await self.rollover()
|
||||||
|
|
||||||
|
async def write(self, s):
|
||||||
|
"""Implementation to anticipate rollover"""
|
||||||
|
if self._file._rolled:
|
||||||
|
cb = partial(self._file.write, s)
|
||||||
|
return await self._loop.run_in_executor(self._executor, cb)
|
||||||
|
else:
|
||||||
|
file = self._file._file # reference underlying base IO object
|
||||||
|
rv = file.write(s)
|
||||||
|
await self._check()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
async def writelines(self, iterable):
|
||||||
|
"""Implementation to anticipate rollover"""
|
||||||
|
if self._file._rolled:
|
||||||
|
cb = partial(self._file.writelines, iterable)
|
||||||
|
return await self._loop.run_in_executor(self._executor, cb)
|
||||||
|
else:
|
||||||
|
file = self._file._file # reference underlying base IO object
|
||||||
|
rv = file.writelines(iterable)
|
||||||
|
await self._check()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor("cleanup")
|
||||||
|
@proxy_property_directly("name")
|
||||||
|
class AsyncTemporaryDirectory:
|
||||||
|
"""Async wrapper for TemporaryDirectory class"""
|
||||||
|
|
||||||
|
def __init__(self, file, loop, executor):
|
||||||
|
self._file = file
|
||||||
|
self._loop = loop
|
||||||
|
self._executor = executor
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self.cleanup()
|
108
deps/lib/python3.10/site-packages/aiofiles/threadpool/__init__.py
vendored
Normal file
108
deps/lib/python3.10/site-packages/aiofiles/threadpool/__init__.py
vendored
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
"""Handle files using a thread pool executor."""
|
||||||
|
import asyncio
|
||||||
|
from types import coroutine
|
||||||
|
|
||||||
|
from io import (
|
||||||
|
FileIO,
|
||||||
|
TextIOBase,
|
||||||
|
BufferedReader,
|
||||||
|
BufferedWriter,
|
||||||
|
BufferedRandom,
|
||||||
|
)
|
||||||
|
from functools import partial, singledispatch
|
||||||
|
|
||||||
|
from .binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO
|
||||||
|
from .text import AsyncTextIOWrapper
|
||||||
|
from ..base import AiofilesContextManager
|
||||||
|
|
||||||
|
sync_open = open
|
||||||
|
|
||||||
|
__all__ = ("open",)
|
||||||
|
|
||||||
|
|
||||||
|
def open(
|
||||||
|
file,
|
||||||
|
mode="r",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
errors=None,
|
||||||
|
newline=None,
|
||||||
|
closefd=True,
|
||||||
|
opener=None,
|
||||||
|
*,
|
||||||
|
loop=None,
|
||||||
|
executor=None
|
||||||
|
):
|
||||||
|
return AiofilesContextManager(
|
||||||
|
_open(
|
||||||
|
file,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
|
newline=newline,
|
||||||
|
closefd=closefd,
|
||||||
|
opener=opener,
|
||||||
|
loop=loop,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@coroutine
|
||||||
|
def _open(
|
||||||
|
file,
|
||||||
|
mode="r",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
errors=None,
|
||||||
|
newline=None,
|
||||||
|
closefd=True,
|
||||||
|
opener=None,
|
||||||
|
*,
|
||||||
|
loop=None,
|
||||||
|
executor=None
|
||||||
|
):
|
||||||
|
"""Open an asyncio file."""
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
cb = partial(
|
||||||
|
sync_open,
|
||||||
|
file,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
|
newline=newline,
|
||||||
|
closefd=closefd,
|
||||||
|
opener=opener,
|
||||||
|
)
|
||||||
|
f = yield from loop.run_in_executor(executor, cb)
|
||||||
|
|
||||||
|
return wrap(f, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def wrap(file, *, loop=None, executor=None):
|
||||||
|
raise TypeError("Unsupported io type: {}.".format(file))
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(TextIOBase)
|
||||||
|
def _(file, *, loop=None, executor=None):
|
||||||
|
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(BufferedWriter)
|
||||||
|
def _(file, *, loop=None, executor=None):
|
||||||
|
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(BufferedReader)
|
||||||
|
@wrap.register(BufferedRandom)
|
||||||
|
def _(file, *, loop=None, executor=None):
|
||||||
|
return AsyncBufferedReader(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(FileIO)
|
||||||
|
def _(file, *, loop=None, executor=None):
|
||||||
|
return AsyncFileIO(file, loop, executor)
|
BIN
deps/lib/python3.10/site-packages/aiofiles/threadpool/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/threadpool/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/aiofiles/threadpool/__pycache__/binary.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/threadpool/__pycache__/binary.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/aiofiles/threadpool/__pycache__/text.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/threadpool/__pycache__/text.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/aiofiles/threadpool/__pycache__/utils.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/aiofiles/threadpool/__pycache__/utils.cpython-310.pyc
vendored
Normal file
Binary file not shown.
57
deps/lib/python3.10/site-packages/aiofiles/threadpool/binary.py
vendored
Normal file
57
deps/lib/python3.10/site-packages/aiofiles/threadpool/binary.py
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
from ..base import AsyncBase
|
||||||
|
from .utils import (
|
||||||
|
delegate_to_executor,
|
||||||
|
proxy_method_directly,
|
||||||
|
proxy_property_directly,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"read1",
|
||||||
|
"readinto",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"seekable",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
"writable",
|
||||||
|
"write",
|
||||||
|
"writelines",
|
||||||
|
)
|
||||||
|
@proxy_method_directly("detach", "fileno", "readable")
|
||||||
|
@proxy_property_directly("closed", "raw", "name", "mode")
|
||||||
|
class AsyncBufferedIOBase(AsyncBase):
|
||||||
|
"""The asyncio executor version of io.BufferedWriter."""
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor("peek")
|
||||||
|
class AsyncBufferedReader(AsyncBufferedIOBase):
|
||||||
|
"""The asyncio executor version of io.BufferedReader and Random."""
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"readall",
|
||||||
|
"readinto",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"seekable",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
"writable",
|
||||||
|
"write",
|
||||||
|
"writelines",
|
||||||
|
)
|
||||||
|
@proxy_method_directly("fileno", "readable")
|
||||||
|
@proxy_property_directly("closed", "name", "mode")
|
||||||
|
class AsyncFileIO(AsyncBase):
|
||||||
|
"""The asyncio executor version of io.FileIO."""
|
37
deps/lib/python3.10/site-packages/aiofiles/threadpool/text.py
vendored
Normal file
37
deps/lib/python3.10/site-packages/aiofiles/threadpool/text.py
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from ..base import AsyncBase
|
||||||
|
from .utils import (
|
||||||
|
delegate_to_executor,
|
||||||
|
proxy_method_directly,
|
||||||
|
proxy_property_directly,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"readable",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"seekable",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
"write",
|
||||||
|
"writable",
|
||||||
|
"writelines",
|
||||||
|
)
|
||||||
|
@proxy_method_directly("detach", "fileno", "readable")
|
||||||
|
@proxy_property_directly(
|
||||||
|
"buffer",
|
||||||
|
"closed",
|
||||||
|
"encoding",
|
||||||
|
"errors",
|
||||||
|
"line_buffering",
|
||||||
|
"newlines",
|
||||||
|
"name",
|
||||||
|
"mode",
|
||||||
|
)
|
||||||
|
class AsyncTextIOWrapper(AsyncBase):
|
||||||
|
"""The asyncio executor version of io.TextIOWrapper."""
|
74
deps/lib/python3.10/site-packages/aiofiles/threadpool/utils.py
vendored
Normal file
74
deps/lib/python3.10/site-packages/aiofiles/threadpool/utils.py
vendored
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import functools
|
||||||
|
from types import coroutine
|
||||||
|
|
||||||
|
|
||||||
|
def delegate_to_executor(*attrs):
|
||||||
|
def cls_builder(cls):
|
||||||
|
for attr_name in attrs:
|
||||||
|
setattr(cls, attr_name, _make_delegate_method(attr_name))
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return cls_builder
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_method_directly(*attrs):
|
||||||
|
def cls_builder(cls):
|
||||||
|
for attr_name in attrs:
|
||||||
|
setattr(cls, attr_name, _make_proxy_method(attr_name))
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return cls_builder
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_property_directly(*attrs):
|
||||||
|
def cls_builder(cls):
|
||||||
|
for attr_name in attrs:
|
||||||
|
setattr(cls, attr_name, _make_proxy_property(attr_name))
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return cls_builder
|
||||||
|
|
||||||
|
|
||||||
|
def cond_delegate_to_executor(*attrs):
|
||||||
|
def cls_builder(cls):
|
||||||
|
for attr_name in attrs:
|
||||||
|
setattr(cls, attr_name, _make_cond_delegate_method(attr_name))
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return cls_builder
|
||||||
|
|
||||||
|
|
||||||
|
def _make_delegate_method(attr_name):
|
||||||
|
@coroutine
|
||||||
|
def method(self, *args, **kwargs):
|
||||||
|
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
|
||||||
|
return (yield from self._loop.run_in_executor(self._executor, cb))
|
||||||
|
|
||||||
|
return method
|
||||||
|
|
||||||
|
|
||||||
|
def _make_proxy_method(attr_name):
|
||||||
|
def method(self, *args, **kwargs):
|
||||||
|
return getattr(self._file, attr_name)(*args, **kwargs)
|
||||||
|
|
||||||
|
return method
|
||||||
|
|
||||||
|
|
||||||
|
def _make_proxy_property(attr_name):
|
||||||
|
def proxy_property(self):
|
||||||
|
return getattr(self._file, attr_name)
|
||||||
|
|
||||||
|
return property(proxy_property)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_cond_delegate_method(attr_name):
|
||||||
|
"""For spooled temp files, delegate only if rolled to file object"""
|
||||||
|
|
||||||
|
async def method(self, *args, **kwargs):
|
||||||
|
if self._file._rolled:
|
||||||
|
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
|
||||||
|
return await self._loop.run_in_executor(self._executor, cb)
|
||||||
|
else:
|
||||||
|
return getattr(self._file, attr_name)(*args, **kwargs)
|
||||||
|
|
||||||
|
return method
|
7
deps/lib/python3.10/site-packages/eq3bt/__init__.py
vendored
Normal file
7
deps/lib/python3.10/site-packages/eq3bt/__init__.py
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# flake8: noqa
|
||||||
|
from .eq3btsmart import Mode, TemperatureException, Thermostat
|
||||||
|
from .structures import *
|
||||||
|
|
||||||
|
|
||||||
|
class BackendException(Exception):
|
||||||
|
"""Exception to wrap backend exceptions."""
|
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/bleakconnection.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/bleakconnection.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/connection.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/connection.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/eq3btsmart.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/eq3btsmart.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/eq3cli.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/eq3cli.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/gattlibconnection.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/gattlibconnection.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/structures.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/eq3bt/__pycache__/structures.cpython-310.pyc
vendored
Normal file
Binary file not shown.
123
deps/lib/python3.10/site-packages/eq3bt/bleakconnection.py
vendored
Normal file
123
deps/lib/python3.10/site-packages/eq3bt/bleakconnection.py
vendored
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
"""
|
||||||
|
Bleak connection backend.
|
||||||
|
This creates a new event loop that is used to integrate bleak's
|
||||||
|
asyncio functions to synchronous architecture of python-eq3bt.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import codecs
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from bleak import BleakClient, BleakError
|
||||||
|
|
||||||
|
from . import BackendException
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 1
|
||||||
|
|
||||||
|
# bleak backends are very loud on debug, this reduces the log spam when using --debug
|
||||||
|
logging.getLogger("bleak.backends").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BleakConnection:
|
||||||
|
"""Representation of a BTLE Connection."""
|
||||||
|
|
||||||
|
def __init__(self, mac, iface):
|
||||||
|
"""Initialize the connection."""
|
||||||
|
|
||||||
|
self._conn: Optional[BleakClient] = None
|
||||||
|
self._mac = mac
|
||||||
|
self._iface = iface
|
||||||
|
self._callbacks = {}
|
||||||
|
self._notifyevent = asyncio.Event()
|
||||||
|
self._notification_handle = None
|
||||||
|
self._loop = asyncio.new_event_loop()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""
|
||||||
|
Context manager __enter__ for connecting the device
|
||||||
|
:rtype: BTLEConnection
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
_LOGGER.debug("Trying to connect to %s", self._mac)
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
if self._iface is not None:
|
||||||
|
kwargs["adapter"] = self._iface
|
||||||
|
self._conn = BleakClient(self._mac, **kwargs)
|
||||||
|
try:
|
||||||
|
self._loop.run_until_complete(self._conn.connect())
|
||||||
|
except BleakError as ex:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Unable to connect to the device %s, retrying: %s", self._mac, ex
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self._loop.run_until_complete(self._conn.connect())
|
||||||
|
except Exception as ex2:
|
||||||
|
_LOGGER.debug("Second connection try to %s failed: %s", self._mac, ex2)
|
||||||
|
raise BackendException(
|
||||||
|
"unable to connect to device using bleak"
|
||||||
|
) from ex2
|
||||||
|
|
||||||
|
# The notification handles are off-by-one compared to gattlib and bluepy
|
||||||
|
self._loop.run_until_complete(
|
||||||
|
self._conn.start_notify(self._notification_handle - 1, self.on_notification)
|
||||||
|
)
|
||||||
|
_LOGGER.debug("Connected to %s", self._mac)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if self._conn:
|
||||||
|
self._loop.run_until_complete(self._conn.disconnect())
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
async def on_notification(self, handle, data):
|
||||||
|
"""Handle Callback from a Bluetooth (GATT) request."""
|
||||||
|
# The notification handles are off-by-one compared to gattlib and bluepy
|
||||||
|
handle = handle + 1
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Got notification from %s: %s", handle, codecs.encode(data, "hex")
|
||||||
|
)
|
||||||
|
self._notifyevent.set()
|
||||||
|
|
||||||
|
if handle in self._callbacks:
|
||||||
|
self._callbacks[handle](data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mac(self):
|
||||||
|
"""Return the MAC address of the connected device."""
|
||||||
|
return self._mac
|
||||||
|
|
||||||
|
def set_callback(self, handle, function):
|
||||||
|
"""Set the callback for a Notification handle. It will be called with the parameter data, which is binary."""
|
||||||
|
self._notification_handle = handle
|
||||||
|
self._callbacks[handle] = function
|
||||||
|
|
||||||
|
async def wait_for_response(self, timeout):
|
||||||
|
with contextlib.suppress(asyncio.TimeoutError):
|
||||||
|
await asyncio.wait_for(self._notifyevent.wait(), timeout)
|
||||||
|
|
||||||
|
def make_request(self, handle, value, timeout=DEFAULT_TIMEOUT, with_response=True):
|
||||||
|
"""Write a GATT Command without callback - not utf-8."""
|
||||||
|
try:
|
||||||
|
with self:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Writing %s to %s",
|
||||||
|
codecs.encode(value, "hex"),
|
||||||
|
handle,
|
||||||
|
)
|
||||||
|
self._notifyevent.clear()
|
||||||
|
|
||||||
|
self._loop.run_until_complete(
|
||||||
|
self._conn.write_gatt_char(handle - 1, value)
|
||||||
|
)
|
||||||
|
if timeout:
|
||||||
|
_LOGGER.debug("Waiting for notifications for %s", timeout)
|
||||||
|
self._loop.run_until_complete(self.wait_for_response(timeout))
|
||||||
|
|
||||||
|
except BleakError as ex:
|
||||||
|
_LOGGER.debug("Got exception from bleak while making a request: %s", ex)
|
||||||
|
raise BackendException("Exception on write using bleak") from ex
|
95
deps/lib/python3.10/site-packages/eq3bt/connection.py
vendored
Normal file
95
deps/lib/python3.10/site-packages/eq3bt/connection.py
vendored
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""
|
||||||
|
A simple wrapper for bluepy's btle.Connection.
|
||||||
|
Handles Connection duties (reconnecting etc.) transparently.
|
||||||
|
"""
|
||||||
|
import codecs
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from bluepy import btle
|
||||||
|
|
||||||
|
from . import BackendException
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 1
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BTLEConnection(btle.DefaultDelegate):
|
||||||
|
"""Representation of a BTLE Connection."""
|
||||||
|
|
||||||
|
def __init__(self, mac, iface):
|
||||||
|
"""Initialize the connection."""
|
||||||
|
btle.DefaultDelegate.__init__(self)
|
||||||
|
|
||||||
|
self._conn = None
|
||||||
|
self._mac = mac
|
||||||
|
self._iface = iface
|
||||||
|
self._callbacks = {}
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""
|
||||||
|
Context manager __enter__ for connecting the device
|
||||||
|
:rtype: btle.Peripheral
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self._conn = btle.Peripheral()
|
||||||
|
self._conn.withDelegate(self)
|
||||||
|
_LOGGER.debug("Trying to connect to %s", self._mac)
|
||||||
|
try:
|
||||||
|
self._conn.connect(self._mac, iface=self._iface)
|
||||||
|
except btle.BTLEException as ex:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Unable to connect to the device %s, retrying: %s", self._mac, ex
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self._conn.connect(self._mac, iface=self._iface)
|
||||||
|
except Exception as ex2:
|
||||||
|
_LOGGER.debug("Second connection try to %s failed: %s", self._mac, ex2)
|
||||||
|
raise BackendException(
|
||||||
|
"Unable to connect to device using bluepy"
|
||||||
|
) from ex2
|
||||||
|
|
||||||
|
_LOGGER.debug("Connected to %s", self._mac)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if self._conn:
|
||||||
|
self._conn.disconnect()
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
def handleNotification(self, handle, data):
|
||||||
|
"""Handle Callback from a Bluetooth (GATT) request."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Got notification from %s: %s", handle, codecs.encode(data, "hex")
|
||||||
|
)
|
||||||
|
if handle in self._callbacks:
|
||||||
|
self._callbacks[handle](data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mac(self):
|
||||||
|
"""Return the MAC address of the connected device."""
|
||||||
|
return self._mac
|
||||||
|
|
||||||
|
def set_callback(self, handle, function):
|
||||||
|
"""Set the callback for a Notification handle. It will be called with the parameter data, which is binary."""
|
||||||
|
self._callbacks[handle] = function
|
||||||
|
|
||||||
|
def make_request(self, handle, value, timeout=DEFAULT_TIMEOUT, with_response=True):
|
||||||
|
"""Write a GATT Command without callback - not utf-8."""
|
||||||
|
try:
|
||||||
|
with self:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Writing %s to %s with with_response=%s",
|
||||||
|
codecs.encode(value, "hex"),
|
||||||
|
handle,
|
||||||
|
with_response,
|
||||||
|
)
|
||||||
|
self._conn.writeCharacteristic(
|
||||||
|
handle, value, withResponse=with_response
|
||||||
|
)
|
||||||
|
if timeout:
|
||||||
|
_LOGGER.debug("Waiting for notifications for %s", timeout)
|
||||||
|
self._conn.waitForNotifications(timeout)
|
||||||
|
except btle.BTLEException as ex:
|
||||||
|
_LOGGER.debug("Got exception from bluepy while making a request: %s", ex)
|
||||||
|
raise BackendException("Exception on write using bluepy") from ex
|
491
deps/lib/python3.10/site-packages/eq3bt/eq3btsmart.py
vendored
Normal file
491
deps/lib/python3.10/site-packages/eq3bt/eq3btsmart.py
vendored
Normal file
@ -0,0 +1,491 @@
|
|||||||
|
"""
|
||||||
|
Support for eq3 Bluetooth Smart thermostats.
|
||||||
|
|
||||||
|
All temperatures in Celsius.
|
||||||
|
|
||||||
|
To get the current state, update() has to be called for powersaving reasons.
|
||||||
|
Schedule needs to be requested with query_schedule() before accessing for similar reasons.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
import logging
|
||||||
|
import struct
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
from construct import Byte
|
||||||
|
|
||||||
|
from .structures import AwayDataAdapter, DeviceId, Schedule, Status
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PROP_WRITE_HANDLE = 0x411
|
||||||
|
PROP_NTFY_HANDLE = 0x421
|
||||||
|
|
||||||
|
PROP_ID_QUERY = 0
|
||||||
|
PROP_ID_RETURN = 1
|
||||||
|
PROP_INFO_QUERY = 3
|
||||||
|
PROP_INFO_RETURN = 2
|
||||||
|
PROP_COMFORT_ECO_CONFIG = 0x11
|
||||||
|
PROP_OFFSET = 0x13
|
||||||
|
PROP_WINDOW_OPEN_CONFIG = 0x14
|
||||||
|
PROP_SCHEDULE_QUERY = 0x20
|
||||||
|
PROP_SCHEDULE_RETURN = 0x21
|
||||||
|
|
||||||
|
PROP_MODE_WRITE = 0x40
|
||||||
|
PROP_TEMPERATURE_WRITE = 0x41
|
||||||
|
PROP_COMFORT = 0x43
|
||||||
|
PROP_ECO = 0x44
|
||||||
|
PROP_BOOST = 0x45
|
||||||
|
PROP_LOCK = 0x80
|
||||||
|
|
||||||
|
EQ3BT_AWAY_TEMP = 12.0
|
||||||
|
EQ3BT_MIN_TEMP = 5.0
|
||||||
|
EQ3BT_MAX_TEMP = 29.5
|
||||||
|
EQ3BT_OFF_TEMP = 4.5
|
||||||
|
EQ3BT_ON_TEMP = 30.0
|
||||||
|
|
||||||
|
|
||||||
|
class Mode(IntEnum):
|
||||||
|
"""Thermostat modes."""
|
||||||
|
|
||||||
|
Unknown = -1
|
||||||
|
Closed = 0
|
||||||
|
Open = 1
|
||||||
|
Auto = 2
|
||||||
|
Manual = 3
|
||||||
|
Away = 4
|
||||||
|
Boost = 5
|
||||||
|
|
||||||
|
|
||||||
|
MODE_NOT_TEMP = [Mode.Unknown, Mode.Closed, Mode.Open]
|
||||||
|
|
||||||
|
|
||||||
|
class TemperatureException(Exception):
|
||||||
|
"""Temperature out of range error."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
class Thermostat:
|
||||||
|
"""Representation of a EQ3 Bluetooth Smart thermostat."""
|
||||||
|
|
||||||
|
def __init__(self, _mac, _iface=None, connection_cls=None):
|
||||||
|
"""Initialize the thermostat."""
|
||||||
|
|
||||||
|
self._target_temperature = Mode.Unknown
|
||||||
|
self._mode = Mode.Unknown
|
||||||
|
self._valve_state = Mode.Unknown
|
||||||
|
self._raw_mode = None
|
||||||
|
|
||||||
|
self._schedule = {}
|
||||||
|
|
||||||
|
self._window_open_temperature = None
|
||||||
|
self._window_open_time = None
|
||||||
|
self._comfort_temperature = None
|
||||||
|
self._eco_temperature = None
|
||||||
|
self._temperature_offset = None
|
||||||
|
|
||||||
|
self._away_temp = EQ3BT_AWAY_TEMP
|
||||||
|
self._away_duration = timedelta(days=30)
|
||||||
|
self._away_end = None
|
||||||
|
|
||||||
|
self._firmware_version = None
|
||||||
|
self._device_serial = None
|
||||||
|
|
||||||
|
if connection_cls is None:
|
||||||
|
from .bleakconnection import BleakConnection as connection_cls
|
||||||
|
self._conn = connection_cls(_mac, _iface)
|
||||||
|
self._conn.set_callback(PROP_NTFY_HANDLE, self.handle_notification)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
away_end = "no"
|
||||||
|
if self.away_end:
|
||||||
|
away_end = "end: %s" % self._away_end
|
||||||
|
|
||||||
|
return "[{}] Target {} (mode: {}, away: {})".format(
|
||||||
|
self._conn.mac, self.target_temperature, self.mode_readable, away_end
|
||||||
|
)
|
||||||
|
|
||||||
|
def _verify_temperature(self, temp):
|
||||||
|
"""Verifies that the temperature is valid.
|
||||||
|
:raises TemperatureException: On invalid temperature.
|
||||||
|
"""
|
||||||
|
if temp < self.min_temp or temp > self.max_temp:
|
||||||
|
raise TemperatureException(
|
||||||
|
"Temperature {} out of range [{}, {}]".format(
|
||||||
|
temp, self.min_temp, self.max_temp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_schedule(self, data):
|
||||||
|
"""Parses the device sent schedule."""
|
||||||
|
sched = Schedule.parse(data)
|
||||||
|
_LOGGER.debug("Got schedule data for day '%s'", sched.day)
|
||||||
|
|
||||||
|
return sched
|
||||||
|
|
||||||
|
def handle_notification(self, data):
|
||||||
|
"""Handle Callback from a Bluetooth (GATT) request."""
|
||||||
|
_LOGGER.debug("Received notification from the device..")
|
||||||
|
|
||||||
|
if data[0] == PROP_INFO_RETURN and data[1] == 1:
|
||||||
|
_LOGGER.debug("Got status: %s" % codecs.encode(data, "hex"))
|
||||||
|
status = Status.parse(data)
|
||||||
|
_LOGGER.debug("Parsed status: %s", status)
|
||||||
|
|
||||||
|
self._raw_mode = status.mode
|
||||||
|
self._valve_state = status.valve
|
||||||
|
self._target_temperature = status.target_temp
|
||||||
|
|
||||||
|
if status.mode.BOOST:
|
||||||
|
self._mode = Mode.Boost
|
||||||
|
elif status.mode.AWAY:
|
||||||
|
self._mode = Mode.Away
|
||||||
|
self._away_end = status.away
|
||||||
|
elif status.mode.MANUAL:
|
||||||
|
if status.target_temp == EQ3BT_OFF_TEMP:
|
||||||
|
self._mode = Mode.Closed
|
||||||
|
elif status.target_temp == EQ3BT_ON_TEMP:
|
||||||
|
self._mode = Mode.Open
|
||||||
|
else:
|
||||||
|
self._mode = Mode.Manual
|
||||||
|
else:
|
||||||
|
self._mode = Mode.Auto
|
||||||
|
|
||||||
|
presets = status.presets
|
||||||
|
if presets:
|
||||||
|
self._window_open_temperature = presets.window_open_temp
|
||||||
|
self._window_open_time = presets.window_open_time
|
||||||
|
self._comfort_temperature = presets.comfort_temp
|
||||||
|
self._eco_temperature = presets.eco_temp
|
||||||
|
self._temperature_offset = presets.offset
|
||||||
|
else:
|
||||||
|
self._window_open_temperature = None
|
||||||
|
self._window_open_time = None
|
||||||
|
self._comfort_temperature = None
|
||||||
|
self._eco_temperature = None
|
||||||
|
self._temperature_offset = None
|
||||||
|
|
||||||
|
_LOGGER.debug("Valve state: %s", self._valve_state)
|
||||||
|
_LOGGER.debug("Mode: %s", self.mode_readable)
|
||||||
|
_LOGGER.debug("Target temp: %s", self._target_temperature)
|
||||||
|
_LOGGER.debug("Away end: %s", self._away_end)
|
||||||
|
_LOGGER.debug("Window open temp: %s", self._window_open_temperature)
|
||||||
|
_LOGGER.debug("Window open time: %s", self._window_open_time)
|
||||||
|
_LOGGER.debug("Comfort temp: %s", self._comfort_temperature)
|
||||||
|
_LOGGER.debug("Eco temp: %s", self._eco_temperature)
|
||||||
|
_LOGGER.debug("Temp offset: %s", self._temperature_offset)
|
||||||
|
|
||||||
|
elif data[0] == PROP_SCHEDULE_RETURN:
|
||||||
|
parsed = self.parse_schedule(data)
|
||||||
|
self._schedule[parsed.day] = parsed
|
||||||
|
|
||||||
|
elif data[0] == PROP_ID_RETURN:
|
||||||
|
parsed = DeviceId.parse(data)
|
||||||
|
_LOGGER.debug("Parsed device data: %s", parsed)
|
||||||
|
self._firmware_version = parsed.version
|
||||||
|
self._device_serial = parsed.serial
|
||||||
|
|
||||||
|
else:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Unknown notification %s (%s)", data[0], codecs.encode(data, "hex")
|
||||||
|
)
|
||||||
|
|
||||||
|
def query_id(self):
|
||||||
|
"""Query device identification information, e.g. the serial number."""
|
||||||
|
_LOGGER.debug("Querying id..")
|
||||||
|
value = struct.pack("B", PROP_ID_QUERY)
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update the data from the thermostat. Always sets the current time."""
|
||||||
|
_LOGGER.debug("Querying the device..")
|
||||||
|
time = datetime.now()
|
||||||
|
value = struct.pack(
|
||||||
|
"BBBBBBB",
|
||||||
|
PROP_INFO_QUERY,
|
||||||
|
time.year % 100,
|
||||||
|
time.month,
|
||||||
|
time.day,
|
||||||
|
time.hour,
|
||||||
|
time.minute,
|
||||||
|
time.second,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
def query_schedule(self, day):
|
||||||
|
_LOGGER.debug("Querying schedule..")
|
||||||
|
|
||||||
|
if day < 0 or day > 6:
|
||||||
|
_LOGGER.error("Invalid day: %s", day)
|
||||||
|
|
||||||
|
value = struct.pack("BB", PROP_SCHEDULE_QUERY, day)
|
||||||
|
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schedule(self):
|
||||||
|
"""Returns previously fetched schedule.
|
||||||
|
:return: Schedule structure or None if not fetched.
|
||||||
|
"""
|
||||||
|
return self._schedule
|
||||||
|
|
||||||
|
def set_schedule(self, data):
|
||||||
|
"""Sets the schedule for the given day."""
|
||||||
|
value = Schedule.build(data)
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self):
|
||||||
|
"""Return the temperature we try to reach."""
|
||||||
|
return self._target_temperature
|
||||||
|
|
||||||
|
@target_temperature.setter
|
||||||
|
def target_temperature(self, temperature):
|
||||||
|
"""Set new target temperature."""
|
||||||
|
dev_temp = int(temperature * 2)
|
||||||
|
if temperature == EQ3BT_OFF_TEMP or temperature == EQ3BT_ON_TEMP:
|
||||||
|
dev_temp |= 0x40
|
||||||
|
value = struct.pack("BB", PROP_MODE_WRITE, dev_temp)
|
||||||
|
else:
|
||||||
|
self._verify_temperature(temperature)
|
||||||
|
value = struct.pack("BB", PROP_TEMPERATURE_WRITE, dev_temp)
|
||||||
|
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode(self):
|
||||||
|
"""Return the current operation mode"""
|
||||||
|
return self._mode
|
||||||
|
|
||||||
|
@mode.setter
|
||||||
|
def mode(self, mode):
|
||||||
|
"""Set the operation mode."""
|
||||||
|
_LOGGER.debug("Setting new mode: %s", mode)
|
||||||
|
|
||||||
|
if self.mode == Mode.Boost and mode != Mode.Boost:
|
||||||
|
self.boost = False
|
||||||
|
|
||||||
|
if mode == Mode.Boost:
|
||||||
|
self.boost = True
|
||||||
|
return
|
||||||
|
elif mode == Mode.Away:
|
||||||
|
end = datetime.now() + self._away_duration
|
||||||
|
return self.set_away(end, self._away_temp)
|
||||||
|
elif mode == Mode.Closed:
|
||||||
|
return self.set_mode(0x40 | int(EQ3BT_OFF_TEMP * 2))
|
||||||
|
elif mode == Mode.Open:
|
||||||
|
return self.set_mode(0x40 | int(EQ3BT_ON_TEMP * 2))
|
||||||
|
|
||||||
|
if mode == Mode.Manual:
|
||||||
|
temperature = max(
|
||||||
|
min(self._target_temperature, self.max_temp), self.min_temp
|
||||||
|
)
|
||||||
|
return self.set_mode(0x40 | int(temperature * 2))
|
||||||
|
else:
|
||||||
|
return self.set_mode(0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def away_end(self):
|
||||||
|
return self._away_end
|
||||||
|
|
||||||
|
def set_away(self, away_end=None, temperature=EQ3BT_AWAY_TEMP):
|
||||||
|
"""Sets away mode with target temperature.
|
||||||
|
When called without parameters disables away mode."""
|
||||||
|
if not away_end:
|
||||||
|
_LOGGER.debug("Disabling away, going to auto mode.")
|
||||||
|
return self.set_mode(0x00)
|
||||||
|
|
||||||
|
_LOGGER.debug("Setting away until %s, temp %s", away_end, temperature)
|
||||||
|
adapter = AwayDataAdapter(Byte[4])
|
||||||
|
packed = adapter.build(away_end)
|
||||||
|
|
||||||
|
self.set_mode(0x80 | int(temperature * 2), packed)
|
||||||
|
|
||||||
|
def set_mode(self, mode, payload=None):
|
||||||
|
value = struct.pack("BB", PROP_MODE_WRITE, mode)
|
||||||
|
if payload:
|
||||||
|
value += payload
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode_readable(self):
|
||||||
|
"""Return a readable representation of the mode.."""
|
||||||
|
ret = ""
|
||||||
|
mode = self._raw_mode
|
||||||
|
|
||||||
|
if mode.MANUAL:
|
||||||
|
ret = "manual"
|
||||||
|
if self.target_temperature < self.min_temp:
|
||||||
|
ret += " off"
|
||||||
|
elif self.target_temperature >= self.max_temp:
|
||||||
|
ret += " on"
|
||||||
|
else:
|
||||||
|
ret += " (%sC)" % self.target_temperature
|
||||||
|
else:
|
||||||
|
ret = "auto"
|
||||||
|
|
||||||
|
if mode.AWAY:
|
||||||
|
ret += " holiday"
|
||||||
|
if mode.BOOST:
|
||||||
|
ret += " boost"
|
||||||
|
if mode.DST:
|
||||||
|
ret += " dst"
|
||||||
|
if mode.WINDOW:
|
||||||
|
ret += " window"
|
||||||
|
if mode.LOCKED:
|
||||||
|
ret += " locked"
|
||||||
|
if mode.LOW_BATTERY:
|
||||||
|
ret += " low battery"
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@property
|
||||||
|
def boost(self):
|
||||||
|
"""Returns True if the thermostat is in boost mode."""
|
||||||
|
return self.mode == Mode.Boost
|
||||||
|
|
||||||
|
@boost.setter
|
||||||
|
def boost(self, boost):
|
||||||
|
"""Sets boost mode."""
|
||||||
|
_LOGGER.debug("Setting boost mode: %s", boost)
|
||||||
|
value = struct.pack("BB", PROP_BOOST, bool(boost))
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valve_state(self):
|
||||||
|
"""Returns the valve state. Probably reported as percent open."""
|
||||||
|
return self._valve_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def window_open(self):
|
||||||
|
"""Returns True if the thermostat reports a open window
|
||||||
|
(detected by sudden drop of temperature)"""
|
||||||
|
return self._raw_mode and self._raw_mode.WINDOW
|
||||||
|
|
||||||
|
def window_open_config(self, temperature, duration):
|
||||||
|
"""Configures the window open behavior. The duration is specified in
|
||||||
|
5 minute increments."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Window open config, temperature: %s duration: %s", temperature, duration
|
||||||
|
)
|
||||||
|
self._verify_temperature(temperature)
|
||||||
|
if duration.seconds < 0 and duration.seconds > 3600:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
value = struct.pack(
|
||||||
|
"BBB",
|
||||||
|
PROP_WINDOW_OPEN_CONFIG,
|
||||||
|
int(temperature * 2),
|
||||||
|
int(duration.seconds / 300),
|
||||||
|
)
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def window_open_temperature(self):
|
||||||
|
"""The temperature to set when an open window is detected."""
|
||||||
|
return self._window_open_temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def window_open_time(self):
|
||||||
|
"""Timeout to reset the thermostat after an open window is detected."""
|
||||||
|
return self._window_open_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def locked(self):
|
||||||
|
"""Returns True if the thermostat is locked."""
|
||||||
|
return self._raw_mode and self._raw_mode.LOCKED
|
||||||
|
|
||||||
|
@locked.setter
|
||||||
|
def locked(self, lock):
|
||||||
|
"""Locks or unlocks the thermostat."""
|
||||||
|
_LOGGER.debug("Setting the lock: %s", lock)
|
||||||
|
value = struct.pack("BB", PROP_LOCK, bool(lock))
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def low_battery(self):
|
||||||
|
"""Returns True if the thermostat reports a low battery."""
|
||||||
|
return self._raw_mode and self._raw_mode.LOW_BATTERY
|
||||||
|
|
||||||
|
def temperature_presets(self, comfort, eco):
|
||||||
|
"""Set the thermostats preset temperatures comfort (sun) and
|
||||||
|
eco (moon)."""
|
||||||
|
_LOGGER.debug("Setting temperature presets, comfort: %s eco: %s", comfort, eco)
|
||||||
|
self._verify_temperature(comfort)
|
||||||
|
self._verify_temperature(eco)
|
||||||
|
value = struct.pack(
|
||||||
|
"BBB", PROP_COMFORT_ECO_CONFIG, int(comfort * 2), int(eco * 2)
|
||||||
|
)
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comfort_temperature(self):
|
||||||
|
"""Returns the comfort temperature preset of the thermostat."""
|
||||||
|
return self._comfort_temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eco_temperature(self):
|
||||||
|
"""Returns the eco temperature preset of the thermostat."""
|
||||||
|
return self._eco_temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature_offset(self):
|
||||||
|
"""Returns the thermostat's temperature offset."""
|
||||||
|
return self._temperature_offset
|
||||||
|
|
||||||
|
@temperature_offset.setter
|
||||||
|
def temperature_offset(self, offset):
|
||||||
|
"""Sets the thermostat's temperature offset."""
|
||||||
|
_LOGGER.debug("Setting offset: %s", offset)
|
||||||
|
# [-3,5 .. 0 .. 3,5 ]
|
||||||
|
# [00 .. 07 .. 0e ]
|
||||||
|
if offset < -3.5 or offset > 3.5:
|
||||||
|
raise TemperatureException("Invalid value: %s" % offset)
|
||||||
|
|
||||||
|
current = -3.5
|
||||||
|
values = {}
|
||||||
|
for i in range(15):
|
||||||
|
values[current] = i
|
||||||
|
current += 0.5
|
||||||
|
|
||||||
|
value = struct.pack("BB", PROP_OFFSET, values[offset])
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
def activate_comfort(self):
|
||||||
|
"""Activates the comfort temperature."""
|
||||||
|
value = struct.pack("B", PROP_COMFORT)
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
def activate_eco(self):
|
||||||
|
"""Activates the comfort temperature."""
|
||||||
|
value = struct.pack("B", PROP_ECO)
|
||||||
|
self._conn.make_request(PROP_WRITE_HANDLE, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_temp(self):
|
||||||
|
"""Return the minimum temperature."""
|
||||||
|
return EQ3BT_MIN_TEMP
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_temp(self):
|
||||||
|
"""Return the maximum temperature."""
|
||||||
|
return EQ3BT_MAX_TEMP
|
||||||
|
|
||||||
|
@property
|
||||||
|
def firmware_version(self):
|
||||||
|
"""Return the firmware version."""
|
||||||
|
return self._firmware_version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_serial(self):
|
||||||
|
"""Return the device serial number."""
|
||||||
|
return self._device_serial
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mac(self):
|
||||||
|
"""Return the mac address."""
|
||||||
|
return self._conn.mac
|
212
deps/lib/python3.10/site-packages/eq3bt/eq3cli.py
vendored
Normal file
212
deps/lib/python3.10/site-packages/eq3bt/eq3cli.py
vendored
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
""" Cli tool for testing connectivity with EQ3 smart thermostats. """
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from eq3bt import Thermostat
|
||||||
|
|
||||||
|
pass_dev = click.make_pass_decorator(Thermostat)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_mac(ctx, param, mac):
|
||||||
|
if re.match("^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$", mac) is None:
|
||||||
|
raise click.BadParameter(mac + " is no valid mac address")
|
||||||
|
return mac
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True)
|
||||||
|
@click.option("--mac", envvar="EQ3_MAC", required=True, callback=validate_mac)
|
||||||
|
@click.option("--interface", default=None)
|
||||||
|
@click.option("--debug/--normal", default=False)
|
||||||
|
@click.option(
|
||||||
|
"--backend", type=click.Choice(["bleak", "bluepy", "gattlib"]), default="bleak"
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, mac, interface, debug, backend):
|
||||||
|
"""Tool to query and modify the state of EQ3 BT smart thermostat."""
|
||||||
|
if debug:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
else:
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
if backend == "bluepy":
|
||||||
|
from .connection import BTLEConnection
|
||||||
|
|
||||||
|
connection_cls = BTLEConnection
|
||||||
|
elif backend == "gattlib":
|
||||||
|
from .gattlibconnection import BTLEConnection
|
||||||
|
|
||||||
|
connection_cls = BTLEConnection
|
||||||
|
else:
|
||||||
|
from .bleakconnection import BleakConnection
|
||||||
|
|
||||||
|
connection_cls = BleakConnection
|
||||||
|
|
||||||
|
thermostat = Thermostat(mac, interface, connection_cls)
|
||||||
|
thermostat.update()
|
||||||
|
ctx.obj = thermostat
|
||||||
|
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
ctx.invoke(state)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--target", type=float, required=False)
|
||||||
|
@pass_dev
|
||||||
|
def temp(dev, target):
|
||||||
|
"""Gets or sets the target temperature."""
|
||||||
|
click.echo("Current target temp: %s" % dev.target_temperature)
|
||||||
|
if target:
|
||||||
|
click.echo("Setting target temp: %s" % target)
|
||||||
|
dev.target_temperature = target
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--target", type=int, required=False)
|
||||||
|
@pass_dev
|
||||||
|
def mode(dev, target):
|
||||||
|
"""Gets or sets the active mode."""
|
||||||
|
click.echo("Current mode: %s" % dev.mode_readable)
|
||||||
|
if target:
|
||||||
|
click.echo("Setting mode: %s" % target)
|
||||||
|
dev.mode = target
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--target", type=bool, required=False)
|
||||||
|
@pass_dev
|
||||||
|
def boost(dev, target):
|
||||||
|
"""Gets or sets the boost mode."""
|
||||||
|
click.echo("Boost: %s" % dev.boost)
|
||||||
|
if target is not None:
|
||||||
|
click.echo("Setting boost: %s" % target)
|
||||||
|
dev.boost = target
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@pass_dev
|
||||||
|
def valve_state(dev):
|
||||||
|
"""Gets the state of the valve."""
|
||||||
|
click.echo("Valve: %s" % dev.valve_state)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--target", type=bool, required=False)
|
||||||
|
@pass_dev
|
||||||
|
def locked(dev, target):
|
||||||
|
"""Gets or sets the lock."""
|
||||||
|
click.echo("Locked: %s" % dev.locked)
|
||||||
|
if target is not None:
|
||||||
|
click.echo("Setting lock: %s" % target)
|
||||||
|
dev.locked = target
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@pass_dev
|
||||||
|
def low_battery(dev):
|
||||||
|
"""Gets the low battery status."""
|
||||||
|
click.echo("Batter low: %s" % dev.low_battery)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--temp", type=float, required=False)
|
||||||
|
@click.option("--duration", type=float, required=False)
|
||||||
|
@pass_dev
|
||||||
|
def window_open(dev, temp, duration):
|
||||||
|
"""Gets and sets the window open settings."""
|
||||||
|
click.echo("Window open: %s" % dev.window_open)
|
||||||
|
if dev.window_open_temperature is not None:
|
||||||
|
click.echo("Window open temp: %s" % dev.window_open_temperature)
|
||||||
|
if dev.window_open_time is not None:
|
||||||
|
click.echo("Window open time: %s" % dev.window_open_time)
|
||||||
|
if temp and duration:
|
||||||
|
click.echo(f"Setting window open conf, temp: {temp} duration: {duration}")
|
||||||
|
dev.window_open_config(temp, duration)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--comfort", type=float, required=False)
|
||||||
|
@click.option("--eco", type=float, required=False)
|
||||||
|
@pass_dev
|
||||||
|
def presets(dev, comfort, eco):
|
||||||
|
"""Sets the preset temperatures for auto mode."""
|
||||||
|
if dev.comfort_temperature is not None:
|
||||||
|
click.echo("Current comfort temp: %s" % dev.comfort_temperature)
|
||||||
|
if dev.eco_temperature is not None:
|
||||||
|
click.echo("Current eco temp: %s" % dev.eco_temperature)
|
||||||
|
if comfort and eco:
|
||||||
|
click.echo(f"Setting presets: comfort {comfort}, eco {eco}")
|
||||||
|
dev.temperature_presets(comfort, eco)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@pass_dev
|
||||||
|
def schedule(dev):
|
||||||
|
"""Gets the schedule from the thermostat."""
|
||||||
|
# TODO: expose setting the schedule somehow?
|
||||||
|
for d in range(7):
|
||||||
|
dev.query_schedule(d)
|
||||||
|
for day in dev.schedule.values():
|
||||||
|
click.echo(f"Day {day.day}, base temp: {day.base_temp}")
|
||||||
|
current_hour = day.next_change_at
|
||||||
|
for hour in day.hours:
|
||||||
|
if current_hour == 0:
|
||||||
|
continue
|
||||||
|
click.echo(f"\t[{current_hour}-{hour.next_change_at}] {hour.target_temp}")
|
||||||
|
current_hour = hour.next_change_at
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("offset", type=float, required=False)
|
||||||
|
@pass_dev
|
||||||
|
def offset(dev, offset):
|
||||||
|
"""Sets the temperature offset [-3,5 3,5]"""
|
||||||
|
if dev.temperature_offset is not None:
|
||||||
|
click.echo("Current temp offset: %s" % dev.temperature_offset)
|
||||||
|
if offset is not None:
|
||||||
|
click.echo("Setting the offset to %s" % offset)
|
||||||
|
dev.temperature_offset = offset
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("away_end", type=click.DateTime(), default=None, required=False)
|
||||||
|
@click.argument("temperature", type=float, default=None, required=False)
|
||||||
|
@pass_dev
|
||||||
|
def away(dev, away_end, temperature):
|
||||||
|
"""Enables or disables the away mode."""
|
||||||
|
if away_end:
|
||||||
|
click.echo(f"Setting away until {away_end}, temperature: {temperature}")
|
||||||
|
else:
|
||||||
|
click.echo("Disabling away mode")
|
||||||
|
dev.set_away(away_end, temperature)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@pass_dev
|
||||||
|
def device(dev):
|
||||||
|
"""Displays basic device information."""
|
||||||
|
dev.query_id()
|
||||||
|
click.echo("Firmware version: %s" % dev.firmware_version)
|
||||||
|
click.echo("Device serial: %s" % dev.device_serial)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
def state(ctx):
|
||||||
|
"""Prints out all available information."""
|
||||||
|
dev = ctx.obj
|
||||||
|
click.echo(dev)
|
||||||
|
ctx.forward(locked)
|
||||||
|
ctx.forward(low_battery)
|
||||||
|
ctx.forward(window_open)
|
||||||
|
ctx.forward(boost)
|
||||||
|
ctx.forward(temp)
|
||||||
|
ctx.forward(presets)
|
||||||
|
ctx.forward(offset)
|
||||||
|
ctx.forward(mode)
|
||||||
|
ctx.forward(valve_state)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
99
deps/lib/python3.10/site-packages/eq3bt/gattlibconnection.py
vendored
Normal file
99
deps/lib/python3.10/site-packages/eq3bt/gattlibconnection.py
vendored
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
A simple adapter to gattlib.
|
||||||
|
Handles Connection duties (reconnecting etc.) transparently.
|
||||||
|
"""
|
||||||
|
import codecs
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import gattlib
|
||||||
|
|
||||||
|
from . import BackendException
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 1
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BTLEConnection:
|
||||||
|
"""Representation of a BTLE Connection."""
|
||||||
|
|
||||||
|
def __init__(self, mac, iface):
|
||||||
|
"""Initialize the connection."""
|
||||||
|
|
||||||
|
self._conn = None
|
||||||
|
self._mac = mac
|
||||||
|
self._iface = iface
|
||||||
|
self._callbacks = {}
|
||||||
|
self._notifyevent = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""
|
||||||
|
Context manager __enter__ for connecting the device
|
||||||
|
:rtype: BTLEConnection
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
_LOGGER.debug("Trying to connect to %s", self._mac)
|
||||||
|
if self._iface is None:
|
||||||
|
self._conn = gattlib.GATTRequester(self._mac, False)
|
||||||
|
else:
|
||||||
|
self._conn = gattlib.GATTRequester(self._mac, False, self._iface)
|
||||||
|
self._conn.on_notification = self.on_notification
|
||||||
|
try:
|
||||||
|
self._conn.connect()
|
||||||
|
except gattlib.BTBaseException as ex:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Unable to connect to the device %s, retrying: %s", self._mac, ex
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self._conn.connect()
|
||||||
|
except Exception as ex2:
|
||||||
|
_LOGGER.debug("Second connection try to %s failed: %s", self._mac, ex2)
|
||||||
|
raise BackendException(
|
||||||
|
"unable to connect to device using gattlib"
|
||||||
|
) from ex2
|
||||||
|
|
||||||
|
_LOGGER.debug("Connected to %s", self._mac)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if self._conn:
|
||||||
|
self._conn.disconnect()
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
def on_notification(self, handle, data):
|
||||||
|
"""Handle Callback from a Bluetooth (GATT) request."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Got notification from %s: %s", handle, codecs.encode(data, "hex")
|
||||||
|
)
|
||||||
|
if handle in self._callbacks:
|
||||||
|
self._callbacks[handle](data[3:])
|
||||||
|
if self._notifyevent:
|
||||||
|
self._notifyevent.set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mac(self):
|
||||||
|
"""Return the MAC address of the connected device."""
|
||||||
|
return self._mac
|
||||||
|
|
||||||
|
def set_callback(self, handle, function):
|
||||||
|
"""Set the callback for a Notification handle. It will be called with the parameter data, which is binary."""
|
||||||
|
self._callbacks[handle] = function
|
||||||
|
|
||||||
|
def make_request(self, handle, value, timeout=DEFAULT_TIMEOUT, with_response=True):
|
||||||
|
"""Write a GATT Command without callback - not utf-8."""
|
||||||
|
try:
|
||||||
|
with self:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Writing %s to %s",
|
||||||
|
codecs.encode(value, "hex"),
|
||||||
|
handle,
|
||||||
|
)
|
||||||
|
self._notifyevent = threading.Event()
|
||||||
|
self._conn.write_by_handle(handle, value)
|
||||||
|
if timeout:
|
||||||
|
_LOGGER.debug("Waiting for notifications for %s", timeout)
|
||||||
|
self._notifyevent.wait(timeout)
|
||||||
|
except gattlib.BTBaseException as ex:
|
||||||
|
_LOGGER.debug("Got exception from gattlib while making a request: %s", ex)
|
||||||
|
raise BackendException("Exception on write using gattlib") from ex
|
174
deps/lib/python3.10/site-packages/eq3bt/structures.py
vendored
Normal file
174
deps/lib/python3.10/site-packages/eq3bt/structures.py
vendored
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
""" Contains construct adapters and structures. """
|
||||||
|
from datetime import datetime, time, timedelta
|
||||||
|
|
||||||
|
from construct import (
|
||||||
|
Adapter,
|
||||||
|
Bytes,
|
||||||
|
Const,
|
||||||
|
Enum,
|
||||||
|
FlagsEnum,
|
||||||
|
GreedyRange,
|
||||||
|
IfThenElse,
|
||||||
|
Int8ub,
|
||||||
|
Optional,
|
||||||
|
Struct,
|
||||||
|
)
|
||||||
|
|
||||||
|
PROP_ID_RETURN = 1
|
||||||
|
PROP_INFO_RETURN = 2
|
||||||
|
PROP_SCHEDULE_SET = 0x10
|
||||||
|
PROP_SCHEDULE_RETURN = 0x21
|
||||||
|
|
||||||
|
NAME_TO_DAY = {"sat": 0, "sun": 1, "mon": 2, "tue": 3, "wed": 4, "thu": 5, "fri": 6}
|
||||||
|
NAME_TO_CMD = {"write": PROP_SCHEDULE_SET, "response": PROP_SCHEDULE_RETURN}
|
||||||
|
HOUR_24_PLACEHOLDER = 1234
|
||||||
|
|
||||||
|
|
||||||
|
class TimeAdapter(Adapter):
|
||||||
|
"""Adapter to encode and decode schedule times."""
|
||||||
|
|
||||||
|
def _decode(self, obj, ctx, path):
|
||||||
|
h, m = divmod(obj * 10, 60)
|
||||||
|
if h == 24: # HACK, can we do better?
|
||||||
|
return HOUR_24_PLACEHOLDER
|
||||||
|
return time(hour=h, minute=m)
|
||||||
|
|
||||||
|
def _encode(self, obj, ctx, path):
|
||||||
|
# TODO: encode h == 24 hack
|
||||||
|
if obj == HOUR_24_PLACEHOLDER:
|
||||||
|
return int(24 * 60 / 10)
|
||||||
|
encoded = int((obj.hour * 60 + obj.minute) / 10)
|
||||||
|
return encoded
|
||||||
|
|
||||||
|
|
||||||
|
class TempAdapter(Adapter):
|
||||||
|
"""Adapter to encode and decode temperature."""
|
||||||
|
|
||||||
|
def _decode(self, obj, ctx, path):
|
||||||
|
return float(obj / 2.0)
|
||||||
|
|
||||||
|
def _encode(self, obj, ctx, path):
|
||||||
|
return int(obj * 2.0)
|
||||||
|
|
||||||
|
|
||||||
|
class WindowOpenTimeAdapter(Adapter):
|
||||||
|
"""Adapter to encode and decode window open times (5 min increments)."""
|
||||||
|
|
||||||
|
def _decode(self, obj, context, path):
|
||||||
|
return timedelta(minutes=float(obj * 5.0))
|
||||||
|
|
||||||
|
def _encode(self, obj, context, path):
|
||||||
|
if isinstance(obj, timedelta):
|
||||||
|
obj = obj.seconds
|
||||||
|
if 0 <= obj <= 3600.0:
|
||||||
|
return int(obj / 300.0)
|
||||||
|
raise ValueError(
|
||||||
|
"Window open time must be between 0 and 60 minutes "
|
||||||
|
"in intervals of 5 minutes."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TempOffsetAdapter(Adapter):
|
||||||
|
"""Adapter to encode and decode the temperature offset."""
|
||||||
|
|
||||||
|
def _decode(self, obj, context, path):
|
||||||
|
return float((obj - 7) / 2.0)
|
||||||
|
|
||||||
|
def _encode(self, obj, context, path):
|
||||||
|
if -3.5 <= obj <= 3.5:
|
||||||
|
return int(obj * 2.0) + 7
|
||||||
|
raise ValueError(
|
||||||
|
"Temperature offset must be between -3.5 and 3.5 (in " "intervals of 0.5)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ModeFlags = "ModeFlags" / FlagsEnum(
|
||||||
|
Int8ub,
|
||||||
|
AUTO=0x00, # always True, doesnt affect building
|
||||||
|
MANUAL=0x01,
|
||||||
|
AWAY=0x02,
|
||||||
|
BOOST=0x04,
|
||||||
|
DST=0x08,
|
||||||
|
WINDOW=0x10,
|
||||||
|
LOCKED=0x20,
|
||||||
|
UNKNOWN=0x40,
|
||||||
|
LOW_BATTERY=0x80,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AwayDataAdapter(Adapter):
|
||||||
|
"""Adapter to encode and decode away data."""
|
||||||
|
|
||||||
|
def _decode(self, obj, ctx, path):
|
||||||
|
(day, year, hour_min, month) = obj
|
||||||
|
year += 2000
|
||||||
|
|
||||||
|
min = 0
|
||||||
|
if hour_min & 0x01:
|
||||||
|
min = 30
|
||||||
|
hour = int(hour_min / 2)
|
||||||
|
|
||||||
|
return datetime(year=year, month=month, day=day, hour=hour, minute=min)
|
||||||
|
|
||||||
|
def _encode(self, obj, ctx, path):
|
||||||
|
if obj.year < 2000 or obj.year > 2099:
|
||||||
|
raise Exception("Invalid year, possible [2000,2099]")
|
||||||
|
year = obj.year - 2000
|
||||||
|
hour = obj.hour * 2
|
||||||
|
if obj.minute: # we encode all minute values to h:30
|
||||||
|
hour |= 0x01
|
||||||
|
return (obj.day, year, hour, obj.month)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSerialAdapter(Adapter):
|
||||||
|
"""Adapter to decode the device serial number."""
|
||||||
|
|
||||||
|
def _decode(self, obj, context, path):
|
||||||
|
return bytearray(n - 0x30 for n in obj).decode()
|
||||||
|
|
||||||
|
|
||||||
|
Status = "Status" / Struct(
|
||||||
|
"cmd" / Const(PROP_INFO_RETURN, Int8ub),
|
||||||
|
Const(0x01, Int8ub),
|
||||||
|
"mode" / ModeFlags,
|
||||||
|
"valve" / Int8ub,
|
||||||
|
Const(0x04, Int8ub),
|
||||||
|
"target_temp" / TempAdapter(Int8ub),
|
||||||
|
"away"
|
||||||
|
/ IfThenElse( # noqa: W503
|
||||||
|
lambda ctx: ctx.mode.AWAY, AwayDataAdapter(Bytes(4)), Optional(Bytes(4))
|
||||||
|
),
|
||||||
|
"presets"
|
||||||
|
/ Optional( # noqa: W503
|
||||||
|
Struct(
|
||||||
|
"window_open_temp" / TempAdapter(Int8ub),
|
||||||
|
"window_open_time" / WindowOpenTimeAdapter(Int8ub),
|
||||||
|
"comfort_temp" / TempAdapter(Int8ub),
|
||||||
|
"eco_temp" / TempAdapter(Int8ub),
|
||||||
|
"offset" / TempOffsetAdapter(Int8ub),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Schedule = "Schedule" / Struct(
|
||||||
|
"cmd" / Enum(Int8ub, **NAME_TO_CMD),
|
||||||
|
"day" / Enum(Int8ub, **NAME_TO_DAY),
|
||||||
|
"base_temp" / TempAdapter(Int8ub),
|
||||||
|
"next_change_at" / TimeAdapter(Int8ub),
|
||||||
|
"hours"
|
||||||
|
/ GreedyRange( # noqa: W503
|
||||||
|
Struct(
|
||||||
|
"target_temp" / TempAdapter(Int8ub),
|
||||||
|
"next_change_at" / TimeAdapter(Int8ub),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeviceId = "DeviceId" / Struct(
|
||||||
|
"cmd" / Const(PROP_ID_RETURN, Int8ub),
|
||||||
|
"version" / Int8ub,
|
||||||
|
Int8ub,
|
||||||
|
Int8ub,
|
||||||
|
"serial" / DeviceSerialAdapter(Bytes(10)),
|
||||||
|
Int8ub,
|
||||||
|
)
|
BIN
deps/lib/python3.10/site-packages/eq3bt/tests/__pycache__/test_thermostat.cpython-310.pyc
vendored
Normal file
BIN
deps/lib/python3.10/site-packages/eq3bt/tests/__pycache__/test_thermostat.cpython-310.pyc
vendored
Normal file
Binary file not shown.
198
deps/lib/python3.10/site-packages/eq3bt/tests/test_thermostat.py
vendored
Normal file
198
deps/lib/python3.10/site-packages/eq3bt/tests/test_thermostat.py
vendored
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import codecs
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from eq3bt import TemperatureException, Thermostat
|
||||||
|
from eq3bt.eq3btsmart import PROP_ID_QUERY, PROP_INFO_QUERY, PROP_NTFY_HANDLE, Mode
|
||||||
|
|
||||||
|
ID_RESPONSE = b"01780000807581626163606067659e"
|
||||||
|
STATUS_RESPONSES = {
|
||||||
|
"auto": b"020100000428",
|
||||||
|
"manual": b"020101000428",
|
||||||
|
"window": b"020110000428",
|
||||||
|
"away": b"0201020004231d132e03",
|
||||||
|
"boost": b"020104000428",
|
||||||
|
"low_batt": b"020180000428",
|
||||||
|
"valve_at_22": b"020100160428",
|
||||||
|
"presets": b"020100000422000000001803282207",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConnection:
|
||||||
|
def __init__(self, _iface, mac):
|
||||||
|
self._callbacks = {}
|
||||||
|
self._res = "auto"
|
||||||
|
|
||||||
|
def set_callback(self, handle, cb):
|
||||||
|
self._callbacks[handle] = cb
|
||||||
|
|
||||||
|
def set_status(self, key):
|
||||||
|
if key in STATUS_RESPONSES:
|
||||||
|
self._res = key
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid key for status test response.")
|
||||||
|
|
||||||
|
def make_request(self, handle, value, timeout=1, with_response=True):
|
||||||
|
"""Write a GATT Command without callback - not utf-8."""
|
||||||
|
if with_response:
|
||||||
|
cb = self._callbacks.get(PROP_NTFY_HANDLE)
|
||||||
|
|
||||||
|
if value[0] == PROP_ID_QUERY:
|
||||||
|
data = ID_RESPONSE
|
||||||
|
elif value[0] == PROP_INFO_QUERY:
|
||||||
|
data = STATUS_RESPONSES[self._res]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
cb(codecs.decode(data, "hex"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestThermostat(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.thermostat = Thermostat(
|
||||||
|
_mac=None, _iface=None, connection_cls=FakeConnection
|
||||||
|
)
|
||||||
|
|
||||||
|
def test__verify_temperature(self):
|
||||||
|
with self.assertRaises(TemperatureException):
|
||||||
|
self.thermostat._verify_temperature(-1)
|
||||||
|
with self.assertRaises(TemperatureException):
|
||||||
|
self.thermostat._verify_temperature(35)
|
||||||
|
|
||||||
|
self.thermostat._verify_temperature(8)
|
||||||
|
self.thermostat._verify_temperature(25)
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_parse_schedule(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_handle_notification(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
def test_query_id(self):
|
||||||
|
self.thermostat.query_id()
|
||||||
|
self.assertEqual(self.thermostat.firmware_version, 120)
|
||||||
|
self.assertEqual(self.thermostat.device_serial, "PEQ2130075")
|
||||||
|
|
||||||
|
def test_update(self):
|
||||||
|
th = self.thermostat
|
||||||
|
|
||||||
|
th._conn.set_status("auto")
|
||||||
|
th.update()
|
||||||
|
self.assertEqual(th.valve_state, 0)
|
||||||
|
self.assertEqual(th.mode, Mode.Auto)
|
||||||
|
self.assertEqual(th.target_temperature, 20.0)
|
||||||
|
self.assertFalse(th.locked)
|
||||||
|
self.assertFalse(th.low_battery)
|
||||||
|
self.assertFalse(th.boost)
|
||||||
|
self.assertFalse(th.window_open)
|
||||||
|
|
||||||
|
th._conn.set_status("manual")
|
||||||
|
th.update()
|
||||||
|
self.assertTrue(th.mode, Mode.Manual)
|
||||||
|
|
||||||
|
th._conn.set_status("away")
|
||||||
|
th.update()
|
||||||
|
self.assertEqual(th.mode, Mode.Away)
|
||||||
|
self.assertEqual(th.target_temperature, 17.5)
|
||||||
|
self.assertEqual(th.away_end, datetime(2019, 3, 29, 23, 00))
|
||||||
|
|
||||||
|
th._conn.set_status("boost")
|
||||||
|
th.update()
|
||||||
|
self.assertTrue(th.boost)
|
||||||
|
self.assertEqual(th.mode, Mode.Boost)
|
||||||
|
|
||||||
|
def test_presets(self):
|
||||||
|
th = self.thermostat
|
||||||
|
self.thermostat._conn.set_status("presets")
|
||||||
|
self.thermostat.update()
|
||||||
|
self.assertEqual(th.window_open_temperature, 12.0)
|
||||||
|
self.assertEqual(th.window_open_time, timedelta(minutes=15.0))
|
||||||
|
self.assertEqual(th.comfort_temperature, 20.0)
|
||||||
|
self.assertEqual(th.eco_temperature, 17.0)
|
||||||
|
self.assertEqual(th.temperature_offset, 0)
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_query_schedule(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_schedule(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_set_schedule(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_target_temperature(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_mode(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_mode_readable(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_boost(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
def test_valve_state(self):
|
||||||
|
th = self.thermostat
|
||||||
|
th._conn.set_status("valve_at_22")
|
||||||
|
th.update()
|
||||||
|
self.assertEqual(th.valve_state, 22)
|
||||||
|
|
||||||
|
def test_window_open(self):
|
||||||
|
th = self.thermostat
|
||||||
|
th._conn.set_status("window")
|
||||||
|
th.update()
|
||||||
|
self.assertTrue(th.window_open)
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_window_open_config(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_locked(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_low_battery(self):
|
||||||
|
th = self.thermostat
|
||||||
|
th._conn.set_status("low_batt")
|
||||||
|
th.update()
|
||||||
|
self.assertTrue(th.low_battery)
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_temperature_offset(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_activate_comfort(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_activate_eco(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_min_temp(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_max_temp(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_away_end(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_decode_mode(self):
|
||||||
|
self.fail()
|
1
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/INSTALLER
vendored
Normal file
1
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/INSTALLER
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
pip
|
21
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/LICENSE
vendored
Normal file
21
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/LICENSE
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 Markus Peter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
230
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/METADATA
vendored
Normal file
230
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/METADATA
vendored
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: python-eq3bt
|
||||||
|
Version: 0.2
|
||||||
|
Summary: EQ3 bluetooth thermostat support library
|
||||||
|
Home-page: https://github.com/rytilahti/python-eq3bt
|
||||||
|
License: MIT
|
||||||
|
Author: Teemu R.
|
||||||
|
Author-email: tpr@iki.fi
|
||||||
|
Requires-Python: >=3.7,<4.0
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Provides-Extra: bluepy
|
||||||
|
Provides-Extra: gattlib
|
||||||
|
Requires-Dist: bleak
|
||||||
|
Requires-Dist: bluepy (>=1.0.5); extra == "bluepy"
|
||||||
|
Requires-Dist: click
|
||||||
|
Requires-Dist: construct
|
||||||
|
Requires-Dist: gattlib; extra == "gattlib"
|
||||||
|
Project-URL: Repository, https://github.com/rytilahti/python-eq3bt
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
|
||||||
|
# python-eq3bt
|
||||||
|
|
||||||
|
Python library and a command line tool for EQ3 Bluetooth smart thermostats, uses bleak (default), bluepy or gattlib for BTLE communication.
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
* Reading device status: locked, low battery, valve state, window open, target temperature, active mode
|
||||||
|
* Writing settings: target temperature, auto mode presets, temperature offset
|
||||||
|
* Setting the active mode: auto, manual, boost, away
|
||||||
|
* Reading the device serial number and firmware version
|
||||||
|
* Reading presets and temperature offset in more recent firmware versions.
|
||||||
|
|
||||||
|
## Not (yet) supported
|
||||||
|
|
||||||
|
* No easy-to-use interface for setting schedules.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install python-eq3bt
|
||||||
|
```
|
||||||
|
|
||||||
|
# Command-line Usage
|
||||||
|
|
||||||
|
To test all available functionality a cli tool inside utils can be used:
|
||||||
|
```
|
||||||
|
$ eq3cli --help
|
||||||
|
Usage: eq3cli [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
Tool to query and modify the state of EQ3 BT smart thermostat.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--mac TEXT [required]
|
||||||
|
--interface TEXT
|
||||||
|
--debug / --normal
|
||||||
|
--backend [bleak|bluepy|gattlib]
|
||||||
|
--help Show this message and exit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
away Enables or disables the away mode.
|
||||||
|
boost Gets or sets the boost mode.
|
||||||
|
device Displays basic device information.
|
||||||
|
locked Gets or sets the lock.
|
||||||
|
low-battery Gets the low battery status.
|
||||||
|
mode Gets or sets the active mode.
|
||||||
|
offset Sets the temperature offset [-3,5 3,5]
|
||||||
|
presets Sets the preset temperatures for auto mode.
|
||||||
|
schedule Gets the schedule from the thermostat.
|
||||||
|
state Prints out all available information.
|
||||||
|
temp Gets or sets the target temperature.
|
||||||
|
valve-state Gets the state of the valve.
|
||||||
|
window-open Gets and sets the window open settings.
|
||||||
|
```
|
||||||
|
|
||||||
|
EQ3_MAC environment variable can be used to define mac to avoid typing it:
|
||||||
|
```bash
|
||||||
|
export EQ3_MAC=XX:XX
|
||||||
|
```
|
||||||
|
|
||||||
|
Without parameters current state of the device is printed out.
|
||||||
|
```bash
|
||||||
|
$ eq3cli
|
||||||
|
|
||||||
|
[00:1A:22:XX:XX:XX] Target 17.0 (mode: auto dst, away: no)
|
||||||
|
Locked: False
|
||||||
|
Batter low: False
|
||||||
|
Window open: False
|
||||||
|
Window open temp: 12.0
|
||||||
|
Window open time: 0:15:00
|
||||||
|
Boost: False
|
||||||
|
Current target temp: 17.0
|
||||||
|
Current comfort temp: 20.0
|
||||||
|
Current eco temp: 17.0
|
||||||
|
Current mode: auto dst locked
|
||||||
|
Valve: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Getting & setting values.
|
||||||
|
```bash
|
||||||
|
$ eq3cli temp
|
||||||
|
|
||||||
|
Current target temp: 17.0
|
||||||
|
|
||||||
|
eq3cli temp --target 20
|
||||||
|
|
||||||
|
Current target temp: 17.0
|
||||||
|
Setting target temp: 20.0
|
||||||
|
```
|
||||||
|
|
||||||
|
# Pairing
|
||||||
|
|
||||||
|
If you have thermostat with firmware version 1.20+ pairing may be needed. Below simple procedure to do that.
|
||||||
|
|
||||||
|
```
|
||||||
|
Press and hold wheel on thermostat until Pair will be displayed. Remember or write it.
|
||||||
|
|
||||||
|
$ sudo bluetoothctl
|
||||||
|
[bluetooth]# power on
|
||||||
|
[bluetooth]# agent on
|
||||||
|
[bluetooth]# default-agent
|
||||||
|
[bluetooth]# scan on
|
||||||
|
[bluetooth]# scan off
|
||||||
|
[bluetooth]# pair 00:1A:22:06:A7:83
|
||||||
|
[agent] Enter passkey (number in 0-999999): <enter pin>
|
||||||
|
[bluetooth]# trust 00:1A:22:06:A7:83
|
||||||
|
[bluetooth]# disconnect 00:1A:22:06:A7:83
|
||||||
|
[bluetooth]# exit
|
||||||
|
|
||||||
|
Optional steps:
|
||||||
|
[bluetooth]# devices - to list all bluetooth devices
|
||||||
|
[bluetooth]# info 00:1A:22:06:A7:83
|
||||||
|
Device 00:1A:22:06:A7:83 (public)
|
||||||
|
Name: CC-RT-BLE
|
||||||
|
Alias: CC-RT-BLE
|
||||||
|
Paired: yes
|
||||||
|
Trusted: yes
|
||||||
|
Blocked: no
|
||||||
|
Connected: no
|
||||||
|
LegacyPairing: no
|
||||||
|
UUID: Generic Access Profile (00001800-0000-1000-8000-00805f9b34fb)
|
||||||
|
UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb)
|
||||||
|
UUID: Device Information (0000180a-0000-1000-8000-00805f9b34fb)
|
||||||
|
UUID: Vendor specific (3e135142-654f-9090-134a-a6ff5bb77046)
|
||||||
|
UUID: Vendor specific (9e5d1e47-5c13-43a0-8635-82ad38a1386f)
|
||||||
|
ManufacturerData Key: 0x0000
|
||||||
|
ManufacturerData Value:
|
||||||
|
00 00 00 00 00 00 00 00 00 .........
|
||||||
|
```
|
||||||
|
|
||||||
|
Be aware that sometimes if you pair your device then mobile application (calor BT) can't connect with thermostat and vice versa.
|
||||||
|
|
||||||
|
|
||||||
|
# Library Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
from eq3bt import Thermostat
|
||||||
|
|
||||||
|
thermostat = Thermostat('AB:CD:EF:12:23:45')
|
||||||
|
thermostat.update() # fetches data from the thermostat
|
||||||
|
|
||||||
|
print(thermostat)
|
||||||
|
```
|
||||||
|
|
||||||
|
<aside class="notice">
|
||||||
|
Notice: The device in question has to be disconnected from bluetoothd, since BTLE devices can only hold one connection at a time.
|
||||||
|
|
||||||
|
The library will try to connect to the device second time in case it wasn't successful in the first time,
|
||||||
|
which can happen if you are running other applications connecting to the same thermostat.
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
## Fetching schedule
|
||||||
|
|
||||||
|
The schedule is queried per day basis and the cached information can be
|
||||||
|
accessed through `schedule` property..
|
||||||
|
|
||||||
|
```
|
||||||
|
from eq3bt import Thermostat
|
||||||
|
|
||||||
|
thermostat = Thermostat('AB:CD:EF:12:34:45')
|
||||||
|
thermostat.query_schedule(0)
|
||||||
|
print(thermostat.schedule)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting schedule
|
||||||
|
|
||||||
|
The 'base_temp' and 'next_change_at' paramater define the first period for that 'day' (the period from midnight up till next_change_at).
|
||||||
|
|
||||||
|
The schedule can be set on a per day basis like follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
from datetime import time
|
||||||
|
from eq3bt import Thermostat
|
||||||
|
from eq3bt import HOUR_24_PLACEHOLDER as END_OF_DAY
|
||||||
|
|
||||||
|
thermostat = Thermostat('12:34:56:78:9A:BC')
|
||||||
|
thermostat.set_schedule(
|
||||||
|
dict(
|
||||||
|
cmd="write",
|
||||||
|
day="sun",
|
||||||
|
base_temp=18,
|
||||||
|
next_change_at=time(8, 0),
|
||||||
|
hours=[
|
||||||
|
dict(target_temp=23, next_change_at=time(20, 0)),
|
||||||
|
dict(target_temp=18, next_change_at=END_OF_DAY),
|
||||||
|
dict(target_temp=23, next_change_at=END_OF_DAY),
|
||||||
|
dict(target_temp=23, next_change_at=END_OF_DAY),
|
||||||
|
dict(target_temp=23, next_change_at=END_OF_DAY),
|
||||||
|
dict(target_temp=23, next_change_at=END_OF_DAY)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Feel free to open pull requests to improve the library!
|
||||||
|
|
||||||
|
This project uses github actions to enforce code formatting using tools like black, isort, flake8, and mypy.
|
||||||
|
You can run these checks locally either by executing `pre-commit run -a` or using `tox` which also runs the test suite.
|
||||||
|
|
||||||
|
|
||||||
|
# History
|
||||||
|
|
||||||
|
This library is a simplified version of bluepy_devices from Markus Peter (https://github.com/bimbar/bluepy_devices.git) with support for more features and robuster device handling.
|
||||||
|
|
25
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/RECORD
vendored
Normal file
25
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/RECORD
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
../../../bin/eq3cli,sha256=TFR4GGzCskn7h5eKh4qUtn2HO-VX7rl4O4VKAFMH92Y,215
|
||||||
|
CHANGELOG,sha256=FVxHhLou6OnfINd-OStO2tzYAjsalbowKDL61Fr9Z-Y,6002
|
||||||
|
eq3bt/__init__.py,sha256=fUKZ9G7dFcM5wEOIJdOUAUcyJBeDODsdeVCk2J-w3q0,189
|
||||||
|
eq3bt/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
eq3bt/__pycache__/bleakconnection.cpython-310.pyc,,
|
||||||
|
eq3bt/__pycache__/connection.cpython-310.pyc,,
|
||||||
|
eq3bt/__pycache__/eq3btsmart.cpython-310.pyc,,
|
||||||
|
eq3bt/__pycache__/eq3cli.cpython-310.pyc,,
|
||||||
|
eq3bt/__pycache__/gattlibconnection.cpython-310.pyc,,
|
||||||
|
eq3bt/__pycache__/structures.cpython-310.pyc,,
|
||||||
|
eq3bt/bleakconnection.py,sha256=E5QWp0fYUDR150zBB8xtY68cRThADXpEUei5F6hTCQo,4269
|
||||||
|
eq3bt/connection.py,sha256=j2E2Nugo3BeteHFY3uozfb_8YZxWB5Y1itNa_SGQtlk,3217
|
||||||
|
eq3bt/eq3btsmart.py,sha256=st-NMiIeuXOR6EYBESkKWyRnMfrFpzZq6b4P-sOy7Sk,15716
|
||||||
|
eq3bt/eq3cli.py,sha256=_YFvGT0XsHE3WdGW0jp1NaH_yrRnYDpvmwBJHPOm7io,6219
|
||||||
|
eq3bt/gattlibconnection.py,sha256=25s9pY0azzwEUfJsTRXcFs0SPeoOKwb2Tx4QUsL5w_g,3285
|
||||||
|
eq3bt/structures.py,sha256=rSFQ6eTYR-18A9WEcNPcBb2P8S-G-u9gx5jVhDIjgl0,4635
|
||||||
|
eq3bt/tests/__pycache__/test_thermostat.cpython-310.pyc,,
|
||||||
|
eq3bt/tests/test_thermostat.py,sha256=lDLCNvCNgVOJoa6JUEXvAtvfhkLZR2UJZh7_XvXGQbI,5381
|
||||||
|
python_eq3bt-0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
python_eq3bt-0.2.dist-info/LICENSE,sha256=og5_OL0-NkYbkZwhvFpb4hR3EfM-0IXaore85BAkhLw,1079
|
||||||
|
python_eq3bt-0.2.dist-info/METADATA,sha256=gjGWD8CeJ_uZqr_JTfQd4wCfRErXhUxoLuJs5l8F3KA,6842
|
||||||
|
python_eq3bt-0.2.dist-info/RECORD,,
|
||||||
|
python_eq3bt-0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
python_eq3bt-0.2.dist-info/WHEEL,sha256=DA86_h4QwwzGeRoz62o1svYt5kGEXpoUTuTtwzoTb30,83
|
||||||
|
python_eq3bt-0.2.dist-info/entry_points.txt,sha256=XClUgvSU6CSK_T1HCDmeMObxQ6wKPpxMjWlZe5TiELk,43
|
0
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/REQUESTED
vendored
Normal file
0
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/REQUESTED
vendored
Normal file
4
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/WHEEL
vendored
Normal file
4
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/WHEEL
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: poetry 1.0.8
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
3
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/entry_points.txt
vendored
Normal file
3
deps/lib/python3.10/site-packages/python_eq3bt-0.2.dist-info/entry_points.txt
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[console_scripts]
|
||||||
|
eq3cli=eq3bt.eq3cli:cli
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
notify:
|
#notify:
|
||||||
- platform: html5
|
# - platform: html5
|
||||||
name: browser
|
# name: browser
|
||||||
vapid_pub_key: !secret google_api_pub
|
# vapid_pub_key: !secret google_api_pub
|
||||||
vapid_prv_key: !secret google_api_priv
|
# vapid_prv_key: !secret google_api_priv
|
||||||
vapid_email: !secret google_api_email
|
# vapid_email: !secret google_api_email
|
||||||
|
4
themes/minimalist-desktop/minimalist-desktop.yaml
Executable file → Normal file
4
themes/minimalist-desktop/minimalist-desktop.yaml
Executable file → Normal file
@ -10,6 +10,10 @@ minimalist-desktop:
|
|||||||
info-color: "var(--google-blue)"
|
info-color: "var(--google-blue)"
|
||||||
divider-color: "rgba(var(--color-theme),.12)"
|
divider-color: "rgba(var(--color-theme),.12)"
|
||||||
accent-color: "var(--google-yellow)"
|
accent-color: "var(--google-yellow)"
|
||||||
|
ha-dialog-border-radius: "10px"
|
||||||
|
# fix added border-lines in 2022.11
|
||||||
|
ha-card-border-width: "0px"
|
||||||
|
|
||||||
card-mod-theme: "minimalist-desktop"
|
card-mod-theme: "minimalist-desktop"
|
||||||
card-mod-view-yaml: |
|
card-mod-view-yaml: |
|
||||||
"*:first-child$": |
|
"*:first-child$": |
|
||||||
|
4
themes/minimalist-ios-tapbar/minimalist-ios-tapbar.yaml
Executable file → Normal file
4
themes/minimalist-ios-tapbar/minimalist-ios-tapbar.yaml
Executable file → Normal file
@ -12,6 +12,10 @@ minimalist-ios-tapbar:
|
|||||||
info-color: "var(--google-blue)"
|
info-color: "var(--google-blue)"
|
||||||
divider-color: "rgba(var(--color-theme),.12)"
|
divider-color: "rgba(var(--color-theme),.12)"
|
||||||
accent-color: "var(--google-yellow)"
|
accent-color: "var(--google-yellow)"
|
||||||
|
ha-dialog-border-radius: "10px"
|
||||||
|
# fix added border-lines in 2022.11
|
||||||
|
ha-card-border-width: "0px"
|
||||||
|
|
||||||
card-mod-theme: "minimalist-ios-tapbar"
|
card-mod-theme: "minimalist-ios-tapbar"
|
||||||
card-mod-root: |
|
card-mod-root: |
|
||||||
app-toolbar {
|
app-toolbar {
|
||||||
|
3
themes/minimalist-mobile-tapbar/minimalist-mobile-tapbar.yaml
Executable file → Normal file
3
themes/minimalist-mobile-tapbar/minimalist-mobile-tapbar.yaml
Executable file → Normal file
@ -14,6 +14,9 @@ minimalist-mobile-tapbar:
|
|||||||
header-height: "calc(var(--header-base-height) + env(safe-area-inset-bottom))"
|
header-height: "calc(var(--header-base-height) + env(safe-area-inset-bottom))"
|
||||||
header-base-height: "70px"
|
header-base-height: "70px"
|
||||||
app-header-selection-bar-color: "transparent"
|
app-header-selection-bar-color: "transparent"
|
||||||
|
ha-dialog-border-radius: "10px"
|
||||||
|
# fix added border-lines in 2022.11
|
||||||
|
ha-card-border-width: "0px"
|
||||||
|
|
||||||
card-mod-view-yaml: |
|
card-mod-view-yaml: |
|
||||||
"*:first-child$": |
|
"*:first-child$": |
|
||||||
|
4
themes/minimalist-mobile/minimalist-mobile.yaml
Executable file → Normal file
4
themes/minimalist-mobile/minimalist-mobile.yaml
Executable file → Normal file
@ -10,6 +10,10 @@ minimalist-mobile:
|
|||||||
info-color: "var(--google-blue)"
|
info-color: "var(--google-blue)"
|
||||||
divider-color: "rgba(var(--color-theme),.12)"
|
divider-color: "rgba(var(--color-theme),.12)"
|
||||||
accent-color: "var(--google-yellow)"
|
accent-color: "var(--google-yellow)"
|
||||||
|
ha-dialog-border-radius: "10px"
|
||||||
|
# fix added border-lines in 2022.11
|
||||||
|
ha-card-border-width: "0px"
|
||||||
|
|
||||||
card-mod-theme: "minimalist-mobile"
|
card-mod-theme: "minimalist-mobile"
|
||||||
card-mod-root: |
|
card-mod-root: |
|
||||||
app-toolbar {
|
app-toolbar {
|
||||||
|
Reference in New Issue
Block a user