Compare commits
359 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bb392980e | ||
|
|
92264619d2 | ||
|
|
3e21829fae | ||
|
|
d3619b0e48 | ||
|
|
0fbb05d62d | ||
|
|
01fc2591ad | ||
|
|
c08d7fa5cd | ||
|
|
91c0e1c125 | ||
|
|
3d137f95d9 | ||
|
|
d5e8062f09 | ||
|
|
2dc4b2cb8a | ||
|
|
5ff0074d33 | ||
|
|
bbc88e175c | ||
|
|
ff54bea0ed | ||
|
|
bcfa807e12 | ||
|
|
728e94f120 | ||
|
|
5f2832d632 | ||
|
|
59e5be280f | ||
|
|
e6424acf5f | ||
|
|
34389e9133 | ||
|
|
b0f7629607 | ||
|
|
6e50f1f769 | ||
|
|
708b506f6f | ||
|
|
673d63daf0 | ||
|
|
ea55fed8d1 | ||
|
|
f5fd539eba | ||
|
|
51a56f441c | ||
|
|
54310b1d79 | ||
|
|
cb30b761dc | ||
|
|
cb50a7cac8 | ||
|
|
738af245cb | ||
|
|
71e9af1b91 | ||
|
|
1cd2566d0a | ||
|
|
1f1e5f23a2 | ||
|
|
3394234e4f | ||
|
|
e9882124ff | ||
|
|
1e5b19c149 | ||
|
|
c9339fec2f | ||
|
|
018c09e84f | ||
|
|
46e7f9a569 | ||
|
|
996ab58252 | ||
|
|
04974d5287 | ||
|
|
1a8a5ccb0e | ||
|
|
4c61c4c345 | ||
|
|
a6d6bfe73d | ||
|
|
f159e9ccb4 | ||
|
|
8ae2932274 | ||
|
|
52786ba954 | ||
|
|
f5cc526e8a | ||
|
|
a616aaba04 | ||
|
|
15b4177ce4 | ||
|
|
5b382cdb0b | ||
|
|
16622099c8 | ||
|
|
a75434fe7b | ||
|
|
020558ed4d | ||
|
|
39a82d03bc | ||
|
|
41ecb5dbc6 | ||
|
|
2d057ca9f6 | ||
|
|
b71b23d2a0 | ||
|
|
b32649435d | ||
|
|
c0096126df | ||
|
|
d632360932 | ||
|
|
400001fa38 | ||
|
|
4ff32a8081 | ||
|
|
33b4ae2f2f | ||
|
|
62194bd627 | ||
|
|
83bb2950fa | ||
|
|
262dee3cfd | ||
|
|
7ccf6ed610 | ||
|
|
21b189f5a8 | ||
|
|
d8d8a050ce | ||
|
|
b9ca810903 | ||
|
|
6ad750d3e9 | ||
|
|
8423b64825 | ||
|
|
742ddef227 | ||
|
|
85d7f0abfb | ||
|
|
c4b4fa293d | ||
|
|
0204abfead | ||
|
|
3510f7b9d3 | ||
|
|
2d4c063dfa | ||
|
|
67b3e2f312 | ||
|
|
82006de30f | ||
|
|
5bde9d7fe6 | ||
|
|
f7cd428366 | ||
|
|
d5e797de9e | ||
|
|
b9d5e7b206 | ||
|
|
2d8c7eb4fd | ||
|
|
f69e07fe68 | ||
|
|
84aab38954 | ||
|
|
dcf37481bd | ||
|
|
1a9cca84d5 | ||
|
|
c5272d67de | ||
|
|
3bcfb14177 | ||
|
|
566280f280 | ||
|
|
a814f7eefb | ||
|
|
097b8ed534 | ||
|
|
da47d72749 | ||
|
|
abd4d18a01 | ||
|
|
2adbce3c21 | ||
|
|
c41324b324 | ||
|
|
151a4f6c2d | ||
|
|
d23777a83f | ||
|
|
bb0aee8337 | ||
|
|
2a23acb73a | ||
|
|
ad5eb0cef6 | ||
|
|
07dd8f55fe | ||
|
|
bafa91cc47 | ||
|
|
16399510fa | ||
|
|
e9f54eec0a | ||
|
|
fbbbc9f215 | ||
|
|
69e4f575c0 | ||
|
|
e95659d2e0 | ||
|
|
35f34310ec | ||
|
|
acc18e20fd | ||
|
|
72460eac08 | ||
|
|
3e5f9d4eca | ||
|
|
e873fa252c | ||
|
|
ff2c083a19 | ||
|
|
a30a84c34b | ||
|
|
97d2023298 | ||
|
|
1ce8430a14 | ||
|
|
1c0b638818 | ||
|
|
e852588eeb | ||
|
|
08b9bfd854 | ||
|
|
7ee2f3a29a | ||
|
|
5ee6a38f39 | ||
|
|
8f0bfd5f83 | ||
|
|
c54f39fc77 | ||
|
|
eb40769ccf | ||
|
|
418c62b40d | ||
|
|
198a480c35 | ||
|
|
2d4bf4e847 | ||
|
|
774e3d1a62 | ||
|
|
ade3cd6fee | ||
|
|
28dc3ccb84 | ||
|
|
6ca8582ec3 | ||
|
|
74b4aeb44a | ||
|
|
9c7ab5ac57 | ||
|
|
65ecf1fea2 | ||
|
|
44142c658b | ||
|
|
25a205ce6c | ||
|
|
25094084cf | ||
|
|
4eac601153 | ||
|
|
f0d8d66b9b | ||
|
|
cfa550f8c0 | ||
|
|
91f6a5bf41 | ||
|
|
464bd6be65 | ||
|
|
031d7e2186 | ||
|
|
126b0d124c | ||
|
|
81c84a3e8f | ||
|
|
406d5bd549 | ||
|
|
cd5fe09fd9 | ||
|
|
766fc4efed | ||
|
|
b70fed40c8 | ||
|
|
255d98fd08 | ||
|
|
78304631f7 | ||
|
|
ab230844fc | ||
|
|
8a58cb9fd3 | ||
|
|
70b6ed73dc | ||
|
|
d2400bf44e | ||
|
|
db780fe876 | ||
|
|
cd84ae828a | ||
|
|
970d5d1031 | ||
|
|
da0e327ec7 | ||
|
|
4c84a8d572 | ||
|
|
1cfd895deb | ||
|
|
d0da08eb10 | ||
|
|
ee4f2cd87d | ||
|
|
8a6577c8aa | ||
|
|
ed5eae4187 | ||
|
|
f3b25027ad | ||
|
|
228daecbbf | ||
|
|
81aa569d07 | ||
|
|
aba8d7b8b9 | ||
|
|
449403bc57 | ||
|
|
997a1bbe31 | ||
|
|
2c7e0e3efe | ||
|
|
6051a46654 | ||
|
|
c7847d2789 | ||
|
|
215af72a6b | ||
|
|
b33586f8eb | ||
|
|
8db81d9b3c | ||
|
|
cf3b2fedf4 | ||
|
|
81db6d8e28 | ||
|
|
d47e59d057 | ||
|
|
8388d2f4ac | ||
|
|
f5ec513ce0 | ||
|
|
8975842f0b | ||
|
|
c6ca1df112 | ||
|
|
92f58a9682 | ||
|
|
5dc19dbd78 | ||
|
|
b0fc11bcc1 | ||
|
|
23ebe460a4 | ||
|
|
92db8c8161 | ||
|
|
d9b522734a | ||
|
|
e02457f0b2 | ||
|
|
104b4c18c2 | ||
|
|
1970f8c924 | ||
|
|
e431a06ac5 | ||
|
|
6da9b58088 | ||
|
|
3108443041 | ||
|
|
37ebb9e6c3 | ||
|
|
6c2e0f59b1 | ||
|
|
6eb9cdce90 | ||
|
|
e96d9447d1 | ||
|
|
d3cca11322 | ||
|
|
86155db455 | ||
|
|
5b078b4b27 | ||
|
|
9672dd6873 | ||
|
|
1587f65196 | ||
|
|
5ff10c0cdd | ||
|
|
aa0a028564 | ||
|
|
82f6d2f274 | ||
|
|
833de3ab43 | ||
|
|
08d273c7c1 | ||
|
|
aac7598187 | ||
|
|
5c0ac4e665 | ||
|
|
7bd5e49412 | ||
|
|
53ff3c5f79 | ||
|
|
36ae6e5272 | ||
|
|
fb3dffb216 | ||
|
|
ff6a6d2ec6 | ||
|
|
f4bbc2c3e5 | ||
|
|
4dbfdbe29c | ||
|
|
5e2a18f91e | ||
|
|
3363bdc592 | ||
|
|
08180a2d59 | ||
|
|
1a64ff4038 | ||
|
|
8ad90a6abb | ||
|
|
8cdd5ff015 | ||
|
|
a08f434e1f | ||
|
|
9acd6d2fea | ||
|
|
0a1cdea2e3 | ||
|
|
73b1a0493c | ||
|
|
d3a5517fa9 | ||
|
|
ad42251ee9 | ||
|
|
0670938ed3 | ||
|
|
3e96889976 | ||
|
|
5a7b43ad74 | ||
|
|
08c4863a2e | ||
|
|
4dbab75cf4 | ||
|
|
a90ad3ba6e | ||
|
|
98a94ce4a6 | ||
|
|
f0a8b6e1c7 | ||
|
|
e07bd3bffb | ||
|
|
dcce944390 | ||
|
|
03ecd118a3 | ||
|
|
97c0331762 | ||
|
|
eda9804dea | ||
|
|
e94c81ce44 | ||
|
|
c95c58138e | ||
|
|
03c93b4de1 | ||
|
|
ff0d15c365 | ||
|
|
eadcb76d31 | ||
|
|
b7ce9288f8 | ||
|
|
e077a099d9 | ||
|
|
8542acfb01 | ||
|
|
0d80ce5a0e | ||
|
|
ddcafe0f2b | ||
|
|
ea195b34db | ||
|
|
7377cb0d26 | ||
|
|
24b66de971 | ||
|
|
62d664a14c | ||
|
|
03b9a90f68 | ||
|
|
fefe0324b9 | ||
|
|
62b14a78b7 | ||
|
|
0ff505bbb4 | ||
|
|
b6c8c930a2 | ||
|
|
903bb93c4e | ||
|
|
59667cf104 | ||
|
|
3fd1b41bec | ||
|
|
6569107f64 | ||
|
|
9d746a6dcb | ||
|
|
fce4c07c32 | ||
|
|
094857758a | ||
|
|
2a49b89849 | ||
|
|
4ecd135734 | ||
|
|
836defc216 | ||
|
|
f8f777b5b5 | ||
|
|
b15e0a7363 | ||
|
|
5c1d06f743 | ||
|
|
51de56feb3 | ||
|
|
256a4ac909 | ||
|
|
09800c8ad2 | ||
|
|
83a7d8c60f | ||
|
|
ee2698be50 | ||
|
|
dc43087b0d | ||
|
|
4fa3511725 | ||
|
|
4b9ae70424 | ||
|
|
74ebffb4fc | ||
|
|
54206da449 | ||
|
|
dd54ff7ee4 | ||
|
|
abef0c3d59 | ||
|
|
957c9a3678 | ||
|
|
50ccfec1b3 | ||
|
|
8e7d6751e2 | ||
|
|
b77c4173c6 | ||
|
|
4941cffb70 | ||
|
|
81d5d23189 | ||
|
|
9da5a836ce | ||
|
|
c9a536fc60 | ||
|
|
fa172b56b0 | ||
|
|
ee45f2342e | ||
|
|
1f59ef025d | ||
|
|
d6a153144f | ||
|
|
99001e2e13 | ||
|
|
92b847656e | ||
|
|
a41525e828 | ||
|
|
5e9588cc56 | ||
|
|
b8239703c1 | ||
|
|
5d49135b59 | ||
|
|
3a5a76080b | ||
|
|
f23e10d629 | ||
|
|
b7d4891140 | ||
|
|
5f5cbd9060 | ||
|
|
8379359caf | ||
|
|
62238192ce | ||
|
|
1997003643 | ||
|
|
3a81844898 | ||
|
|
0ac80fb205 | ||
|
|
9494018c12 | ||
|
|
0bc86c98c5 | ||
|
|
f0d69c9ca7 | ||
|
|
b81590bd2e | ||
|
|
a53e01df6f | ||
|
|
f63e063954 | ||
|
|
9cbaf7076a | ||
|
|
daa5ac5870 | ||
|
|
0b8c08016b | ||
|
|
8c768d351b | ||
|
|
c9e7fa2629 | ||
|
|
9d3f2b5968 | ||
|
|
283e3d5e11 | ||
|
|
add4b575c2 | ||
|
|
af2f1e9ad5 | ||
|
|
8258320a7b | ||
|
|
a5dc7f485b | ||
|
|
025b5bf6f0 | ||
|
|
3d3064d78e | ||
|
|
2e3991355b | ||
|
|
73a4cf5834 | ||
|
|
b120064e80 | ||
|
|
3ec833e700 | ||
|
|
29aeea1194 | ||
|
|
994d53ae3b | ||
|
|
a95333eb1c | ||
|
|
c5f2d71791 | ||
|
|
26ae6ebfb2 | ||
|
|
e65cb0573d | ||
|
|
f8590b0c5f | ||
|
|
43b4992cee | ||
|
|
98e2cfae84 | ||
|
|
cb01c1a8ee | ||
|
|
36a273ec2b | ||
|
|
6a0dc03b9d | ||
|
|
ce7b006c8f | ||
|
|
88cc05bcea | ||
|
|
ae749f4a90 | ||
|
|
36b30a2cdd |
17
.coveragerc
Normal file
17
.coveragerc
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
# Skip @abstractmethod
|
||||||
|
@abstractmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
|
||||||
|
# Don't complain if tests don't hit defensive assertion code:
|
||||||
|
raise AssertionError
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# Don't complain about missing debug-only code:
|
||||||
|
def __repr__
|
||||||
|
if self\.debug
|
||||||
|
|
||||||
|
# Don't complain if non-runnable code isn't run:
|
||||||
|
if 0:
|
||||||
|
if __name__ == .__main__.:
|
||||||
2
.github/workflows/python-publish.yml
vendored
2
.github/workflows/python-publish.yml
vendored
@@ -19,4 +19,4 @@ jobs:
|
|||||||
- name: Build using poetry and publish to PyPi
|
- name: Build using poetry and publish to PyPi
|
||||||
uses: JRubics/poetry-publish@v1.11
|
uses: JRubics/poetry-publish@v1.11
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ pyvenv.cfg
|
|||||||
.env/
|
.env/
|
||||||
bin/
|
bin/
|
||||||
lib/
|
lib/
|
||||||
|
.idea/
|
||||||
|
|||||||
27
.pre-commit-config.yaml
Normal file
27
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.3.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-added-large-files
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.6.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: 5.10.1
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
name: isort (python)
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: unittest
|
||||||
|
name: unittest
|
||||||
|
entry: python -m unittest discover
|
||||||
|
language: system
|
||||||
|
'types': [python]
|
||||||
|
args: ["-p '*test.py'"] # Probably this option is absolutely not needed.
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [commit]
|
||||||
176
LICENSE.txt
Normal file
176
LICENSE.txt
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
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
|
||||||
113
README.md
113
README.md
@@ -5,102 +5,82 @@
|
|||||||
[](https://pypi.org/project/pyasic/)
|
[](https://pypi.org/project/pyasic/)
|
||||||
[](https://pypi.org/project/pyasic/)
|
[](https://pypi.org/project/pyasic/)
|
||||||
[](https://pyasic.readthedocs.io/en/latest/)
|
[](https://pyasic.readthedocs.io/en/latest/)
|
||||||
## Documentation
|
[](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
|
||||||
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/)
|
[](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
|
||||||
|
## Documentation and Supported Miners
|
||||||
|
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/).
|
||||||
|
|
||||||
## Usage
|
Supported miners are listed in the docs, [here](https://pyasic.readthedocs.io/en/latest/miners/supported_types/).
|
||||||
|
|
||||||
### Standard Usage
|
## Installation
|
||||||
You can install pyasic directly from pip with the command `pip install pyasic`
|
You can install pyasic directly from pip with the command `pip install pyasic`.
|
||||||
|
|
||||||
For those of you who aren't comfortable with code and developer tools, there are windows builds of GUI applications that use this library here -> (https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing)
|
For those of you who aren't comfortable with code and developer tools, there are windows builds of GUI applications that use this library [here](https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing).
|
||||||
|
|
||||||
### Developers
|
## Developer Setup
|
||||||
To use this repo, first download it, create a virtual environment, enter the virtual environment, and install relevant packages by navigating to this directory and running ```pip install -r requirements.txt``` on Windows or ```pip3 install -r requirements.txt``` on Mac or UNIX if the first command fails.
|
It is highly reccommended that you contribute to this project through [`pyasic-super`](https://github.com/UpstreamData/pyasic-super) using its submodules. This allows testing in conjunction with other `pyasic` related programs.
|
||||||
|
|
||||||
You can also use poetry by initializing and running ```poetry install```
|
|
||||||
|
|
||||||
### Interfacing with miners programmatically
|
|
||||||
|
|
||||||
##### Note: If you are trying to interface with Whatsminers, there is a bug in the way they are interacted with on Windows, so to fix that you need to change the event loop policy using this code:
|
|
||||||
```python
|
|
||||||
# need to import these 2 libraries, you need asyncio anyway so make sure you have sys imported
|
|
||||||
import sys
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
|
||||||
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
```
|
|
||||||
|
|
||||||
##### It is likely a good idea to use this code in your program anyway to be preventative.
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
To write your own custom programs with this repo, you have many options.
|
This repo uses poetry for dependencies, which can be installed by following the guide on their website [here](https://python-poetry.org/docs/#installation).
|
||||||
|
|
||||||
It is recommended that you explore the files in this repo to familiarize yourself with them, try starting with the miners module and going from there.
|
After you have poetry installed, run `poetry install --with dev`, or `poetry install --with dev,docs` if you want to include packages required for documentation.
|
||||||
|
|
||||||
There are 2 main ways to get a miner and it's functions via scanning or via the MinerFactory.
|
Finally, initialize pre-commit hooks with `poetry run pre-commit install`.
|
||||||
|
|
||||||
|
### Documentation Testing
|
||||||
|
Testing the documentation can be done by running `poetry run mkdocs serve`, whcih will serve the documentation locally on port 8000.
|
||||||
|
|
||||||
|
## Interfacing with miners programmatically
|
||||||
|
|
||||||
|
There are 2 main ways to get a miner (and the functions attached to it), via scanning or via the `MinerFactory()`.
|
||||||
|
|
||||||
#### Scanning for miners
|
#### Scanning for miners
|
||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
|
|
||||||
from pyasic.network import MinerNetwork
|
from pyasic.network import MinerNetwork
|
||||||
|
|
||||||
# Fix whatsminer bug
|
|
||||||
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
|
||||||
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
|
|
||||||
|
|
||||||
# define asynchronous function to scan for miners
|
# define asynchronous function to scan for miners
|
||||||
async def scan_and_get_data():
|
async def scan_and_get_data():
|
||||||
# Define network range to be used for scanning
|
# Define network range to be used for scanning
|
||||||
# This can take a list of IPs, a constructor string, or an IP and subnet mask
|
# This can take a list of IPs, a constructor string, or an IP and subnet mask
|
||||||
# The standard mask is /24, and you can pass any IP address in the subnet
|
# The standard mask is /24 (x.x.x.0-255), and you can pass any IP address in the subnet
|
||||||
net = MinerNetwork("192.168.1.69", mask=24)
|
net = MinerNetwork("192.168.1.69", mask=24)
|
||||||
# Scan the network for miners
|
# Scan the network for miners
|
||||||
# This function returns a list of miners of the correct type as a class
|
# This function returns a list of miners of the correct type as a class
|
||||||
miners: list = await net.scan_network_for_miners()
|
miners: list = await net.scan_network_for_miners()
|
||||||
|
|
||||||
# We can now get data from any of these miners
|
# We can now get data from any of these miners
|
||||||
# To do them all we have to create a list of tasks and gather them
|
# To do them all we have to create a list of tasks and gather them
|
||||||
tasks = [miner.get_data() for miner in miners]
|
tasks = [miner.get_data() for miner in miners]
|
||||||
# Gather all tasks asynchronously and run them
|
# Gather all tasks asynchronously and run them
|
||||||
data = await asyncio.gather(*tasks)
|
data = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
# Data is now a list of MinerData, and we can reference any part of that
|
# Data is now a list of MinerData, and we can reference any part of that
|
||||||
# Print out all data for now
|
# Print out all data for now
|
||||||
for item in data:
|
for item in data:
|
||||||
print(item)
|
print(item)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(scan_and_get_data())
|
asyncio.run(scan_and_get_data())
|
||||||
```
|
```
|
||||||
|
|
||||||
</br>
|
|
||||||
|
|
||||||
#### Getting a miner if you know the IP
|
#### Getting a miner if you know the IP
|
||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
|
|
||||||
from pyasic.miners.miner_factory import MinerFactory
|
from pyasic import get_miner
|
||||||
|
|
||||||
# Fix whatsminer bug
|
|
||||||
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
|
||||||
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
|
|
||||||
|
|
||||||
# define asynchronous function to get miner and data
|
# define asynchronous function to get miner and data
|
||||||
async def get_miner_data(miner_ip: str):
|
async def get_miner_data(miner_ip: str):
|
||||||
# Use MinerFactory to get miner
|
# Use MinerFactory to get miner
|
||||||
# MinerFactory is a singleton, so we can just get the instance in place
|
# MinerFactory is a singleton, so we can just get the instance in place
|
||||||
miner = await MinerFactory().get_miner(miner_ip)
|
miner = await get_miner(miner_ip)
|
||||||
|
|
||||||
# Get data from the miner
|
# Get data from the miner
|
||||||
data = await miner.get_data()
|
data = await miner.get_data()
|
||||||
print(data)
|
print(data)
|
||||||
@@ -113,27 +93,26 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
If needed, this library exposes a wrapper for the miner API that can be used for advanced data gathering.
|
If needed, this library exposes a wrapper for the miner API that can be used for advanced data gathering.
|
||||||
|
|
||||||
|
You can see more information on basic usage of the APIs past this example in the docs [here](https://pyasic.readthedocs.io/en/latest/API/api/).
|
||||||
|
|
||||||
|
Please see the appropriate API documentation page (pyasic docs -> Advanced -> Miner APIs -> your API type) for a link to that specific miner's API documentation page and more information.
|
||||||
|
|
||||||
#### List available API commands
|
#### List available API commands
|
||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
|
|
||||||
from pyasic.miners.miner_factory import MinerFactory
|
from pyasic import get_miner
|
||||||
|
|
||||||
# Fix whatsminer bug
|
|
||||||
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
|
||||||
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
|
|
||||||
|
|
||||||
async def get_api_commands(miner_ip: str):
|
async def get_api_commands(miner_ip: str):
|
||||||
# Get the miner
|
# Get the miner
|
||||||
miner = await MinerFactory().get_miner(miner_ip)
|
miner = await get_miner(miner_ip)
|
||||||
|
|
||||||
# List all available commands
|
# List all available commands
|
||||||
print(miner.api.get_commands())
|
# Can also be called explicitly with the function miner.api.get_commands()
|
||||||
|
print(miner.api.commands)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(get_api_commands("192.168.1.69"))
|
asyncio.run(get_api_commands("192.168.1.69"))
|
||||||
```
|
```
|
||||||
@@ -144,26 +123,20 @@ The miner API commands will raise an `APIError` if they fail with a bad status c
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
|
|
||||||
from pyasic.miners.miner_factory import MinerFactory
|
from pyasic import get_miner
|
||||||
|
|
||||||
# Fix whatsminer bug
|
|
||||||
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
|
||||||
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
|
|
||||||
|
|
||||||
async def get_api_commands(miner_ip: str):
|
async def get_api_commands(miner_ip: str):
|
||||||
# Get the miner
|
# Get the miner
|
||||||
miner = await MinerFactory().get_miner(miner_ip)
|
miner = await get_miner(miner_ip)
|
||||||
|
|
||||||
# Run the devdetails command
|
# Run the devdetails command
|
||||||
# This is equivalent to await miner.api.send_command("devdetails")
|
# This is equivalent to await miner.api.send_command("devdetails")
|
||||||
devdetails: dict = await miner.api.devdetails()
|
devdetails: dict = await miner.api.devdetails()
|
||||||
print(devdetails)
|
print(devdetails)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(get_api_commands("192.168.1.69"))
|
asyncio.run(get_api_commands("192.168.1.69"))
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
## Miner APIs
|
## Miner APIs
|
||||||
Each miner has a unique API that is used to communicate with it.
|
Each miner has a unique API that is used to communicate with it.
|
||||||
Each of these API types has commands that differ between them, and some commands have data that others do not.
|
Each of these API types has commands that differ between them, and some commands have data that others do not.
|
||||||
Each miner that is a subclass of `BaseMiner` should have an API linked to it as `Miner.api`.
|
Each miner that is a subclass of [`BaseMiner`][pyasic.miners.BaseMiner] should have an API linked to it as `Miner.api`.
|
||||||
|
|
||||||
All API implementations inherit from [`BaseMinerAPI`][pyasic.API.BaseMinerAPI], which implements the basic communications protocols.
|
All API implementations inherit from [`BaseMinerAPI`][pyasic.API.BaseMinerAPI], which implements the basic communications protocols.
|
||||||
|
|
||||||
BaseMinerAPI should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
|
[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
|
||||||
|
[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] cannot be instantiated directly, it will raise a `TypeError`.
|
||||||
Use these instead -
|
Use these instead -
|
||||||
|
|
||||||
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]
|
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]
|
||||||
|
|||||||
33
docs/data/error_codes.md
Normal file
33
docs/data/error_codes.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# pyasic
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Whatsminer Error Codes
|
||||||
|
::: pyasic.data.error_codes.WhatsminerError
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Braiins OS Error Codes
|
||||||
|
::: pyasic.data.error_codes.BraiinsOSError
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## X19 Error Codes
|
||||||
|
::: pyasic.data.error_codes.X19Error
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Innosilicon Error Codes
|
||||||
|
::: pyasic.data.error_codes.InnosiliconError
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
@@ -6,3 +6,10 @@
|
|||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
|
||||||
|
## HashBoard Data
|
||||||
|
::: pyasic.data.HashBoard
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|||||||
189
docs/index.md
189
docs/index.md
@@ -5,10 +5,14 @@
|
|||||||
[](https://pypi.org/project/pyasic/)
|
[](https://pypi.org/project/pyasic/)
|
||||||
[](https://pypi.org/project/pyasic/)
|
[](https://pypi.org/project/pyasic/)
|
||||||
[](https://pyasic.readthedocs.io/en/latest/)
|
[](https://pyasic.readthedocs.io/en/latest/)
|
||||||
|
[](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
|
||||||
|
[](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
|
||||||
|
|
||||||
## Intro
|
## Intro
|
||||||
Welcome to pyasic! Pyasic uses an asynchronous method of communicating with asic miners on your network, which makes it super fast.
|
Welcome to pyasic! Pyasic uses an asynchronous method of communicating with asic miners on your network, which makes it super fast.
|
||||||
|
|
||||||
|
[Supported Miner Types](miners/supported_types.md)
|
||||||
|
|
||||||
Getting started with pyasic is easy. First, find your miner (or miners) on the network by scanning for them or getting the correct class automatically for them if you know the IP.
|
Getting started with pyasic is easy. First, find your miner (or miners) on the network by scanning for them or getting the correct class automatically for them if you know the IP.
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
@@ -25,13 +29,13 @@ async def scan_miners(): # define async scan function to allow awaiting
|
|||||||
# create a miner network
|
# create a miner network
|
||||||
# you can pass in any IP and it will use that in a subnet with a /24 mask (255 IPs).
|
# you can pass in any IP and it will use that in a subnet with a /24 mask (255 IPs).
|
||||||
network = MinerNetwork("192.168.1.50") # this uses the 192.168.1.0-255 network
|
network = MinerNetwork("192.168.1.50") # this uses the 192.168.1.0-255 network
|
||||||
|
|
||||||
# scan for miners asynchronously
|
# scan for miners asynchronously
|
||||||
# this will return the correct type of miners if they are supported with all functionality.
|
# this will return the correct type of miners if they are supported with all functionality.
|
||||||
miners = await network.scan_network_for_miners()
|
miners = await network.scan_network_for_miners()
|
||||||
print(miners)
|
print(miners)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(scan_miners()) # run the scan asynchronously with asyncio.run()
|
asyncio.run(scan_miners()) # run the scan asynchronously with asyncio.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -52,8 +56,8 @@ async def get_miners(): # define async scan function to allow awaiting
|
|||||||
miner_1 = await MinerFactory().get_miner("192.168.1.75")
|
miner_1 = await MinerFactory().get_miner("192.168.1.75")
|
||||||
miner_2 = await MinerFactory().get_miner("192.168.1.76")
|
miner_2 = await MinerFactory().get_miner("192.168.1.76")
|
||||||
print(miner_1, miner_2)
|
print(miner_1, miner_2)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run()
|
asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ async def gather_miner_data():
|
|||||||
print(miner_data) # all data from the dataclass
|
print(miner_data) # all data from the dataclass
|
||||||
print(miner_data.hashrate) # hashrate of the miner in TH/s
|
print(miner_data.hashrate) # hashrate of the miner in TH/s
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(gather_miner_data())
|
asyncio.run(gather_miner_data())
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,13 +91,184 @@ from pyasic.network import MinerNetwork # miner network handles the scanning
|
|||||||
async def gather_miner_data(): # define async scan function to allow awaiting
|
async def gather_miner_data(): # define async scan function to allow awaiting
|
||||||
network = MinerNetwork("192.168.1.50")
|
network = MinerNetwork("192.168.1.50")
|
||||||
miners = await network.scan_network_for_miners()
|
miners = await network.scan_network_for_miners()
|
||||||
|
|
||||||
# we need to asyncio.gather() all the miners get_data() functions to make them run together
|
# we need to asyncio.gather() all the miners get_data() functions to make them run together
|
||||||
all_miner_data = await asyncio.gather(*[miner.get_data() for miner in miners])
|
all_miner_data = await asyncio.gather(*[miner.get_data() for miner in miners])
|
||||||
|
|
||||||
for miner_data in all_miner_data:
|
for miner_data in all_miner_data:
|
||||||
print(miner_data) # print out all the data one by one
|
print(miner_data) # print out all the data one by one
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(gather_miner_data())
|
asyncio.run(gather_miner_data())
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Controlling miners via pyasic
|
||||||
|
Every miner class in pyasic must implement all the control functions defined in [`BaseMiner`][pyasic.miners.BaseMiner].
|
||||||
|
|
||||||
|
These functions are
|
||||||
|
[`check_light`](#check-light),
|
||||||
|
[`fault_light_off`](#fault-light-off),
|
||||||
|
[`fault_light_on`](#fault-light-on),
|
||||||
|
[`get_config`](#get-config),
|
||||||
|
[`get_data`](#get-data),
|
||||||
|
[`get_errors`](#get-errors),
|
||||||
|
[`get_hostname`](#get-hostname),
|
||||||
|
[`get_model`](#get-model),
|
||||||
|
[`reboot`](#reboot),
|
||||||
|
[`restart_backend`](#restart-backend),
|
||||||
|
[`stop_mining`](#stop-mining),
|
||||||
|
[`resume_mining`](#resume-mining),
|
||||||
|
[`send_config`](#send-config), and
|
||||||
|
[`set_power_limit`](#set-power-limit).
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Check Light
|
||||||
|
::: pyasic.miners.BaseMiner.check_light
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Fault Light Off
|
||||||
|
::: pyasic.miners.BaseMiner.fault_light_off
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Fault Light On
|
||||||
|
::: pyasic.miners.BaseMiner.fault_light_on
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Get Config
|
||||||
|
::: pyasic.miners.BaseMiner.get_config
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Get Data
|
||||||
|
::: pyasic.miners.BaseMiner.get_data
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Get Errors
|
||||||
|
::: pyasic.miners.BaseMiner.get_errors
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Get Hostname
|
||||||
|
::: pyasic.miners.BaseMiner.get_hostname
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Get Model
|
||||||
|
::: pyasic.miners.BaseMiner.get_model
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Reboot
|
||||||
|
::: pyasic.miners.BaseMiner.reboot
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Restart Backend
|
||||||
|
::: pyasic.miners.BaseMiner.restart_backend
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Stop Mining
|
||||||
|
::: pyasic.miners.BaseMiner.stop_mining
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Resume Mining
|
||||||
|
::: pyasic.miners.BaseMiner.resume_mining
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Send Config
|
||||||
|
::: pyasic.miners.BaseMiner.send_config
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Set Power Limit
|
||||||
|
::: pyasic.miners.BaseMiner.set_power_limit
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## [`MinerConfig`][pyasic.config.MinerConfig] and [`MinerData`][pyasic.data.MinerData]
|
||||||
|
|
||||||
|
Pyasic implements a few dataclasses as helpers to make data return types consistent across different miners and miner APIs. The different fields of these dataclasses can all be viewed with the classmethod `cls.fields()`.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### [`MinerData`][pyasic.data.MinerData]
|
||||||
|
|
||||||
|
[`MinerData`][pyasic.data.MinerData] is a return from the [`get_data()`](#get-data) function, and is used to have a consistent dataset across all returns.
|
||||||
|
|
||||||
|
You can call [`MinerData.asdict()`][pyasic.data.MinerData.asdict] to get the dataclass as a dictionary, and there are many other helper functions contained in the class to convert to different data formats.
|
||||||
|
|
||||||
|
[`MinerData`][pyasic.data.MinerData] instances can also be added to each other to combine their data and can be divided by a number to divide all their data, allowing you to get average data from many miners by doing -
|
||||||
|
```python
|
||||||
|
from pyasic import MinerData
|
||||||
|
|
||||||
|
# examples of miner data
|
||||||
|
d1 = MinerData("192.168.1.1")
|
||||||
|
d2 = MinerData("192.168.1.2")
|
||||||
|
|
||||||
|
list_of_miner_data = [d1, d2]
|
||||||
|
|
||||||
|
average_data = sum(list_of_miner_data, start=MinerData("0.0.0.0"))/len(list_of_miner_data)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### [`MinerConfig`][pyasic.config.MinerConfig]
|
||||||
|
|
||||||
|
[`MinerConfig`][pyasic.config.MinerConfig] is pyasic's way to represent a configuration file from a miner.
|
||||||
|
It is the return from [`get_config()`](#get-config).
|
||||||
|
|
||||||
|
Each miner has a unique way to convert the [`MinerConfig`][pyasic.config.MinerConfig] to their specific type, there are helper functions in the class.
|
||||||
|
In most cases these helper functions should not be used, as [`send_config()`](#send-config) takes a [`MinerConfig`][pyasic.config.MinerConfig] and will do the conversion to the right type for you.
|
||||||
|
|||||||
117
docs/miners/antminer/X17.md
Normal file
117
docs/miners/antminer/X17.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# pyasic
|
||||||
|
## X17 Models
|
||||||
|
|
||||||
|
## S17
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S17+
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X17.S17_Plus.BMMinerS17Plus
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S17 Pro
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X17.S17_Pro.BMMinerS17Pro
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S17e
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X17.S17e.BMMinerS17e
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T17
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T17+
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X17.T17_Plus.BMMinerT17Plus
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## T17e
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X17.T17e.BMMinerT17e
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## S17 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S17+ (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.S17_Plus.BOSMinerS17Plus
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S17 Pro (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.S17_Pro.BOSMinerS17Pro
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S17e (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.S17e.BOSMinerS17e
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T17 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T17+ (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.T17_Plus.BOSMinerT17Plus
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## T17e (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.T17e.BOSMinerT17e
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
102
docs/miners/antminer/X19.md
Normal file
102
docs/miners/antminer/X19.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# pyasic
|
||||||
|
## X19 Models
|
||||||
|
|
||||||
|
## S19
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S19 Pro
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X19.S19_Pro.BMMinerS19Pro
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## S19a
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X19.S19a.BMMinerS19a
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## S19j
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X19.S19j.BMMinerS19j
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S19j Pro
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X19.S19j_Pro.BMMinerS19jPro
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S19 XP
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X19.S19_XP.BMMinerS19XP
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T19
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X19.T19.BMMinerT19
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## S19 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S19 Pro (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.S19_Pro.BOSMinerS19Pro
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## S19j (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.S19j.BOSMinerS19j
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S19j Pro (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.S19j_Pro.BOSMinerS19jPro
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T19 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
36
docs/miners/antminer/X9.md
Normal file
36
docs/miners/antminer/X9.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# pyasic
|
||||||
|
## X9 Models
|
||||||
|
|
||||||
|
|
||||||
|
## X9 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## S9
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S9i
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X9.S9i.BMMinerS9i
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T9
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bmminer.X9.T9.BMMinerT9
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
26
docs/miners/avalonminer/A10X.md
Normal file
26
docs/miners/avalonminer/A10X.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# pyasic
|
||||||
|
## A10X Models
|
||||||
|
|
||||||
|
## A1026
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## A1047
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## A1066
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
26
docs/miners/avalonminer/A7X.md
Normal file
26
docs/miners/avalonminer/A7X.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# pyasic
|
||||||
|
## A7X Models
|
||||||
|
|
||||||
|
## A721
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A7X.A721.CGMinerAvalon721
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## A741
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A7X.A741.CGMinerAvalon741
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## A761
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A7X.A761.CGMinerAvalon761
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
26
docs/miners/avalonminer/A8X.md
Normal file
26
docs/miners/avalonminer/A8X.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# pyasic
|
||||||
|
## A8X Models
|
||||||
|
|
||||||
|
## A821
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A8X.A821.CGMinerAvalon821
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## A841
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A8X.A841.CGMinerAvalon841
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## A851
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A8X.A851.CGMinerAvalon851
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
10
docs/miners/avalonminer/A9X.md
Normal file
10
docs/miners/avalonminer/A9X.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# pyasic
|
||||||
|
## A9X Models
|
||||||
|
|
||||||
|
## A921
|
||||||
|
|
||||||
|
::: pyasic.miners.avalonminer.cgminer.A9X.A921.CGMinerAvalon921
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
8
docs/miners/backends/bmminer.md
Normal file
8
docs/miners/backends/bmminer.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# pyasic
|
||||||
|
## BMMiner Backend
|
||||||
|
|
||||||
|
::: pyasic.miners._backends.bmminer.BMMiner
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
8
docs/miners/backends/bosminer.md
Normal file
8
docs/miners/backends/bosminer.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# pyasic
|
||||||
|
## BOSMiner Backend
|
||||||
|
|
||||||
|
::: pyasic.miners._backends.bosminer.BOSMiner
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
8
docs/miners/backends/btminer.md
Normal file
8
docs/miners/backends/btminer.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# pyasic
|
||||||
|
## BTMiner Backend
|
||||||
|
|
||||||
|
::: pyasic.miners._backends.btminer.BTMiner
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
8
docs/miners/backends/cgminer.md
Normal file
8
docs/miners/backends/cgminer.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# pyasic
|
||||||
|
## CGMiner Backend
|
||||||
|
|
||||||
|
::: pyasic.miners._backends.cgminer.CGMiner
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
8
docs/miners/backends/hiveon.md
Normal file
8
docs/miners/backends/hiveon.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# pyasic
|
||||||
|
## Hiveon Backend
|
||||||
|
|
||||||
|
::: pyasic.miners._backends.hiveon.Hiveon
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
10
docs/miners/base_miner.md
Normal file
10
docs/miners/base_miner.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# pyasic
|
||||||
|
## Base Miner
|
||||||
|
[`BaseMiner`][pyasic.miners.BaseMiner] is the basis for all miner classes, they all subclass (usually indirectly) from this class.
|
||||||
|
|
||||||
|
You may not instantiate this class on its own, only subclass from it. Trying to instantiate an instance of this class will raise `TypeError`.
|
||||||
|
|
||||||
|
::: pyasic.miners.BaseMiner
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
10
docs/miners/innosilicon/T3X.md
Normal file
10
docs/miners/innosilicon/T3X.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# pyasic
|
||||||
|
## T3X Models
|
||||||
|
|
||||||
|
## T3H+
|
||||||
|
|
||||||
|
::: pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
@@ -6,3 +6,15 @@
|
|||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## AnyMiner
|
||||||
|
::: pyasic.miners.miner_factory.AnyMiner
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
[`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] is a placeholder type variable used for typing returns of functions.
|
||||||
|
A function returning [`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] will always return a subclass of [`BaseMiner`][pyasic.miners.BaseMiner],
|
||||||
|
and is used to specify a function returning some arbitrary type of miner class instance.
|
||||||
|
|||||||
510
docs/miners/supported_types.md
Normal file
510
docs/miners/supported_types.md
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
# pyasic
|
||||||
|
## Supported Miners
|
||||||
|
|
||||||
|
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
|
||||||
|
|
||||||
|
##### pyasic currently supports the following miners and subtypes:
|
||||||
|
<style>
|
||||||
|
details {
|
||||||
|
margin:0px;
|
||||||
|
padding-top:0px;
|
||||||
|
padding-bottom:0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<details style="margin:0px; padding-top:0px; padding-bottom:0px;">
|
||||||
|
<summary>Braiins OS+ Devices:</summary>
|
||||||
|
<ul>
|
||||||
|
<details>
|
||||||
|
<summary>X19 Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../antminer/X19#s19-bos">S19</a></li>
|
||||||
|
<li><a href="../antminer/X19#s19-pro-bos">S19 Pro</a></li>
|
||||||
|
<li><a href="../antminer/X19#s19j-bos">S19j</a></li>
|
||||||
|
<li><a href="../antminer/X19#s19j-pro-bos">S19j Pro</a></li>
|
||||||
|
<li><a href="../antminer/X19#t19-bos">T19</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>X17 Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../antminer/X17#s17-bos">S17</a></li>
|
||||||
|
<li><a href="../antminer/X17#s17-plus-bos">S17+</a></li>
|
||||||
|
<li><a href="../antminer/X17#s17-pro-bos">S17 Pro</a></li>
|
||||||
|
<li><a href="../antminer/X17#s17e-bos">S17e</a></li>
|
||||||
|
<li><a href="../antminer/X17#t17-bos">T17</a></li>
|
||||||
|
<li><a href="../antminer/X17#t17-plus-bos">T17+</a></li>
|
||||||
|
<li><a href="../antminer/X17#t17e-bos">T17e</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>X9 Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../antminer/X9#s9-bos">S9</a></li>
|
||||||
|
<li><a href="../antminer/X9#s9-bos">S9i</a></li>
|
||||||
|
<li><a href="../antminer/X9#s9-bos">S9j</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Stock Firmware Whatsminers:</summary>
|
||||||
|
<ul>
|
||||||
|
<details>
|
||||||
|
<summary>M2X Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M2X/#M20'>M20</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M2X/#M20V10'>M20V10</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M2X/#M20S'>M20S</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M2X/#M20SV10'>M20SV10</a></li>
|
||||||
|
<li><a href='../whatsminer/M2X/#M20SV20'>M20SV20</a></li>
|
||||||
|
<li><a href='../whatsminer/M2X/#M20SV30'>M20SV30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M2X/#M20S_1'>M20S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M2X/#M20S_1V30'>M20S+V30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M2X/#M21'>M21</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M2X/#M21V10'>M21V10</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M2X/#M21S'>M21S</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M2X/#M21SV20'>M21SV20</a></li>
|
||||||
|
<li><a href='../whatsminer/M2X/#M21SV60'>M21SV60</a></li>
|
||||||
|
<li><a href='../whatsminer/M2X/#M21SV70'>M21SV70</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M2X/#M21S_1'>M21S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M2X/#M21S_1V20'>M21S+V20</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M2X/#M29'>M29</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M2X/#M29V10'>M29V10</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>M3X Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M30'>M30</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30V10'>M30V10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30V20'>M30V20</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M30S'>M30S</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SV10'>M30SV10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SV20'>M30SV20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SV30'>M30SV30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SV40'>M30SV40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SV50'>M30SV50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SV60'>M30SV60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SV70'>M30SV70</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SV80'>M30SV80</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVE10'>M30SVE10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVE20'>M30SVE20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVE30'>M30SVE30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVE40'>M30SVE40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVE50'>M30SVE50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVE60'>M30SVE60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVE70'>M30SVE70</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVF10'>M30SVF10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVF20'>M30SVF20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVF30'>M30SVF30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVG10'>M30SVG10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVG20'>M30SVG20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVG30'>M30SVG30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVG40'>M30SVG40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVH10'>M30SVH10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVH20'>M30SVH20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVH30'>M30SVH30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVH40'>M30SVH40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVH50'>M30SVH50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVH60'>M30SVH60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30SVI20'>M30SVI20</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M30S_1'>M30S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V10'>M30S+V10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V20'>M30S+V20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V30'>M30S+V30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V40'>M30S+V40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V50'>M30S+V50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V60'>M30S+V60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V70'>M30S+V70</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V80'>M30S+V80</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V90'>M30S+V90</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1V100'>M30S+V100</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VE30'>M30S+VE30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VE40'>M30S+VE40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VE50'>M30S+VE50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VE60'>M30S+VE60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VE70'>M30S+VE70</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VE80'>M30S+VE80</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VE90'>M30S+VE90</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VE100'>M30S+VE100</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VF20'>M30S+VF20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VF30'>M30S+VF30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M36S_1VG30'>M36S+VG30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VG30'>M30S+VG30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VG40'>M30S+VG40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VG50'>M30S+VG50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VG60'>M30S+VG60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VH10'>M30S+VH10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VH20'>M30S+VH20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VH30'>M30S+VH30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VH40'>M30S+VH40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VH50'>M30S+VH50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_1VH60'>M30S+VH60</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M30S_2'>M30S++</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2V10'>M30S++V10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2V20'>M30S++V20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VE30'>M30S++VE30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VE40'>M30S++VE40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VE50'>M30S++VE50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VF40'>M30S++VF40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VG30'>M30S++VG30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VG40'>M30S++VG40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VG50'>M30S++VG50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH10'>M30S++VH10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH20'>M30S++VH20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH30'>M30S++VH30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH40'>M30S++VH40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH50'>M30S++VH50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH60'>M30S++VH60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH70'>M30S++VH70</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH80'>M30S++VH80</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH90'>M30S++VH90</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VH100'>M30S++VH100</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VJ20'>M30S++VJ20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M30S_2VJ30'>M30S++VJ30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M31'>M31</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31V10'>M31V10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31V20'>M31V20</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M31S'>M31S</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SV10'>M31SV10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SV20'>M31SV20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SV30'>M31SV30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SV40'>M31SV40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SV50'>M31SV50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SV60'>M31SV60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SV70'>M31SV70</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SV80'>M31SV80</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SV90'>M31SV90</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SVE10'>M31SVE10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SVE20'>M31SVE20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SVE30'>M31SVE30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M31SE'>M31SE</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SEV10'>M31SEV10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SEV20'>M31SEV20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31SEV30'>M31SEV30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M31H'>M31H</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31HV40'>M31HV40</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M31S_1'>M31S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1V10'>M31S+V10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1V20'>M31S+V20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1V30'>M31S+V30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1V40'>M31S+V40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1V50'>M31S+V50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1V60'>M31S+V60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1V80'>M31S+V80</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1V90'>M31S+V90</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1V100'>M31S+V100</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VE10'>M31S+VE10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VE20'>M31S+VE20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VE30'>M31S+VE30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VE40'>M31S+VE40</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VE50'>M31S+VE50</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VE60'>M31S+VE60</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VE80'>M31S+VE80</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VF20'>M31S+VF20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VF30'>M31S+VF30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VG20'>M31S+VG20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M31S_1VG30'>M31S+VG30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M32'>M32</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M32V10'>M32V10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M32V20'>M32V20</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M33'>M33</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M33V10'>M33V10</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M33V20'>M33V20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M33V30'>M33V30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M33S'>M33S</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M33SVG30'>M33SVG30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M33S_1'>M33S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M33S_1VH20'>M33S+VH20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M33S_1VH30'>M33S+VH30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M33S_2'>M33S++</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M33S_2VH20'>M33S++VH20</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M33S_2VH30'>M33S++VH30</a></li>
|
||||||
|
<li><a href='../whatsminer/M3X/#M33S_2VG40'>M33S++VG40</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M34S_1'>M34S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M34S_1VE10'>M34S+VE10</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M36S'>M36S</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M36SVE10'>M36SVE10</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M36S_1'>M36S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M36S_1VG30'>M36S+VG30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M36S_2'>M36S++</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M36S_2VH30'>M36S++VH30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M3X/#M39'>M39</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M3X/#M39V20'>M39V20</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>M5X Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M50'>M50</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VG30'>M50VG30</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VH10'>M50VH10</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VH20'>M50VH20</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VH30'>M50VH30</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VH40'>M50VH40</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VH50'>M50VH50</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VH60'>M50VH60</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VH70'>M50VH70</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VH80'>M50VH80</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VJ10'>M50VJ10</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VJ20'>M50VJ20</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50VJ30'>M50VJ30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M50S'>M50S</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50SVJ10'>M50SVJ10</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50SVJ20'>M50SVJ20</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50SVJ30'>M50SVJ30</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50SVH10'>M50SVH10</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50SVH20'>M50SVH20</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50SVH30'>M50SVH30</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50SVH40'>M50SVH40</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50SVH50'>M50SVH50</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M50S_1'>M50S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50S_1VH30'>M50S+VH30</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50S_1VH40'>M50S+VH40</a></li>
|
||||||
|
<li><a href='../whatsminer/M5X/#M50S_1VJ30'>M50S+VJ30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M53'>M53</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M53VH30'>M53VH30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M53S'>M53S</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M53SVH30'>M53SVH30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M53S_1'>M53S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M53S_1VJ30'>M53S+VJ30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M56'>M56</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M56VH30'>M56VH30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M56S'>M56S</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M56SVH30'>M56SVH30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M56S_1'>M56S+</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M56S_1VJ30'>M56S+VJ30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><a href='../whatsminer/M5X/#M59'>M59</a></summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href='../whatsminer/M5X/#M59VH30'>M59VH30</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Stock Firmware Antminers:</summary>
|
||||||
|
<ul>
|
||||||
|
<details>
|
||||||
|
<summary>X19 Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../antminer/X19/#s19">S19</a></li>
|
||||||
|
<li><a href="../antminer/X19/#s19-pro">S19 Pro</a></li>
|
||||||
|
<li><a href="../antminer/X19/#s19a">S19a</a></li>
|
||||||
|
<li><a href="../antminer/X19/#s19j">S19j</a></li>
|
||||||
|
<li><a href="../antminer/X19/#s19j-pro">S19j Pro</a></li>
|
||||||
|
<li><a href="../antminer/X19/#s19-xp">S19 XP</a></li>
|
||||||
|
<li><a href="../antminer/X19/#t19">T19</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>X17 Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../antminer/X17/#s17">S17</a></li>
|
||||||
|
<li><a href="../antminer/X17/#s17_1">S17+</a></li>
|
||||||
|
<li><a href="../antminer/X17/#s17-pro">S17 Pro</a></li>
|
||||||
|
<li><a href="../antminer/X17/#s17e">S17e</a></li>
|
||||||
|
<li><a href="../antminer/X17/#t17">T17</a></li>
|
||||||
|
<li><a href="../antminer/X17/#t17_1">T17+</a></li>
|
||||||
|
<li><a href="../antminer/X17/#t17e">T17e</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>X9 Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../antminer/X9/#s9">S9</a></li>
|
||||||
|
<li><a href="../antminer/X9/#s9i">S9i</a></li>
|
||||||
|
<li><a href="../antminer/X9/#t9">T9</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Stock Firmware Avalonminers:</summary>
|
||||||
|
<ul>
|
||||||
|
<details>
|
||||||
|
<summary>A7X Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../avalonminer/A7X/#a721">A721</a></li>
|
||||||
|
<li><a href="../avalonminer/A7X/#a741">A741</a></li>
|
||||||
|
<li><a href="../avalonminer/A7X/#a761">A761</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>A8X Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../avalonminer/A8X/#a821">A821</a></li>
|
||||||
|
<li><a href="../avalonminer/A8X/#a841">A841</a></li>
|
||||||
|
<li><a href="../avalonminer/A8X/#a851">A851</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>A9X Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../avalonminer/A9X/#a921">A921</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>A10X Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../avalonminer/A10X/#a1026">A1026</a></li>
|
||||||
|
<li><a href="../avalonminer/A10X/#a1047">A1047</a></li>
|
||||||
|
<li><a href="../avalonminer/A10X/#a1066">A1066</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Stock Firmware Innosilicon Miners:</summary>
|
||||||
|
<ul>
|
||||||
|
<details>
|
||||||
|
<summary>T3X Series:</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../innosilicon/T3X/#t3h">T3H+</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
90
docs/miners/whatsminer/M2X.md
Normal file
90
docs/miners/whatsminer/M2X.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# pyasic
|
||||||
|
## M2X Models
|
||||||
|
|
||||||
|
## M20V10
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20V10
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M20SV10
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV10
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M20SV20
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV20
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M20SV30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M20S+V30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M20S_Plus.BTMinerM20SPlusV30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M21V10
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M21.BTMinerM21V10
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M21SV20
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV20
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M21SV60
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV60
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M21SV70
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV70
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M21S+V20
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M21S_Plus.BTMinerM21SPlusV20
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M29V10
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M29.BTMinerM29V10
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
1098
docs/miners/whatsminer/M3X.md
Normal file
1098
docs/miners/whatsminer/M3X.md
Normal file
File diff suppressed because it is too large
Load Diff
242
docs/miners/whatsminer/M5X.md
Normal file
242
docs/miners/whatsminer/M5X.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# pyasic
|
||||||
|
## M5X Models
|
||||||
|
|
||||||
|
## M50VG30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VG30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VH10
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH10
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VH20
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH20
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VH30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VH40
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH40
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VH50
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH50
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VH60
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH60
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VH70
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH70
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VH80
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH80
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VJ10
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ10
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VJ20
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ20
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50VJ30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50SVJ10
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ10
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50SVJ20
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ20
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50SVJ30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50SVH10
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH10
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50SVH20
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH20
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50SVH30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50SVH40
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH40
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50SVH50
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH50
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50S+VH30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50S+VH40
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH40
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M50S+VJ30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVJ30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M53VH30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VH30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M53SVH30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVH30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M53S+VJ30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVJ30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M56VH30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M56.BTMinerM56VH30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M56SVH30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M56S.BTMinerM56SVH30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M56S+VJ30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVJ30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M59VH30
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M5X.M59.BTMinerM59VH30
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
12
docs/network/miner_network_range.md
Normal file
12
docs/network/miner_network_range.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# pyasic
|
||||||
|
## Miner Network Range
|
||||||
|
|
||||||
|
[`MinerNetworkRange`][pyasic.network.net_range.MinerNetworkRange] is a class used by [`MinerNetwork`][pyasic.network.MinerNetwork] to handle any constructor stings.
|
||||||
|
The goal is to emulate what is produced by `ipaddress.ip_network` by allowing [`MinerNetwork`][pyasic.network.MinerNetwork] to get a list of hosts.
|
||||||
|
This allows this class to be the [`MinerNetwork.network`][pyasic.network.MinerNetwork] and hence be used for scanning.
|
||||||
|
|
||||||
|
::: pyasic.network.net_range.MinerNetworkRange
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
29
mkdocs.yml
29
mkdocs.yml
@@ -3,22 +3,45 @@ repo_url: https://github.com/UpstreamData/pyasic
|
|||||||
nav:
|
nav:
|
||||||
- Introduction: "index.md"
|
- Introduction: "index.md"
|
||||||
- Miners:
|
- Miners:
|
||||||
|
- Supported Miners: "miners/supported_types.md"
|
||||||
- Miner Factory: "miners/miner_factory.md"
|
- Miner Factory: "miners/miner_factory.md"
|
||||||
|
- Backends:
|
||||||
|
- BMMiner: "miners/backends/bmminer.md"
|
||||||
|
- BOSMiner: "miners/backends/bosminer.md"
|
||||||
|
- BTMiner: "miners/backends/btminer.md"
|
||||||
|
- CGMiner: "miners/backends/cgminer.md"
|
||||||
|
- Hiveon: "miners/backends/hiveon.md"
|
||||||
|
- Classes:
|
||||||
|
- Antminer X9: "miners/antminer/X9.md"
|
||||||
|
- Antminer X17: "miners/antminer/X17.md"
|
||||||
|
- Antminer X19: "miners/antminer/X19.md"
|
||||||
|
- Avalon 7X: "miners/avalonminer/A7X.md"
|
||||||
|
- Avalon 8X: "miners/avalonminer/A8X.md"
|
||||||
|
- Avalon 9X: "miners/avalonminer/A9X.md"
|
||||||
|
- Avalon 10X: "miners/avalonminer/A10X.md"
|
||||||
|
- Whatsminer M2X: "miners/whatsminer/M2X.md"
|
||||||
|
- Whatsminer M3X: "miners/whatsminer/M3X.md"
|
||||||
|
- Whatsminer M5X: "miners/whatsminer/M5X.md"
|
||||||
|
- Innosilicon T3X: "miners/innosilicon/T3X.md"
|
||||||
- Network:
|
- Network:
|
||||||
- Miner Network: "network/miner_network.md"
|
- Miner Network: "network/miner_network.md"
|
||||||
- Data:
|
- Miner Network Range: "network/miner_network_range.md"
|
||||||
|
- Dataclasses:
|
||||||
- Miner Data: "data/miner_data.md"
|
- Miner Data: "data/miner_data.md"
|
||||||
- Config:
|
- Error Codes: "data/error_codes.md"
|
||||||
- Miner Config: "config/miner_config.md"
|
- Miner Config: "config/miner_config.md"
|
||||||
- Advanced:
|
- Advanced:
|
||||||
- Miner APIs:
|
- Miner APIs:
|
||||||
- Base: "API/api.md"
|
- Intro: "API/api.md"
|
||||||
- BMMiner: "API/bmminer.md"
|
- BMMiner: "API/bmminer.md"
|
||||||
- BOSMiner: "API/bosminer.md"
|
- BOSMiner: "API/bosminer.md"
|
||||||
- BTMiner: "API/btminer.md"
|
- BTMiner: "API/btminer.md"
|
||||||
- CGMiner: "API/cgminer.md"
|
- CGMiner: "API/cgminer.md"
|
||||||
- Unknown: "API/unknown.md"
|
- Unknown: "API/unknown.md"
|
||||||
|
|
||||||
|
- Base Miner: "miners/base_miner.md"
|
||||||
|
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- mkdocstrings
|
- mkdocstrings
|
||||||
- search
|
- search
|
||||||
|
|||||||
715
poetry.lock
generated
715
poetry.lock
generated
@@ -1,6 +1,6 @@
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "3.6.1"
|
version = "3.6.2"
|
||||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -11,13 +11,13 @@ idna = ">=2.8"
|
|||||||
sniffio = ">=1.1"
|
sniffio = ">=1.1"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
|
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||||
test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
|
test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
|
||||||
trio = ["trio (>=0.16)"]
|
trio = ["trio (>=0.16,<0.22)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asyncssh"
|
name = "asyncssh"
|
||||||
version = "2.11.0"
|
version = "2.13.1"
|
||||||
description = "AsyncSSH: Asynchronous SSHv2 client and server library"
|
description = "AsyncSSH: Asynchronous SSHv2 client and server library"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -38,7 +38,7 @@ pywin32 = ["pywin32 (>=227)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2022.6.15"
|
version = "2022.12.7"
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -55,9 +55,36 @@ python-versions = "*"
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pycparser = "*"
|
pycparser = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfgv"
|
||||||
|
version = "3.3.1"
|
||||||
|
description = "Validate configuration and produce human readable error messages."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.3"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "37.0.4"
|
version = "39.0.1"
|
||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -67,33 +94,83 @@ python-versions = ">=3.6"
|
|||||||
cffi = ">=1.12"
|
cffi = ">=1.12"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
|
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
||||||
docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
|
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
|
||||||
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
|
pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"]
|
||||||
sdist = ["setuptools_rust (>=0.11.4)"]
|
sdist = ["setuptools-rust (>=0.11.4)"]
|
||||||
ssh = ["bcrypt (>=3.1.5)"]
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
|
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"]
|
||||||
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
tox = ["tox"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distlib"
|
||||||
|
version = "0.3.6"
|
||||||
|
description = "Distribution utilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filelock"
|
||||||
|
version = "3.9.0"
|
||||||
|
description = "A platform independent file lock."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
|
||||||
|
testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ghp-import"
|
||||||
|
version = "2.1.0"
|
||||||
|
description = "Copy your docs directly to the gh-pages branch."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
python-dateutil = ">=2.8.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["flake8", "markdown", "twine", "wheel"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "griffe"
|
||||||
|
version = "0.25.5"
|
||||||
|
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = ">=0.4"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
async = ["aiofiles (>=0.7,<1.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.12.0"
|
version = "0.14.0"
|
||||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpcore"
|
name = "httpcore"
|
||||||
version = "0.15.0"
|
version = "0.16.3"
|
||||||
description = "A minimal low-level HTTP client."
|
description = "A minimal low-level HTTP client."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
anyio = ">=3.0.0,<4.0.0"
|
anyio = ">=3.0,<5.0"
|
||||||
certifi = "*"
|
certifi = "*"
|
||||||
h11 = ">=0.11,<0.13"
|
h11 = ">=0.13,<0.15"
|
||||||
sniffio = ">=1.0.0,<2.0.0"
|
sniffio = ">=1.0.0,<2.0.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@@ -102,7 +179,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx"
|
name = "httpx"
|
||||||
version = "0.23.0"
|
version = "0.23.3"
|
||||||
description = "The next generation HTTP client."
|
description = "The next generation HTTP client."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -110,24 +187,199 @@ python-versions = ">=3.7"
|
|||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
certifi = "*"
|
certifi = "*"
|
||||||
httpcore = ">=0.15.0,<0.16.0"
|
httpcore = ">=0.15.0,<0.17.0"
|
||||||
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
|
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
|
||||||
sniffio = "*"
|
sniffio = "*"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
brotli = ["brotlicffi", "brotli"]
|
brotli = ["brotli", "brotlicffi"]
|
||||||
cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"]
|
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
|
||||||
http2 = ["h2 (>=3,<5)"]
|
http2 = ["h2 (>=3,<5)"]
|
||||||
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "identify"
|
||||||
|
version = "2.5.18"
|
||||||
|
description = "File identification library for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
license = ["ukkonen"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.3"
|
version = "3.4"
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "6.0.0"
|
||||||
|
description = "Read metadata from Python packages"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||||
|
perf = ["ipython"]
|
||||||
|
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "5.12.0"
|
||||||
|
description = "A Python utility / library to sort Python imports."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colors = ["colorama (>=0.4.3)"]
|
||||||
|
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
|
||||||
|
plugins = ["setuptools"]
|
||||||
|
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "Jinja2"
|
||||||
|
version = "3.1.2"
|
||||||
|
description = "A very fast and expressive template engine."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
i18n = ["Babel (>=2.7)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "Markdown"
|
||||||
|
version = "3.3.7"
|
||||||
|
description = "Python implementation of Markdown."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["coverage", "pyyaml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "MarkupSafe"
|
||||||
|
version = "2.1.2"
|
||||||
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mergedeep"
|
||||||
|
version = "1.3.4"
|
||||||
|
description = "A deep merge function for 🐍."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mkdocs"
|
||||||
|
version = "1.4.2"
|
||||||
|
description = "Project documentation with Markdown."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=7.0"
|
||||||
|
colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
|
||||||
|
ghp-import = ">=1.0"
|
||||||
|
importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
|
||||||
|
jinja2 = ">=2.11.1"
|
||||||
|
markdown = ">=3.2.1,<3.4"
|
||||||
|
mergedeep = ">=1.3.4"
|
||||||
|
packaging = ">=20.5"
|
||||||
|
pyyaml = ">=5.1"
|
||||||
|
pyyaml-env-tag = ">=0.1"
|
||||||
|
watchdog = ">=2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
i18n = ["babel (>=2.9.0)"]
|
||||||
|
min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mkdocs-autorefs"
|
||||||
|
version = "0.4.1"
|
||||||
|
description = "Automatically link across pages in MkDocs."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Markdown = ">=3.3"
|
||||||
|
mkdocs = ">=1.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mkdocstrings"
|
||||||
|
version = "0.20.0"
|
||||||
|
description = "Automatic documentation from sources, for MkDocs."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Jinja2 = ">=2.11.1"
|
||||||
|
Markdown = ">=3.3"
|
||||||
|
MarkupSafe = ">=1.1"
|
||||||
|
mkdocs = ">=1.2"
|
||||||
|
mkdocs-autorefs = ">=0.3.1"
|
||||||
|
mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
|
||||||
|
pymdown-extensions = ">=6.3"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
crystal = ["mkdocstrings-crystal (>=0.3.4)"]
|
||||||
|
python = ["mkdocstrings-python (>=0.5.2)"]
|
||||||
|
python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mkdocstrings-python"
|
||||||
|
version = "0.8.3"
|
||||||
|
description = "A Python handler for mkdocstrings."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
griffe = ">=0.24"
|
||||||
|
mkdocstrings = ">=0.19"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.7.0"
|
||||||
|
description = "Node.js virtual environment builder"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
setuptools = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "23.0"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "passlib"
|
name = "passlib"
|
||||||
version = "1.7.4"
|
version = "1.7.4"
|
||||||
@@ -139,9 +391,36 @@ python-versions = "*"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
argon2 = ["argon2-cffi (>=18.2.0)"]
|
argon2 = ["argon2-cffi (>=18.2.0)"]
|
||||||
bcrypt = ["bcrypt (>=3.1.0)"]
|
bcrypt = ["bcrypt (>=3.1.0)"]
|
||||||
build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"]
|
build_docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
||||||
totp = ["cryptography"]
|
totp = ["cryptography"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
|
||||||
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pre-commit"
|
||||||
|
version = "3.1.0"
|
||||||
|
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cfgv = ">=2.0.0"
|
||||||
|
identify = ">=1.0.0"
|
||||||
|
nodeenv = ">=0.11.1"
|
||||||
|
pyyaml = ">=5.1"
|
||||||
|
virtualenv = ">=20.10.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyaml"
|
name = "pyaml"
|
||||||
version = "21.10.1"
|
version = "21.10.1"
|
||||||
@@ -162,13 +441,46 @@ optional = false
|
|||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pymdown-extensions"
|
||||||
|
version = "9.9.2"
|
||||||
|
description = "Extension pack for Python Markdown."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
markdown = ">=3.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.8.2"
|
||||||
|
description = "Extensions to the standard Python datetime module"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
six = ">=1.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "PyYAML"
|
||||||
version = "6.0"
|
version = "6.0"
|
||||||
description = "YAML parser and emitter for Python"
|
description = "YAML parser and emitter for Python"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml_env_tag"
|
||||||
|
version = "0.1"
|
||||||
|
description = "A custom YAML tag for referencing environment variables in YAML files. "
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pyyaml = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfc3986"
|
name = "rfc3986"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -183,13 +495,34 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
idna2008 = ["idna"]
|
idna2008 = ["idna"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "67.4.0"
|
||||||
|
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||||
|
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||||
|
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.16.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.2.0"
|
version = "1.3.0"
|
||||||
description = "Sniff out which async library your code is running under"
|
description = "Sniff out which async library your code is running under"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
@@ -201,29 +534,69 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.3.0"
|
version = "4.5.0"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtualenv"
|
||||||
|
version = "20.19.0"
|
||||||
|
description = "Virtual Python Environment builder"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
distlib = ">=0.3.6,<1"
|
||||||
|
filelock = ">=3.4.1,<4"
|
||||||
|
platformdirs = ">=2.4,<4"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
|
||||||
|
test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "watchdog"
|
||||||
|
version = "2.2.1"
|
||||||
|
description = "Filesystem events monitoring"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
watchmedo = ["PyYAML (>=3.10)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.14.0"
|
||||||
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||||
|
testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.8"
|
||||||
content-hash = "8d93eafd928d7fed4b0a00d13e46982c2d4310c37acb2faec7e7a477b3f35e9c"
|
content-hash = "44b511a675e63d02eaf82f3ec78baa2364e5116e990f9f0a6fc2cb8f21cf616f"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
anyio = [
|
anyio = [
|
||||||
{file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"},
|
{file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
|
||||||
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
|
{file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
|
||||||
]
|
]
|
||||||
asyncssh = [
|
asyncssh = [
|
||||||
{file = "asyncssh-2.11.0-py3-none-any.whl", hash = "sha256:7302348cbd54c58d3259da17f13e77912de1b005e366b15c8b183d948c8a91a8"},
|
{file = "asyncssh-2.13.1-py3-none-any.whl", hash = "sha256:c90eb5e2b4f9a7cc6e6af01fd844563d722c0d667f8c5f51fe5b3c2a79fa0575"},
|
||||||
{file = "asyncssh-2.11.0.tar.gz", hash = "sha256:59c36ce77ba9dda8dd57ad875776e7105ddb1fa851bc039bb3aeadeac4f67b56"},
|
{file = "asyncssh-2.13.1.tar.gz", hash = "sha256:ebbb83c05c0b45cf230de1ef2f06059e360f9afa5c3ddf60fc92faf7b94ff887"},
|
||||||
]
|
]
|
||||||
certifi = [
|
certifi = [
|
||||||
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
|
{file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
|
||||||
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
|
{file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
|
||||||
]
|
]
|
||||||
cffi = [
|
cffi = [
|
||||||
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
|
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
|
||||||
@@ -291,50 +664,187 @@ cffi = [
|
|||||||
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
|
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
|
||||||
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
|
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
|
||||||
]
|
]
|
||||||
|
cfgv = [
|
||||||
|
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
|
||||||
|
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
|
||||||
|
]
|
||||||
|
click = [
|
||||||
|
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||||
|
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||||
|
]
|
||||||
|
colorama = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
cryptography = [
|
cryptography = [
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"},
|
{file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"},
|
{file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"},
|
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"},
|
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"},
|
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"},
|
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"},
|
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"},
|
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"},
|
{file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"},
|
{file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"},
|
||||||
{file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"},
|
{file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"},
|
||||||
{file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"},
|
{file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"},
|
||||||
{file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"},
|
{file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"},
|
||||||
{file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"},
|
{file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"},
|
||||||
{file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"},
|
{file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"},
|
||||||
{file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"},
|
{file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"},
|
||||||
{file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"},
|
{file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"},
|
||||||
{file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"},
|
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"},
|
||||||
{file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"},
|
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"},
|
||||||
{file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"},
|
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"},
|
||||||
{file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"},
|
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"},
|
||||||
{file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"},
|
{file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"},
|
||||||
|
{file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"},
|
||||||
|
]
|
||||||
|
distlib = [
|
||||||
|
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
|
||||||
|
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
|
||||||
|
]
|
||||||
|
filelock = [
|
||||||
|
{file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"},
|
||||||
|
{file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"},
|
||||||
|
]
|
||||||
|
ghp-import = [
|
||||||
|
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
|
||||||
|
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
|
||||||
|
]
|
||||||
|
griffe = [
|
||||||
|
{file = "griffe-0.25.5-py3-none-any.whl", hash = "sha256:1fb9edff48e66d4873014a2ebf21aca5f271d0006a4c937826e3cf592ffb3706"},
|
||||||
|
{file = "griffe-0.25.5.tar.gz", hash = "sha256:11ea3403ef0560a1cbcf7f302eb5d21cf4c1d8ed3f8a16a75aa9f6f458caf3f1"},
|
||||||
]
|
]
|
||||||
h11 = [
|
h11 = [
|
||||||
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
|
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||||
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
|
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||||
]
|
]
|
||||||
httpcore = [
|
httpcore = [
|
||||||
{file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"},
|
{file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
|
||||||
{file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"},
|
{file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
|
||||||
]
|
]
|
||||||
httpx = [
|
httpx = [
|
||||||
{file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"},
|
{file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
|
||||||
{file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"},
|
{file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
|
||||||
|
]
|
||||||
|
identify = [
|
||||||
|
{file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"},
|
||||||
|
{file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"},
|
||||||
]
|
]
|
||||||
idna = [
|
idna = [
|
||||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||||
|
]
|
||||||
|
importlib-metadata = [
|
||||||
|
{file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"},
|
||||||
|
{file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"},
|
||||||
|
]
|
||||||
|
isort = [
|
||||||
|
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
|
||||||
|
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
|
||||||
|
]
|
||||||
|
Jinja2 = [
|
||||||
|
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
|
||||||
|
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
|
||||||
|
]
|
||||||
|
Markdown = [
|
||||||
|
{file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"},
|
||||||
|
{file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"},
|
||||||
|
]
|
||||||
|
MarkupSafe = [
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
|
||||||
|
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
|
||||||
|
]
|
||||||
|
mergedeep = [
|
||||||
|
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
|
||||||
|
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
|
||||||
|
]
|
||||||
|
mkdocs = [
|
||||||
|
{file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"},
|
||||||
|
{file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"},
|
||||||
|
]
|
||||||
|
mkdocs-autorefs = [
|
||||||
|
{file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
|
||||||
|
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
|
||||||
|
]
|
||||||
|
mkdocstrings = [
|
||||||
|
{file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"},
|
||||||
|
{file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"},
|
||||||
|
]
|
||||||
|
mkdocstrings-python = [
|
||||||
|
{file = "mkdocstrings-python-0.8.3.tar.gz", hash = "sha256:9ae473f6dc599339b09eee17e4d2b05d6ac0ec29860f3fc9b7512d940fc61adf"},
|
||||||
|
{file = "mkdocstrings_python-0.8.3-py3-none-any.whl", hash = "sha256:4e6e1cd6f37a785de0946ced6eb846eb2f5d891ac1cc2c7b832943d3529087a7"},
|
||||||
|
]
|
||||||
|
nodeenv = [
|
||||||
|
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
|
||||||
|
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
|
||||||
|
]
|
||||||
|
packaging = [
|
||||||
|
{file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
|
||||||
|
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
|
||||||
]
|
]
|
||||||
passlib = [
|
passlib = [
|
||||||
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
|
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
|
||||||
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
|
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
|
||||||
]
|
]
|
||||||
|
platformdirs = [
|
||||||
|
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"},
|
||||||
|
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"},
|
||||||
|
]
|
||||||
|
pre-commit = [
|
||||||
|
{file = "pre_commit-3.1.0-py2.py3-none-any.whl", hash = "sha256:7001dfcd174540658822b1fd3630ceadf4f41375a5d1844b5c3b3830f227348c"},
|
||||||
|
{file = "pre_commit-3.1.0.tar.gz", hash = "sha256:61bd9f1b96d3d1e763f2a9a0f8522aed341646800642ff6803c73fac5781f5b7"},
|
||||||
|
]
|
||||||
pyaml = [
|
pyaml = [
|
||||||
{file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"},
|
{file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"},
|
||||||
{file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"},
|
{file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"},
|
||||||
@@ -343,7 +853,15 @@ pycparser = [
|
|||||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||||
]
|
]
|
||||||
pyyaml = [
|
pymdown-extensions = [
|
||||||
|
{file = "pymdown_extensions-9.9.2-py3-none-any.whl", hash = "sha256:c3d804eb4a42b85bafb5f36436342a5ad38df03878bb24db8855a4aa8b08b765"},
|
||||||
|
{file = "pymdown_extensions-9.9.2.tar.gz", hash = "sha256:ebb33069bafcb64d5f5988043331d4ea4929325dc678a6bcf247ddfcf96499f8"},
|
||||||
|
]
|
||||||
|
python-dateutil = [
|
||||||
|
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
|
||||||
|
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
|
||||||
|
]
|
||||||
|
PyYAML = [
|
||||||
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
||||||
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
|
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
|
||||||
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
|
||||||
@@ -351,6 +869,13 @@ pyyaml = [
|
|||||||
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
|
||||||
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
|
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
|
||||||
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
|
||||||
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
|
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
|
||||||
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
|
||||||
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
||||||
@@ -378,19 +903,69 @@ pyyaml = [
|
|||||||
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
|
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
|
||||||
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||||
]
|
]
|
||||||
|
pyyaml_env_tag = [
|
||||||
|
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
|
||||||
|
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
|
||||||
|
]
|
||||||
rfc3986 = [
|
rfc3986 = [
|
||||||
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
|
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
|
||||||
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
|
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
|
||||||
]
|
]
|
||||||
|
setuptools = [
|
||||||
|
{file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"},
|
||||||
|
{file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"},
|
||||||
|
]
|
||||||
|
six = [
|
||||||
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
|
]
|
||||||
sniffio = [
|
sniffio = [
|
||||||
{file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
|
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
|
||||||
{file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
|
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
|
||||||
]
|
]
|
||||||
toml = [
|
toml = [
|
||||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||||
]
|
]
|
||||||
typing-extensions = [
|
typing-extensions = [
|
||||||
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
{file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
|
||||||
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
{file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
|
||||||
|
]
|
||||||
|
virtualenv = [
|
||||||
|
{file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"},
|
||||||
|
{file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"},
|
||||||
|
]
|
||||||
|
watchdog = [
|
||||||
|
{file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"},
|
||||||
|
{file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5100eae58133355d3ca6c1083a33b81355c4f452afa474c2633bd2fbbba398b3"},
|
||||||
|
{file = "watchdog-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e618a4863726bc7a3c64f95c218437f3349fb9d909eb9ea3a1ed3b567417c661"},
|
||||||
|
{file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:102a60093090fc3ff76c983367b19849b7cc24ec414a43c0333680106e62aae1"},
|
||||||
|
{file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:748ca797ff59962e83cc8e4b233f87113f3cf247c23e6be58b8a2885c7337aa3"},
|
||||||
|
{file = "watchdog-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ccd8d84b9490a82b51b230740468116b8205822ea5fdc700a553d92661253a3"},
|
||||||
|
{file = "watchdog-2.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e01d699cd260d59b84da6bda019dce0a3353e3fcc774408ae767fe88ee096b7"},
|
||||||
|
{file = "watchdog-2.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8586d98c494690482c963ffb24c49bf9c8c2fe0589cec4dc2f753b78d1ec301d"},
|
||||||
|
{file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:adaf2ece15f3afa33a6b45f76b333a7da9256e1360003032524d61bdb4c422ae"},
|
||||||
|
{file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83a7cead445008e880dbde833cb9e5cc7b9a0958edb697a96b936621975f15b9"},
|
||||||
|
{file = "watchdog-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8ac23ff2c2df4471a61af6490f847633024e5aa120567e08d07af5718c9d092"},
|
||||||
|
{file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d0f29fd9f3f149a5277929de33b4f121a04cf84bb494634707cfa8ea8ae106a8"},
|
||||||
|
{file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:967636031fa4c4955f0f3f22da3c5c418aa65d50908d31b73b3b3ffd66d60640"},
|
||||||
|
{file = "watchdog-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96cbeb494e6cbe3ae6aacc430e678ce4b4dd3ae5125035f72b6eb4e5e9eb4f4e"},
|
||||||
|
{file = "watchdog-2.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61fdb8e9c57baf625e27e1420e7ca17f7d2023929cd0065eb79c83da1dfbeacd"},
|
||||||
|
{file = "watchdog-2.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cb5ecc332112017fbdb19ede78d92e29a8165c46b68a0b8ccbd0a154f196d5e"},
|
||||||
|
{file = "watchdog-2.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a480d122740debf0afac4ddd583c6c0bb519c24f817b42ed6f850e2f6f9d64a8"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:978a1aed55de0b807913b7482d09943b23a2d634040b112bdf31811a422f6344"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:8c28c23972ec9c524967895ccb1954bc6f6d4a557d36e681a36e84368660c4ce"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-manylinux2014_i686.whl", hash = "sha256:c27d8c1535fd4474e40a4b5e01f4ba6720bac58e6751c667895cbc5c8a7af33c"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d6b87477752bd86ac5392ecb9eeed92b416898c30bd40c7e2dd03c3146105646"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cece1aa596027ff56369f0b50a9de209920e1df9ac6d02c7f9e5d8162eb4f02b"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:8b5cde14e5c72b2df5d074774bdff69e9b55da77e102a91f36ef26ca35f9819c"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e038be858425c4f621900b8ff1a3a1330d9edcfeaa1c0468aeb7e330fb87693e"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-win32.whl", hash = "sha256:bc43c1b24d2f86b6e1cc15f68635a959388219426109233e606517ff7d0a5a73"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-win_amd64.whl", hash = "sha256:17f1708f7410af92ddf591e94ae71a27a13974559e72f7e9fde3ec174b26ba2e"},
|
||||||
|
{file = "watchdog-2.2.1-py3-none-win_ia64.whl", hash = "sha256:195ab1d9d611a4c1e5311cbf42273bc541e18ea8c32712f2fb703cfc6ff006f9"},
|
||||||
|
{file = "watchdog-2.2.1.tar.gz", hash = "sha256:cdcc23c9528601a8a293eb4369cbd14f6b4f34f07ae8769421252e9c22718b6f"},
|
||||||
|
]
|
||||||
|
zipp = [
|
||||||
|
{file = "zipp-3.14.0-py3-none-any.whl", hash = "sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7"},
|
||||||
|
{file = "zipp-3.14.0.tar.gz", hash = "sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb"},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,37 +1,28 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import warnings
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
import warnings
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
from pyasic.errors import APIError, APIWarning
|
||||||
class APIError(Exception):
|
|
||||||
def __init__(self, *args):
|
|
||||||
if args:
|
|
||||||
self.message = args[0]
|
|
||||||
else:
|
|
||||||
self.message = None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.message:
|
|
||||||
return f"{self.message}"
|
|
||||||
else:
|
|
||||||
return "Incorrect API parameters."
|
|
||||||
|
|
||||||
|
|
||||||
class APIWarning(Warning):
|
|
||||||
def __init__(self, *args):
|
|
||||||
if args:
|
|
||||||
self.message = args[0]
|
|
||||||
else:
|
|
||||||
self.message = None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.message:
|
|
||||||
return f"{self.message}"
|
|
||||||
else:
|
|
||||||
return "Incorrect API parameters."
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMinerAPI:
|
class BaseMinerAPI:
|
||||||
@@ -41,16 +32,106 @@ class BaseMinerAPI:
|
|||||||
# ip address of the miner
|
# ip address of the miner
|
||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ipaddress.ip_address(ip)
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls is BaseMinerAPI:
|
||||||
|
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}: {str(self.ip)}"
|
||||||
|
|
||||||
|
async def send_command(
|
||||||
|
self,
|
||||||
|
command: Union[str, bytes],
|
||||||
|
parameters: Union[str, int, bool] = None,
|
||||||
|
ignore_errors: bool = False,
|
||||||
|
allow_warning: bool = True,
|
||||||
|
) -> dict:
|
||||||
|
"""Send an API command to the miner and return the result.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
command: The command to sent to the miner.
|
||||||
|
parameters: Any additional parameters to be sent with the command.
|
||||||
|
ignore_errors: Whether to raise APIError when the command returns an error.
|
||||||
|
allow_warning: Whether to warn if the command fails.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The return data from the API command parsed from JSON into a dict.
|
||||||
|
"""
|
||||||
|
logging.debug(
|
||||||
|
f"{self} - (Send Privileged Command) - {command} "
|
||||||
|
+ f"with args {parameters}"
|
||||||
|
if parameters
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
# create the command
|
||||||
|
cmd = {"command": command}
|
||||||
|
if parameters:
|
||||||
|
cmd["parameter"] = parameters
|
||||||
|
|
||||||
|
# send the command
|
||||||
|
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
|
||||||
|
|
||||||
|
data = self._load_api_data(data)
|
||||||
|
|
||||||
|
# check for if the user wants to allow errors to return
|
||||||
|
if not ignore_errors:
|
||||||
|
# validate the command succeeded
|
||||||
|
validation = self._validate_command_output(data)
|
||||||
|
if not validation[0]:
|
||||||
|
if allow_warning:
|
||||||
|
logging.warning(
|
||||||
|
f"{self.ip}: API Command Error: {command}: {validation[1]}"
|
||||||
|
)
|
||||||
|
raise APIError(validation[1])
|
||||||
|
|
||||||
|
logging.debug(f"{self} - (Send Command) - Received data.")
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Privileged command handler, only used by whatsminers, defined here for consistency.
|
||||||
|
async def send_privileged_command(self, *args, **kwargs) -> dict:
|
||||||
|
return await self.send_command(*args, **kwargs)
|
||||||
|
|
||||||
|
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
|
||||||
|
"""Creates and sends multiple commands as one command to the miner.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
*commands: The commands to send as a multicommand to the miner.
|
||||||
|
ignore_errors: Whether to raise APIError when the command returns an error.
|
||||||
|
allow_warning: A boolean to supress APIWarnings.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# make sure we can actually run each command, otherwise they will fail
|
||||||
|
commands = self._check_commands(*commands)
|
||||||
|
# standard multicommand format is "command1+command2"
|
||||||
|
# standard format doesn't work for X19
|
||||||
|
command = "+".join(commands)
|
||||||
|
try:
|
||||||
|
data = await self.send_command(command, allow_warning=allow_warning)
|
||||||
|
except APIError:
|
||||||
|
return {command: [{}] for command in commands}
|
||||||
|
logging.debug(f"{self} - (Multicommand) - Received data")
|
||||||
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def commands(self) -> list:
|
||||||
|
return self.get_commands()
|
||||||
|
|
||||||
def get_commands(self) -> list:
|
def get_commands(self) -> list:
|
||||||
"""Get a list of command accessible to a specific type of API on the miner."""
|
"""Get a list of command accessible to a specific type of API on the miner.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of all API commands that the miner supports.
|
||||||
|
"""
|
||||||
return [
|
return [
|
||||||
func
|
func
|
||||||
for func in
|
for func in
|
||||||
# each function in self
|
# each function in self
|
||||||
dir(self)
|
dir(self)
|
||||||
|
if not func == "commands"
|
||||||
if callable(getattr(self, func)) and
|
if callable(getattr(self, func)) and
|
||||||
# no __ methods
|
# no __ or _ methods
|
||||||
not func.startswith("__") and
|
not func.startswith("__") and not func.startswith("_") and
|
||||||
# remove all functions that are in this base class
|
# remove all functions that are in this base class
|
||||||
func
|
func
|
||||||
not in [
|
not in [
|
||||||
@@ -60,102 +141,72 @@ class BaseMinerAPI:
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
async def multicommand(
|
def _check_commands(self, *commands):
|
||||||
self, *commands: str, ignore_x19_error: bool = False
|
|
||||||
) -> dict:
|
|
||||||
"""Creates and sends multiple commands as one command to the miner."""
|
|
||||||
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
|
|
||||||
# split the commands into a proper list
|
|
||||||
user_commands = [*commands]
|
|
||||||
allowed_commands = self.get_commands()
|
allowed_commands = self.get_commands()
|
||||||
# make sure we can actually run the command, otherwise it will fail
|
return_commands = []
|
||||||
commands = [command for command in user_commands if command in allowed_commands]
|
for command in [*commands]:
|
||||||
for item in list(set(user_commands) - set(commands)):
|
if command in allowed_commands:
|
||||||
warnings.warn(
|
return_commands.append(command)
|
||||||
f"""Removing incorrect command: {item}
|
else:
|
||||||
If you are sure you want to use this command please use API.send_command("{item}", ignore_errors=True) instead.""",
|
warnings.warn(
|
||||||
APIWarning,
|
f"""Removing incorrect command: {command}
|
||||||
)
|
If you are sure you want to use this command please use API.send_command("{command}", ignore_errors=True) instead.""",
|
||||||
# standard multicommand format is "command1+command2"
|
APIWarning,
|
||||||
# doesnt work for S19 which is dealt with in the send command function
|
)
|
||||||
command = "+".join(commands)
|
return return_commands
|
||||||
data = None
|
|
||||||
try:
|
|
||||||
data = await self.send_command(command, x19_command=ignore_x19_error)
|
|
||||||
except APIError:
|
|
||||||
try:
|
|
||||||
data = {}
|
|
||||||
# S19 handler, try again
|
|
||||||
for cmd in command.split("+"):
|
|
||||||
data[cmd] = []
|
|
||||||
data[cmd].append(await self.send_command(cmd))
|
|
||||||
except APIError as e:
|
|
||||||
raise APIError(e)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
|
|
||||||
if data:
|
|
||||||
logging.debug(f"{self.ip}: Received multicommand data.")
|
|
||||||
return data
|
|
||||||
|
|
||||||
async def send_command(
|
async def _send_bytes(self, data: bytes, timeout: int = 100) -> bytes:
|
||||||
self,
|
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
|
||||||
command: Union[str, bytes],
|
|
||||||
parameters: Union[str, int, bool] = None,
|
|
||||||
ignore_errors: bool = False,
|
|
||||||
x19_command: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
"""Send an API command to the miner and return the result."""
|
|
||||||
try:
|
try:
|
||||||
# get reader and writer streams
|
# get reader and writer streams
|
||||||
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
||||||
# handle OSError 121
|
# handle OSError 121
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.winerror == "121":
|
if e.errno == 121:
|
||||||
logging.warning("Semaphore Timeout has Expired.")
|
logging.warning(
|
||||||
return {}
|
f"{self} - ([Hidden] Send Bytes) - Semaphore timeout expired."
|
||||||
|
)
|
||||||
# create the command
|
return b"{}"
|
||||||
cmd = {"command": command}
|
|
||||||
if parameters is not None:
|
|
||||||
cmd["parameter"] = parameters
|
|
||||||
|
|
||||||
# send the command
|
# send the command
|
||||||
writer.write(json.dumps(cmd).encode("utf-8"))
|
logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing")
|
||||||
|
writer.write(data)
|
||||||
|
logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
ret_data = await asyncio.wait_for(reader.read(4096), timeout=timeout)
|
||||||
# instantiate data
|
try:
|
||||||
data = b""
|
# Fix for stupid whatsminer bug, reboot/restart seem to not load properly in the loop
|
||||||
|
# have to receive, save the data, check if there is more data by reading with a short timeout
|
||||||
|
# append that data if there is more, and then onto the main loop.
|
||||||
|
ret_data += await asyncio.wait_for(reader.read(1), timeout=1)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return ret_data
|
||||||
|
|
||||||
# loop to receive all the data
|
# loop to receive all the data
|
||||||
|
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
d = await reader.read(4096)
|
try:
|
||||||
if not d:
|
d = await asyncio.wait_for(reader.read(4096), timeout=timeout)
|
||||||
break
|
if not d:
|
||||||
data += d
|
break
|
||||||
|
ret_data += d
|
||||||
|
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
|
||||||
|
raise e
|
||||||
|
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"{self.ip}: API Command Error: {e}")
|
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
|
||||||
|
|
||||||
data = self.load_api_data(data)
|
|
||||||
|
|
||||||
# close the connection
|
# close the connection
|
||||||
|
logging.debug(f"{self} - ([Hidden] Send Bytes) - Closing")
|
||||||
writer.close()
|
writer.close()
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
|
|
||||||
# check for if the user wants to allow errors to return
|
return ret_data
|
||||||
if not ignore_errors:
|
|
||||||
# validate the command succeeded
|
|
||||||
validation = self.validate_command_output(data)
|
|
||||||
if not validation[0]:
|
|
||||||
if not x19_command:
|
|
||||||
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
|
|
||||||
raise APIError(validation[1])
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_command_output(data: dict) -> tuple:
|
def _validate_command_output(data: dict) -> tuple:
|
||||||
"""Check if the returned command output is correctly formatted."""
|
|
||||||
# check if the data returned is correct or an error
|
# check if the data returned is correct or an error
|
||||||
# if status isn't a key, it is a multicommand
|
# if status isn't a key, it is a multicommand
|
||||||
if "STATUS" not in data.keys():
|
if "STATUS" not in data.keys():
|
||||||
@@ -182,34 +233,44 @@ If you are sure you want to use this command please use API.send_command("{item}
|
|||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_api_data(data: bytes) -> dict:
|
def _load_api_data(data: bytes) -> dict:
|
||||||
"""Convert API data from JSON to dict"""
|
# some json from the API returns with a null byte (\x00) on the end
|
||||||
str_data = None
|
if data.endswith(b"\x00"):
|
||||||
|
# handle the null byte
|
||||||
|
str_data = data.decode("utf-8")[:-1]
|
||||||
|
else:
|
||||||
|
# no null byte
|
||||||
|
str_data = data.decode("utf-8")
|
||||||
|
# fix an error with a btminer return having an extra comma that breaks json.loads()
|
||||||
|
str_data = str_data.replace(",}", "}")
|
||||||
|
# fix an error with a btminer return having a newline that breaks json.loads()
|
||||||
|
str_data = str_data.replace("\n", "")
|
||||||
|
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
|
||||||
|
str_data = str_data.replace("}{", "},{")
|
||||||
|
# fix an error with a bmminer return having a specific comma that breaks json.loads()
|
||||||
|
str_data = str_data.replace("[,{", "[{")
|
||||||
|
# fix an error with a btminer return having a missing comma. (2023-01-06 version)
|
||||||
|
str_data = str_data.replace('""temp0', '","temp0')
|
||||||
|
# fix an error with Avalonminers returning inf and nan
|
||||||
|
str_data = str_data.replace("info", "1nfo")
|
||||||
|
str_data = str_data.replace("inf", "0")
|
||||||
|
str_data = str_data.replace("1nfo", "info")
|
||||||
|
str_data = str_data.replace("nan", "0")
|
||||||
|
# fix whatever this garbage from avalonminers is `,"id":1}`
|
||||||
|
if str_data.startswith(","):
|
||||||
|
str_data = f"{{{str_data[1:]}"
|
||||||
|
# try to fix an error with overflowing the receive buffer
|
||||||
|
# this can happen in cases such as bugged btminers returning arbitrary length error info with 100s of errors.
|
||||||
|
if not str_data.endswith("}"):
|
||||||
|
str_data = ",".join(str_data.split(",")[:-1]) + "}"
|
||||||
|
|
||||||
|
# fix a really nasty bug with whatsminer API v2.0.4 where they return a list structured like a dict
|
||||||
|
if re.search(r"\"error_code\":\[\".+\"\]", str_data):
|
||||||
|
str_data = str_data.replace("[", "{").replace("]", "}")
|
||||||
|
|
||||||
|
# parse the json
|
||||||
try:
|
try:
|
||||||
# some json from the API returns with a null byte (\x00) on the end
|
|
||||||
if data.endswith(b"\x00"):
|
|
||||||
# handle the null byte
|
|
||||||
str_data = data.decode("utf-8")[:-1]
|
|
||||||
else:
|
|
||||||
# no null byte
|
|
||||||
str_data = data.decode("utf-8")
|
|
||||||
# fix an error with a btminer return having an extra comma that breaks json.loads()
|
|
||||||
str_data = str_data.replace(",}", "}")
|
|
||||||
# fix an error with a btminer return having a newline that breaks json.loads()
|
|
||||||
str_data = str_data.replace("\n", "")
|
|
||||||
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
|
|
||||||
str_data = str_data.replace("}{", "},{")
|
|
||||||
# fix an error with a bmminer return having a specific comma that breaks json.loads()
|
|
||||||
str_data = str_data.replace("[,{", "[{")
|
|
||||||
# fix an error with Avalonminers returning inf and nan
|
|
||||||
str_data = str_data.replace("inf", "0")
|
|
||||||
str_data = str_data.replace("nan", "0")
|
|
||||||
# fix whatever this garbage from avalonminers is `,"id":1}`
|
|
||||||
if str_data.startswith(","):
|
|
||||||
str_data = f"{{{str_data[1:]}"
|
|
||||||
# parse the json
|
|
||||||
parsed_data = json.loads(str_data)
|
parsed_data = json.loads(str_data)
|
||||||
# handle bad json
|
|
||||||
except json.decoder.JSONDecodeError as e:
|
except json.decoder.JSONDecodeError as e:
|
||||||
raise APIError(f"Decode Error {e}: {str_data}")
|
raise APIError(f"Decode Error {e}: {str_data}")
|
||||||
return parsed_data
|
return parsed_data
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
from pyasic.API import BaseMinerAPI
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyasic.API import APIError, BaseMinerAPI
|
||||||
|
|
||||||
|
|
||||||
class BMMinerAPI(BaseMinerAPI):
|
class BMMinerAPI(BaseMinerAPI):
|
||||||
@@ -19,8 +36,42 @@ class BMMinerAPI(BaseMinerAPI):
|
|||||||
port: The port to reference the API on. Default is 4028.
|
port: The port to reference the API on. Default is 4028.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ip: str, port: int = 4028) -> None:
|
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028) -> None:
|
||||||
super().__init__(ip, port)
|
super().__init__(ip, port=port)
|
||||||
|
self.api_ver = api_ver
|
||||||
|
|
||||||
|
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
|
||||||
|
# make sure we can actually run each command, otherwise they will fail
|
||||||
|
commands = self._check_commands(*commands)
|
||||||
|
# standard multicommand format is "command1+command2"
|
||||||
|
# doesn't work for S19 which uses the backup _x19_multicommand
|
||||||
|
command = "+".join(commands)
|
||||||
|
try:
|
||||||
|
data = await self.send_command(command, allow_warning=allow_warning)
|
||||||
|
except APIError:
|
||||||
|
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
|
||||||
|
data = await self._x19_multicommand(
|
||||||
|
*command.split("+"), allow_warning=allow_warning
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def _x19_multicommand(self, *commands, allow_warning: bool = True):
|
||||||
|
data = None
|
||||||
|
try:
|
||||||
|
data = {}
|
||||||
|
# send all commands individually
|
||||||
|
for cmd in commands:
|
||||||
|
data[cmd] = []
|
||||||
|
data[cmd].append(
|
||||||
|
await self.send_command(cmd, allow_warning=allow_warning)
|
||||||
|
)
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(
|
||||||
|
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
async def version(self) -> dict:
|
async def version(self) -> dict:
|
||||||
"""Get miner version info.
|
"""Get miner version info.
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from pyasic.API import BaseMinerAPI
|
from pyasic.API import BaseMinerAPI
|
||||||
|
|
||||||
|
|
||||||
@@ -19,8 +35,9 @@ class BOSMinerAPI(BaseMinerAPI):
|
|||||||
port: The port to reference the API on. Default is 4028.
|
port: The port to reference the API on. Default is 4028.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ip: str, port: int = 4028):
|
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028) -> None:
|
||||||
super().__init__(ip, port)
|
super().__init__(ip, port=port)
|
||||||
|
self.api_ver = api_ver
|
||||||
|
|
||||||
async def asccount(self) -> dict:
|
async def asccount(self) -> dict:
|
||||||
"""Get data on the number of ASC devices and their info.
|
"""Get data on the number of ASC devices and their info.
|
||||||
@@ -152,7 +169,7 @@ class BOSMinerAPI(BaseMinerAPI):
|
|||||||
return await self.send_command("estats")
|
return await self.send_command("estats")
|
||||||
|
|
||||||
async def check(self, command: str) -> dict:
|
async def check(self, command: str) -> dict:
|
||||||
"""Check if the command command exists in BOSMiner.
|
"""Check if the command `command` exists in BOSMiner.
|
||||||
<details>
|
<details>
|
||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,43 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import hashlib
|
|
||||||
import binascii
|
|
||||||
import base64
|
import base64
|
||||||
|
import binascii
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from passlib.handlers.md5_crypt import md5_crypt
|
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
from passlib.handlers.md5_crypt import md5_crypt
|
||||||
|
|
||||||
from pyasic.API import BaseMinerAPI, APIError
|
from pyasic.API import BaseMinerAPI
|
||||||
from pyasic.settings import WHATSMINER_PWD
|
from pyasic.errors import APIError
|
||||||
|
from pyasic.misc import api_min_version
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
### IMPORTANT ###
|
### IMPORTANT ###
|
||||||
# you need to change the password of the miners using the Whatsminer
|
# you need to change the password of the miners using the Whatsminer
|
||||||
# tool, then you can set them back to admin with this tool, but they
|
# tool, then you can set them back to admin with this tool, but they
|
||||||
# must be changed to something else and set back to admin with this
|
# must be changed to something else and set back to admin with this
|
||||||
# or the privileged API will not work using admin as the password. If
|
# or the privileged API will not work using admin as the password. If
|
||||||
# you change the password, you can pass that to the this class as pwd,
|
# you change the password, you can pass that to this class as pwd,
|
||||||
# or add it as the Whatsminer_pwd in the settings.toml file.
|
# or add it as the Whatsminer_pwd in the settings.toml file.
|
||||||
|
|
||||||
|
|
||||||
@@ -67,7 +86,7 @@ def _add_to_16(string: str) -> bytes:
|
|||||||
def parse_btminer_priviledge_data(token_data: dict, data: dict):
|
def parse_btminer_priviledge_data(token_data: dict, data: dict):
|
||||||
"""Parses data returned from the BTMiner privileged API.
|
"""Parses data returned from the BTMiner privileged API.
|
||||||
|
|
||||||
Parses data from the BTMiner privileged API using the the token
|
Parses data from the BTMiner privileged API using the token
|
||||||
from the API in an AES format.
|
from the API in an AES format.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
@@ -109,6 +128,7 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
|
|||||||
Returns:
|
Returns:
|
||||||
The encrypted privileged command to be sent to the miner.
|
The encrypted privileged command to be sent to the miner.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"(Create Prilileged Command) - Creating Privileged Command")
|
||||||
# add token to command
|
# add token to command
|
||||||
command["token"] = token_data["host_sign"]
|
command["token"] = token_data["host_sign"]
|
||||||
# encode host_passwd data and get hexdigest
|
# encode host_passwd data and get hexdigest
|
||||||
@@ -161,66 +181,90 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
pwd: The admin password of the miner. Default is admin.
|
pwd: The admin password of the miner. Default is admin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ip: str, port: int = 4028, pwd: str = WHATSMINER_PWD):
|
def __init__(
|
||||||
super().__init__(ip, port)
|
|
||||||
self.admin_pwd = pwd
|
|
||||||
self.current_token = None
|
|
||||||
|
|
||||||
async def send_command(
|
|
||||||
self,
|
self,
|
||||||
command: str or bytes,
|
ip: str,
|
||||||
parameters: str or int or bool = None,
|
api_ver: str = "0.0.0",
|
||||||
|
port: int = 4028,
|
||||||
|
pwd: str = PyasicSettings().global_whatsminer_password,
|
||||||
|
):
|
||||||
|
super().__init__(ip, port)
|
||||||
|
self.pwd = pwd
|
||||||
|
self.current_token = None
|
||||||
|
self.api_ver = api_ver
|
||||||
|
|
||||||
|
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
|
||||||
|
"""Creates and sends multiple commands as one command to the miner.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
*commands: The commands to send as a multicommand to the miner.
|
||||||
|
allow_warning: A boolean to supress APIWarnings.
|
||||||
|
"""
|
||||||
|
# make sure we can actually run each command, otherwise they will fail
|
||||||
|
commands = self._check_commands(*commands)
|
||||||
|
# standard multicommand format is "command1+command2"
|
||||||
|
# commands starting with "get_" aren't supported, but we can fake that
|
||||||
|
get_commands_data = {}
|
||||||
|
for command in list(commands):
|
||||||
|
if command.startswith("get_"):
|
||||||
|
commands.remove(command)
|
||||||
|
# send seperately and append later
|
||||||
|
try:
|
||||||
|
get_commands_data[command] = [
|
||||||
|
await self.send_command(command, allow_warning=allow_warning)
|
||||||
|
]
|
||||||
|
except APIError:
|
||||||
|
get_commands_data[command] = [{}]
|
||||||
|
|
||||||
|
command = "+".join(commands)
|
||||||
|
try:
|
||||||
|
main_data = await self.send_command(command, allow_warning=allow_warning)
|
||||||
|
except APIError:
|
||||||
|
main_data = {command: [{}] for command in commands}
|
||||||
|
logging.debug(f"{self} - (Multicommand) - Received data")
|
||||||
|
|
||||||
|
data = dict(**main_data, **get_commands_data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def send_privileged_command(
|
||||||
|
self,
|
||||||
|
command: Union[str, bytes],
|
||||||
ignore_errors: bool = False,
|
ignore_errors: bool = False,
|
||||||
|
timeout: int = 10,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
# check if command is a string
|
logging.debug(
|
||||||
# if its bytes its encoded and needs to be sent raw
|
f"{self} - (Send Privileged Command) - {command} " + f"with args {kwargs}"
|
||||||
if isinstance(command, str):
|
if len(kwargs) > 0
|
||||||
# if it is a string, put it into the standard command format
|
else ""
|
||||||
command = json.dumps({"command": command}).encode("utf-8")
|
)
|
||||||
|
command = {"cmd": command, **kwargs}
|
||||||
|
|
||||||
|
token_data = await self.get_token()
|
||||||
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
|
|
||||||
|
logging.debug(f"{self} - (Send Privileged Command) - Sending")
|
||||||
try:
|
try:
|
||||||
# get reader and writer streams
|
data = await self._send_bytes(enc_command, timeout)
|
||||||
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
|
||||||
# handle OSError 121
|
if ignore_errors:
|
||||||
except OSError as e:
|
return {}
|
||||||
if e.winerror == "121":
|
raise APIError("No data was returned from the API.")
|
||||||
print("Semaphore Timeout has Expired.")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# send the command
|
if not data:
|
||||||
writer.write(command)
|
if ignore_errors:
|
||||||
await writer.drain()
|
return {}
|
||||||
|
raise APIError("No data was returned from the API.")
|
||||||
|
data = self._load_api_data(data)
|
||||||
|
|
||||||
# instantiate data
|
|
||||||
data = b""
|
|
||||||
|
|
||||||
# loop to receive all the data
|
|
||||||
try:
|
try:
|
||||||
while True:
|
data = parse_btminer_priviledge_data(self.current_token, data)
|
||||||
d = await reader.read(4096)
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
data += d
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.info(f"{str(self.ip)}: {e}")
|
logging.info(f"{str(self.ip)}: {e}")
|
||||||
|
|
||||||
data = self.load_api_data(data)
|
|
||||||
|
|
||||||
# close the connection
|
|
||||||
writer.close()
|
|
||||||
await writer.wait_closed()
|
|
||||||
|
|
||||||
# check if the returned data is encoded
|
|
||||||
if "enc" in data.keys():
|
|
||||||
# try to parse the encoded data
|
|
||||||
try:
|
|
||||||
data = parse_btminer_priviledge_data(self.current_token, data)
|
|
||||||
except Exception as e:
|
|
||||||
logging.info(f"{str(self.ip)}: {e}")
|
|
||||||
|
|
||||||
if not ignore_errors:
|
if not ignore_errors:
|
||||||
# if it fails to validate, it is likely an error
|
# if it fails to validate, it is likely an error
|
||||||
validation = self.validate_command_output(data)
|
validation = self._validate_command_output(data)
|
||||||
if not validation[0]:
|
if not validation[0]:
|
||||||
raise APIError(validation[1])
|
raise APIError(validation[1])
|
||||||
|
|
||||||
@@ -236,11 +280,18 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
An encoded token and md5 password, which are used for the privileged API.
|
An encoded token and md5 password, which are used for the privileged API.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"{self} - (Get Token) - Getting token")
|
||||||
|
if self.current_token:
|
||||||
|
if self.current_token[
|
||||||
|
"timestamp"
|
||||||
|
] > datetime.datetime.now() - datetime.timedelta(minutes=30):
|
||||||
|
return self.current_token
|
||||||
|
|
||||||
# get the token
|
# get the token
|
||||||
data = await self.send_command("get_token")
|
data = await self.send_command("get_token")
|
||||||
|
|
||||||
# encrypt the admin password with the salt
|
# encrypt the admin password with the salt
|
||||||
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + "$")
|
pwd = _crypt(self.pwd, "$1$" + data["Msg"]["salt"] + "$")
|
||||||
pwd = pwd.split("$")
|
pwd = pwd.split("$")
|
||||||
|
|
||||||
# take the 4th item from the pwd split
|
# take the 4th item from the pwd split
|
||||||
@@ -257,7 +308,11 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
self.current_token = {
|
self.current_token = {
|
||||||
"host_sign": host_sign,
|
"host_sign": host_sign,
|
||||||
"host_passwd_md5": host_passwd_md5,
|
"host_passwd_md5": host_passwd_md5,
|
||||||
|
"timestamp": datetime.datetime.now(),
|
||||||
}
|
}
|
||||||
|
logging.debug(
|
||||||
|
f"{self} - (Get Token) - Gathered token data: {self.current_token}"
|
||||||
|
)
|
||||||
return self.current_token
|
return self.current_token
|
||||||
|
|
||||||
#### PRIVILEGED COMMANDS ####
|
#### PRIVILEGED COMMANDS ####
|
||||||
@@ -299,46 +354,18 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
A dict from the API to confirm the pools were updated.
|
A dict from the API to confirm the pools were updated.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
# get the token and password from the miner
|
return await self.send_privileged_command(
|
||||||
token_data = await self.get_token()
|
"update_pools",
|
||||||
|
pool1=pool_1,
|
||||||
# parse pool data
|
worker1=worker_1,
|
||||||
if not pool_1:
|
passwd1=passwd_1,
|
||||||
raise APIError("No pools set.")
|
pool2=pool_2,
|
||||||
elif pool_2 and pool_3:
|
worker2=worker_2,
|
||||||
command = {
|
passwd2=passwd_2,
|
||||||
"cmd": "update_pools",
|
pool3=pool_3,
|
||||||
"pool1": pool_1,
|
worker3=worker_3,
|
||||||
"worker1": worker_1,
|
passwd3=passwd_3,
|
||||||
"passwd1": passwd_1,
|
)
|
||||||
"pool2": pool_2,
|
|
||||||
"worker2": worker_2,
|
|
||||||
"passwd2": passwd_2,
|
|
||||||
"pool3": pool_3,
|
|
||||||
"worker3": worker_3,
|
|
||||||
"passwd3": passwd_3,
|
|
||||||
}
|
|
||||||
elif pool_2:
|
|
||||||
command = {
|
|
||||||
"cmd": "update_pools",
|
|
||||||
"pool1": pool_1,
|
|
||||||
"worker1": worker_1,
|
|
||||||
"passwd1": passwd_1,
|
|
||||||
"pool2": pool_2,
|
|
||||||
"worker2": worker_2,
|
|
||||||
"passwd2": passwd_2,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
command = {
|
|
||||||
"cmd": "update_pools",
|
|
||||||
"pool1": pool_1,
|
|
||||||
"worker1": worker_1,
|
|
||||||
"passwd1": passwd_1,
|
|
||||||
}
|
|
||||||
# encode the command with the token data
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
# send the command
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def restart(self) -> dict:
|
async def restart(self) -> dict:
|
||||||
"""Restart BTMiner using the API.
|
"""Restart BTMiner using the API.
|
||||||
@@ -352,10 +379,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
A reply informing of the restart.
|
A reply informing of the restart.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "restart_btminer"}
|
return await self.send_privileged_command("restart_btminer")
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def power_off(self, respbefore: bool = True) -> dict:
|
async def power_off(self, respbefore: bool = True) -> dict:
|
||||||
"""Power off the miner using the API.
|
"""Power off the miner using the API.
|
||||||
@@ -372,12 +396,8 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
if respbefore:
|
if respbefore:
|
||||||
command = {"cmd": "power_off", "respbefore": "true"}
|
return await self.send_privileged_command("power_off", respbefore="true")
|
||||||
else:
|
return await self.send_privileged_command("power_off", respbefore="false")
|
||||||
command = {"cmd": "power_off", "respbefore": "false"}
|
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def power_on(self) -> dict:
|
async def power_on(self) -> dict:
|
||||||
"""Power on the miner using the API.
|
"""Power on the miner using the API.
|
||||||
@@ -388,14 +408,10 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
the password of the miner using the Whatsminer tool.
|
the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of powering on.
|
A reply informing of the status of powering on.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "power_on"}
|
return await self.send_privileged_command("power_on")
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def reset_led(self) -> dict:
|
async def reset_led(self) -> dict:
|
||||||
"""Reset the LED on the miner using the API.
|
"""Reset the LED on the miner using the API.
|
||||||
@@ -406,20 +422,17 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
changing the password of the miner using the Whatsminer tool.
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of resetting the LED.
|
A reply informing of the status of resetting the LED.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "set_led", "param": "auto"}
|
return await self.set_led(auto=True)
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def set_led(
|
async def set_led(
|
||||||
self,
|
self,
|
||||||
|
auto: bool = True,
|
||||||
color: str = "red",
|
color: str = "red",
|
||||||
period: int = 2000,
|
period: int = 60,
|
||||||
duration: int = 1000,
|
duration: int = 20,
|
||||||
start: int = 0,
|
start: int = 0,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Set the LED on the miner using the API.
|
"""Set the LED on the miner using the API.
|
||||||
@@ -430,25 +443,20 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
changing the password of the miner using the Whatsminer tool.
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
auto: Whether or not to reset the LED to auto mode.
|
||||||
color: The LED color to set, either 'red' or 'green'.
|
color: The LED color to set, either 'red' or 'green'.
|
||||||
period: The flash cycle in ms.
|
period: The flash cycle in ms.
|
||||||
duration: LED on time in the cycle in ms.
|
duration: LED on time in the cycle in ms.
|
||||||
start: LED on time offset in the cycle in ms.
|
start: LED on time offset in the cycle in ms.
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of setting the LED.
|
A reply informing of the status of setting the LED.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {
|
if auto:
|
||||||
"cmd": "set_led",
|
return await self.send_privileged_command("set_led", param="auto")
|
||||||
"color": color,
|
return await self.send_privileged_command(
|
||||||
"period": period,
|
"set_led", color=color, period=period, duration=duration, start=start
|
||||||
"duration": duration,
|
)
|
||||||
"start": start,
|
|
||||||
}
|
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def set_low_power(self) -> dict:
|
async def set_low_power(self) -> dict:
|
||||||
"""Set low power mode on the miner using the API.
|
"""Set low power mode on the miner using the API.
|
||||||
@@ -459,14 +467,10 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
changing the password of the miner using the Whatsminer tool.
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of setting low power mode.
|
A reply informing of the status of setting low power mode.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "set_low_power"}
|
return await self.send_privileged_command("set_low_power")
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def update_firmware(self): # noqa - static
|
async def update_firmware(self): # noqa - static
|
||||||
"""Not implemented."""
|
"""Not implemented."""
|
||||||
@@ -474,20 +478,23 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
# requires a file stream in bytes
|
# requires a file stream in bytes
|
||||||
return NotImplementedError
|
return NotImplementedError
|
||||||
|
|
||||||
async def reboot(self) -> dict:
|
async def reboot(self, timeout: int = 10) -> dict:
|
||||||
"""Reboot the miner using the API.
|
"""Reboot the miner using the API.
|
||||||
<details>
|
<details>
|
||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of the reboot.
|
A reply informing of the status of the reboot.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "reboot"}
|
try:
|
||||||
token_data = await self.get_token()
|
d = await asyncio.wait_for(
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
self.send_privileged_command("reboot"), timeout=timeout
|
||||||
return await self.send_command(enc_command)
|
)
|
||||||
|
except (asyncio.CancelledError, asyncio.TimeoutError):
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
return d
|
||||||
|
|
||||||
async def factory_reset(self) -> dict:
|
async def factory_reset(self) -> dict:
|
||||||
"""Reset the miner to factory defaults.
|
"""Reset the miner to factory defaults.
|
||||||
@@ -495,14 +502,10 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of the reset.
|
A reply informing of the status of the reset.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "factory_reset"}
|
return await self.send_privileged_command("factory_reset")
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def update_pwd(self, old_pwd: str, new_pwd: str) -> dict:
|
async def update_pwd(self, old_pwd: str, new_pwd: str) -> dict:
|
||||||
"""Update the admin user's password.
|
"""Update the admin user's password.
|
||||||
@@ -519,19 +522,40 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
old_pwd: The old admin password.
|
old_pwd: The old admin password.
|
||||||
new_pwd: The new password to set.
|
new_pwd: The new password to set.
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of setting the password.
|
A reply informing of the status of setting the password.
|
||||||
"""
|
"""
|
||||||
|
self.pwd = old_pwd
|
||||||
# check if password length is greater than 8 bytes
|
# check if password length is greater than 8 bytes
|
||||||
if len(new_pwd.encode("utf-8")) > 8:
|
if len(new_pwd.encode("utf-8")) > 8:
|
||||||
raise APIError(
|
raise APIError(
|
||||||
f"New password too long, the max length is 8. "
|
f"New password too long, the max length is 8. "
|
||||||
f"Password size: {len(new_pwd.encode('utf-8'))}"
|
f"Password size: {len(new_pwd.encode('utf-8'))}"
|
||||||
)
|
)
|
||||||
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
|
try:
|
||||||
token_data = await self.get_token()
|
data = await self.send_privileged_command(
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
"update_pwd", old=old_pwd, new=new_pwd
|
||||||
return await self.send_command(enc_command)
|
)
|
||||||
|
except APIError as e:
|
||||||
|
raise e
|
||||||
|
self.pwd = new_pwd
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def net_config(
|
||||||
|
self,
|
||||||
|
ip: str = None,
|
||||||
|
mask: str = None,
|
||||||
|
gate: str = None,
|
||||||
|
dns: str = None,
|
||||||
|
host: str = None,
|
||||||
|
dhcp: bool = True,
|
||||||
|
):
|
||||||
|
if dhcp:
|
||||||
|
return await self.send_privileged_command("net_config", param="dhcp")
|
||||||
|
if None in [ip, mask, gate, dns, host]:
|
||||||
|
raise APIError("Incorrect parameters.")
|
||||||
|
return await self.send_privileged_command(
|
||||||
|
"net_config", ip=ip, mask=mask, gate=gate, dns=dns, host=host
|
||||||
|
)
|
||||||
|
|
||||||
async def set_target_freq(self, percent: int) -> dict:
|
async def set_target_freq(self, percent: int) -> dict:
|
||||||
"""Update the target frequency.
|
"""Update the target frequency.
|
||||||
@@ -546,7 +570,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
Parameters:
|
Parameters:
|
||||||
percent: The frequency % to set.
|
percent: The frequency % to set.
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of setting the frequency.
|
A reply informing of the status of setting the frequency.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -556,10 +579,9 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
f"range. Please set a % between -10 and "
|
f"range. Please set a % between -10 and "
|
||||||
f"100"
|
f"100"
|
||||||
)
|
)
|
||||||
command = {"cmd": "set_target_freq", "percent": str(percent)}
|
return await self.send_privileged_command(
|
||||||
token_data = await self.get_token()
|
"set_target_freq", percent=str(percent)
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
)
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def enable_fast_boot(self) -> dict:
|
async def enable_fast_boot(self) -> dict:
|
||||||
"""Turn on fast boot.
|
"""Turn on fast boot.
|
||||||
@@ -571,14 +593,10 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
the miner using the Whatsminer tool.
|
the miner using the Whatsminer tool.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of enabling fast boot.
|
A reply informing of the status of enabling fast boot.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "enable_btminer_fast_boot"}
|
return await self.send_privileged_command("enable_btminer_fast_boot")
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def disable_fast_boot(self) -> dict:
|
async def disable_fast_boot(self) -> dict:
|
||||||
"""Turn off fast boot.
|
"""Turn off fast boot.
|
||||||
@@ -590,14 +608,10 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
the miner using the Whatsminer tool.
|
the miner using the Whatsminer tool.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of disabling fast boot.
|
A reply informing of the status of disabling fast boot.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "disable_btminer_fast_boot"}
|
return await self.send_privileged_command("disable_btminer_fast_boot")
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def enable_web_pools(self) -> dict:
|
async def enable_web_pools(self) -> dict:
|
||||||
"""Turn on web pool updates.
|
"""Turn on web pool updates.
|
||||||
@@ -609,14 +623,10 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
password of the miner using the Whatsminer tool.
|
password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of enabling web pools.
|
A reply informing of the status of enabling web pools.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "enable_web_pools"}
|
return await self.send_privileged_command("enable_web_pools")
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def disable_web_pools(self) -> dict:
|
async def disable_web_pools(self) -> dict:
|
||||||
"""Turn off web pool updates.
|
"""Turn off web pool updates.
|
||||||
@@ -628,14 +638,10 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
password of the miner using the Whatsminer tool.
|
password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of disabling web pools.
|
A reply informing of the status of disabling web pools.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "disable_web_pools"}
|
return await self.send_privileged_command("disable_web_pools")
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def set_hostname(self, hostname: str) -> dict:
|
async def set_hostname(self, hostname: str) -> dict:
|
||||||
"""Set the hostname of the miner.
|
"""Set the hostname of the miner.
|
||||||
@@ -649,17 +655,13 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
Parameters:
|
Parameters:
|
||||||
hostname: The new hostname to use.
|
hostname: The new hostname to use.
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of setting the hostname.
|
A reply informing of the status of setting the hostname.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {"cmd": "set_hostname", "hostname": hostname}
|
return await self.send_privileged_command("set_hostname", hostname=hostname)
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def set_power_pct(self, percent: int) -> dict:
|
async def set_power_pct(self, percent: int) -> dict:
|
||||||
"""Set the power percentage of the miner.
|
"""Set the power percentage of the miner based on current power. Used for temporary adjustment.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
@@ -670,7 +672,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
Parameters:
|
Parameters:
|
||||||
percent: The power percentage to set.
|
percent: The power percentage to set.
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of setting the power percentage.
|
A reply informing of the status of setting the power percentage.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -681,10 +682,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
f"range. Please set a % between 0 and "
|
f"range. Please set a % between 0 and "
|
||||||
f"100"
|
f"100"
|
||||||
)
|
)
|
||||||
command = {"cmd": "set_power_pct", "percent": str(percent)}
|
return await self.send_privileged_command("set_power_pct", percent=str(percent))
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def pre_power_on(self, complete: bool, msg: str) -> dict:
|
async def pre_power_on(self, complete: bool, msg: str) -> dict:
|
||||||
"""Configure or check status of pre power on.
|
"""Configure or check status of pre power on.
|
||||||
@@ -702,7 +700,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
* `adjust complete`
|
* `adjust complete`
|
||||||
* `adjust continue`
|
* `adjust continue`
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
A reply informing of the status of pre power on.
|
A reply informing of the status of pre power on.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -715,13 +712,133 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
'"adjust continue"]'
|
'"adjust continue"]'
|
||||||
)
|
)
|
||||||
if complete:
|
if complete:
|
||||||
complete = "true"
|
return await self.send_privileged_command(
|
||||||
else:
|
"pre_power_on", complete="true", msg=msg
|
||||||
complete = "false"
|
)
|
||||||
command = {"cmd": "pre_power_on", "complete": complete, "msg": msg}
|
return await self.send_privileged_command(
|
||||||
token_data = await self.get_token()
|
"pre_power_on", complete="false", msg=msg
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
)
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
### ADDED IN V2.0.5 Whatsminer API ###
|
||||||
|
@api_min_version("2.0.5")
|
||||||
|
async def set_temp_offset(self, temp_offset: int) -> dict:
|
||||||
|
"""Set the offset of miner hash board target temperature.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Set the offset of miner hash board target temperature, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
temp_offset: Target temperature offset.
|
||||||
|
Returns:
|
||||||
|
A reply informing of the status of setting temp offset.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not -30 < temp_offset < 0:
|
||||||
|
raise APIError(
|
||||||
|
f"Temp offset is outside of the allowed "
|
||||||
|
f"range. Please set a number between -30 and "
|
||||||
|
f"0."
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.send_privileged_command(
|
||||||
|
"set_temp_offset", temp_offset=temp_offset
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_min_version("2.0.5")
|
||||||
|
async def adjust_power_limit(self, power_limit: int) -> dict:
|
||||||
|
"""Set the upper limit of the miner's power. Cannot be higher than the ordinary power of the machine.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Set the upper limit of the miner's power, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
The miner will reboot after this is set.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
power_limit: New power limit.
|
||||||
|
Returns:
|
||||||
|
A reply informing of the status of setting power limit.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
"""
|
||||||
|
return await self.send_privileged_command(
|
||||||
|
"adjust_power_limit", power_limit=power_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_min_version("2.0.5")
|
||||||
|
async def adjust_upfreq_speed(self, upfreq_speed: int) -> dict:
|
||||||
|
"""Set the upfreq speed, 0 is the normal speed, 9 is the fastest speed.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Set the upfreq speed, 0 is the normal speed, 9 is the fastest speed, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
The faster the speed, the greater the final hash rate and power deviation, and the stability
|
||||||
|
may be impacted. Fast boot mode cannot be used at the same time.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
upfreq_speed: New upfreq speed.
|
||||||
|
Returns:
|
||||||
|
A reply informing of the status of setting upfreq speed.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if not 0 < upfreq_speed < 9:
|
||||||
|
raise APIError(
|
||||||
|
f"Upfreq speed is outside of the allowed "
|
||||||
|
f"range. Please set a number between 0 (Normal) and "
|
||||||
|
f"9 (Fastest)."
|
||||||
|
)
|
||||||
|
return await self.send_privileged_command(
|
||||||
|
"adjust_upfreq_speed", upfreq_speed=upfreq_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_min_version("2.0.5")
|
||||||
|
async def set_poweroff_cool(self, poweroff_cool: bool) -> dict:
|
||||||
|
"""Set whether to cool the machine when mining is stopped.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Set whether to cool the machine when mining is stopped, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
poweroff_cool: Whether to cool the miner during power off mode.
|
||||||
|
Returns:
|
||||||
|
A reply informing of the status of setting power off cooling mode.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return await self.send_privileged_command(
|
||||||
|
"set_poweroff_cool", poweroff_cool=int(poweroff_cool)
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_min_version("2.0.5")
|
||||||
|
async def set_fan_zero_speed(self, fan_zero_speed: bool) -> dict:
|
||||||
|
"""Sets whether the fan speed supports the lowest 0 speed.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Sets whether the fan speed supports the lowest 0 speed, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
fan_zero_speed: Whether the fan is allowed to support 0 speed.
|
||||||
|
Returns:
|
||||||
|
A reply informing of the status of setting fan minimum speed.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
"""
|
||||||
|
return await self.send_privileged_command(
|
||||||
|
"set_fan_zero_speed", fan_zero_speed=int(fan_zero_speed)
|
||||||
|
)
|
||||||
|
|
||||||
#### END privileged COMMANDS ####
|
#### END privileged COMMANDS ####
|
||||||
|
|
||||||
@@ -731,7 +848,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
Summary status of the miner.
|
Summary status of the miner.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -743,7 +859,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
Pool status of the miner.
|
Pool status of the miner.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -755,7 +870,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
Data on each PGA/ASC with their details.
|
Data on each PGA/ASC with their details.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -767,7 +881,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
Data on each PGA/ASC with their details.
|
Data on each PGA/ASC with their details.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -779,7 +892,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
Data on all devices with their static details.
|
Data on all devices with their static details.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -791,7 +903,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
Data on the PSU and power information.
|
Data on the PSU and power information.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -807,7 +918,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
with the other miner APIs.
|
with the other miner APIs.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
Version data for the miner.
|
Version data for the miner.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -819,7 +929,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
Version data for the miner.
|
Version data for the miner.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -831,7 +940,6 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
BTMiner status and firmware version.
|
BTMiner status and firmware version.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
@@ -843,8 +951,21 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
General miner info.
|
General miner info.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
return await self.send_command("get_miner_info")
|
return await self.send_command("get_miner_info", allow_warning=False)
|
||||||
|
|
||||||
|
@api_min_version("2.0.1")
|
||||||
|
async def get_error_code(self) -> dict:
|
||||||
|
"""Get a list of error codes from the miner.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
Get a list of error codes from the miner. Replaced `summary` as the location of error codes with API version 2.0.4.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of error codes on the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("get_error_code", allow_warning=False)
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
from pyasic.API import BaseMinerAPI
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyasic.API import APIError, BaseMinerAPI
|
||||||
|
|
||||||
|
|
||||||
class CGMinerAPI(BaseMinerAPI):
|
class CGMinerAPI(BaseMinerAPI):
|
||||||
@@ -19,8 +37,38 @@ class CGMinerAPI(BaseMinerAPI):
|
|||||||
port: The port to reference the API on. Default is 4028.
|
port: The port to reference the API on. Default is 4028.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ip: str, port: int = 4028):
|
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028):
|
||||||
super().__init__(ip, port)
|
super().__init__(ip, port)
|
||||||
|
self.api_ver = api_ver
|
||||||
|
|
||||||
|
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
|
||||||
|
# make sure we can actually run each command, otherwise they will fail
|
||||||
|
commands = self._check_commands(*commands)
|
||||||
|
# standard multicommand format is "command1+command2"
|
||||||
|
# doesn't work for S19 which uses the backup _x19_multicommand
|
||||||
|
command = "+".join(commands)
|
||||||
|
try:
|
||||||
|
data = await self.send_command(command, allow_warning=allow_warning)
|
||||||
|
except APIError:
|
||||||
|
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
|
||||||
|
data = await self._x19_multicommand(*command.split("+"))
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def _x19_multicommand(self, *commands):
|
||||||
|
data = None
|
||||||
|
try:
|
||||||
|
data = {}
|
||||||
|
# send all commands individually
|
||||||
|
for cmd in commands:
|
||||||
|
data[cmd] = []
|
||||||
|
data[cmd].append(await self.send_command(cmd, allow_warning=True))
|
||||||
|
except APIError as e:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(
|
||||||
|
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
async def version(self) -> dict:
|
async def version(self) -> dict:
|
||||||
"""Get miner version info.
|
"""Get miner version info.
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from pyasic.API import BaseMinerAPI
|
from pyasic.API import BaseMinerAPI
|
||||||
|
|
||||||
|
|
||||||
class UnknownAPI(BaseMinerAPI):
|
class UnknownAPI(BaseMinerAPI):
|
||||||
"""An abstraction of an API for a miner which is unknown.
|
"""An abstraction of an API for a miner which is unknown.
|
||||||
|
|
||||||
This class is designed to try to be a intersection of as many miner APIs
|
This class is designed to try to be an intersection of as many miner APIs
|
||||||
and API commands as possible (API ⋂ API), to ensure that it can be used
|
and API commands as possible (API ⋂ API), to ensure that it can be used
|
||||||
with as many APIs as possible.
|
with as many APIs as possible.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ip, port=4028):
|
def __init__(self, ip, api_ver: str = "0.0.0", port: int = 4028):
|
||||||
super().__init__(ip, port)
|
super().__init__(ip, port)
|
||||||
|
self.api_ver = api_ver
|
||||||
|
|
||||||
async def asccount(self) -> dict:
|
async def asccount(self) -> dict:
|
||||||
return await self.send_command("asccount")
|
return await self.send_command("asccount")
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
from pyasic.API.bmminer import BMMinerAPI
|
||||||
|
from pyasic.API.bosminer import BOSMinerAPI
|
||||||
|
from pyasic.API.btminer import BTMinerAPI
|
||||||
|
from pyasic.API.cgminer import CGMinerAPI
|
||||||
|
from pyasic.API.unknown import UnknownAPI
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
from pyasic.data import (
|
||||||
|
BraiinsOSError,
|
||||||
|
InnosiliconError,
|
||||||
|
MinerData,
|
||||||
|
WhatsminerError,
|
||||||
|
X19Error,
|
||||||
|
)
|
||||||
|
from pyasic.errors import APIError, APIWarning
|
||||||
|
from pyasic.miners import get_miner
|
||||||
|
from pyasic.miners.base import AnyMiner
|
||||||
|
from pyasic.miners.miner_factory import MinerFactory
|
||||||
|
from pyasic.miners.miner_listener import MinerListener
|
||||||
|
from pyasic.network import MinerNetwork
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BMMinerAPI",
|
||||||
|
"BOSMinerAPI",
|
||||||
|
"BTMinerAPI",
|
||||||
|
"CGMinerAPI",
|
||||||
|
"UnknownAPI",
|
||||||
|
"MinerConfig",
|
||||||
|
"MinerData",
|
||||||
|
"BraiinsOSError",
|
||||||
|
"InnosiliconError",
|
||||||
|
"WhatsminerError",
|
||||||
|
"X19Error",
|
||||||
|
"APIError",
|
||||||
|
"APIWarning",
|
||||||
|
"get_miner",
|
||||||
|
"AnyMiner",
|
||||||
|
"MinerFactory",
|
||||||
|
"MinerListener",
|
||||||
|
"MinerNetwork",
|
||||||
|
"PyasicSettings",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
from dataclasses import dataclass, asdict
|
# ------------------------------------------------------------------------------
|
||||||
from typing import Literal, List
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
import time
|
||||||
|
from dataclasses import asdict, dataclass, fields
|
||||||
|
from typing import Dict, List, Literal
|
||||||
|
|
||||||
import toml
|
import toml
|
||||||
import yaml
|
import yaml
|
||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -23,6 +40,10 @@ class _Pool:
|
|||||||
username: str = ""
|
username: str = ""
|
||||||
password: str = ""
|
password: str = ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fields(cls):
|
||||||
|
return fields(cls)
|
||||||
|
|
||||||
def from_dict(self, data: dict):
|
def from_dict(self, data: dict):
|
||||||
"""Convert raw pool data as a dict to usable data and save it to this class.
|
"""Convert raw pool data as a dict to usable data and save it to this class.
|
||||||
|
|
||||||
@@ -38,6 +59,19 @@ class _Pool:
|
|||||||
self.password = data[key]
|
self.password = data[key]
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def as_wm(self, user_suffix: str = None) -> dict:
|
||||||
|
"""Convert the data in this class to a dict usable by an Whatsminer device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
username = self.username
|
||||||
|
if user_suffix:
|
||||||
|
username = f"{username}{user_suffix}"
|
||||||
|
|
||||||
|
pool = {"url": self.url, "user": username, "pass": self.password}
|
||||||
|
return pool
|
||||||
|
|
||||||
def as_x19(self, user_suffix: str = None) -> dict:
|
def as_x19(self, user_suffix: str = None) -> dict:
|
||||||
"""Convert the data in this class to a dict usable by an X19 device.
|
"""Convert the data in this class to a dict usable by an X19 device.
|
||||||
|
|
||||||
@@ -51,6 +85,23 @@ class _Pool:
|
|||||||
pool = {"url": self.url, "user": username, "pass": self.password}
|
pool = {"url": self.url, "user": username, "pass": self.password}
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
|
def as_inno(self, user_suffix: str = None) -> dict:
|
||||||
|
"""Convert the data in this class to a dict usable by an Innosilicon device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
username = self.username
|
||||||
|
if user_suffix:
|
||||||
|
username = f"{username}{user_suffix}"
|
||||||
|
|
||||||
|
pool = {
|
||||||
|
f"Pool": self.url,
|
||||||
|
f"UserName": username,
|
||||||
|
f"Password": self.password,
|
||||||
|
}
|
||||||
|
return pool
|
||||||
|
|
||||||
def as_avalon(self, user_suffix: str = None) -> str:
|
def as_avalon(self, user_suffix: str = None) -> str:
|
||||||
"""Convert the data in this class to a string usable by an Avalonminer device.
|
"""Convert the data in this class to a string usable by an Avalonminer device.
|
||||||
|
|
||||||
@@ -92,6 +143,10 @@ class _PoolGroup:
|
|||||||
group_name: str = None
|
group_name: str = None
|
||||||
pools: List[_Pool] = None
|
pools: List[_Pool] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fields(cls):
|
||||||
|
return fields(cls)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if not self.group_name:
|
if not self.group_name:
|
||||||
self.group_name = "".join(
|
self.group_name = "".join(
|
||||||
@@ -127,6 +182,32 @@ class _PoolGroup:
|
|||||||
pools.append(pool.as_x19(user_suffix=user_suffix))
|
pools.append(pool.as_x19(user_suffix=user_suffix))
|
||||||
return pools
|
return pools
|
||||||
|
|
||||||
|
def as_inno(self, user_suffix: str = None) -> dict:
|
||||||
|
"""Convert the data in this class to a list usable by an Innosilicon device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
pools = {}
|
||||||
|
for idx, pool in enumerate(self.pools[:3]):
|
||||||
|
pool_data = pool.as_inno(user_suffix=user_suffix)
|
||||||
|
for key in pool_data:
|
||||||
|
pools[f"{key}{idx+1}"] = pool_data[key]
|
||||||
|
return pools
|
||||||
|
|
||||||
|
def as_wm(self, user_suffix: str = None) -> List[dict]:
|
||||||
|
"""Convert the data in this class to a list usable by a Whatsminer device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
pools = []
|
||||||
|
for pool in self.pools[:3]:
|
||||||
|
pools.append(pool.as_wm(user_suffix=user_suffix))
|
||||||
|
while len(pools) < 3:
|
||||||
|
pools.append({"url": None, "user": None, "pass": None})
|
||||||
|
return pools
|
||||||
|
|
||||||
def as_avalon(self, user_suffix: str = None) -> str:
|
def as_avalon(self, user_suffix: str = None) -> str:
|
||||||
"""Convert the data in this class to a dict usable by an Avalonminer device.
|
"""Convert the data in this class to a dict usable by an Avalonminer device.
|
||||||
|
|
||||||
@@ -164,7 +245,9 @@ class MinerConfig:
|
|||||||
fan_speed: Manual fan speed to run the fan at (only if temp_mode == "manual").
|
fan_speed: Manual fan speed to run the fan at (only if temp_mode == "manual").
|
||||||
asicboost: Whether or not to enable asicboost.
|
asicboost: Whether or not to enable asicboost.
|
||||||
autotuning_enabled: Whether or not to enable autotuning.
|
autotuning_enabled: Whether or not to enable autotuning.
|
||||||
|
autotuning_mode: Autotuning mode, either "wattage" or "hashrate".
|
||||||
autotuning_wattage: The wattage to use when autotuning.
|
autotuning_wattage: The wattage to use when autotuning.
|
||||||
|
autotuning_hashrate: The hashrate to use when autotuning.
|
||||||
dps_enabled: Whether or not to enable dynamic power scaling.
|
dps_enabled: Whether or not to enable dynamic power scaling.
|
||||||
dps_power_step: The amount of power to reduce autotuning by when the miner reaches dangerous temp.
|
dps_power_step: The amount of power to reduce autotuning by when the miner reaches dangerous temp.
|
||||||
dps_min_power: The minimum power to reduce autotuning to.
|
dps_min_power: The minimum power to reduce autotuning to.
|
||||||
@@ -177,7 +260,7 @@ class MinerConfig:
|
|||||||
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
|
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
|
||||||
temp_target: float = 70.0
|
temp_target: float = 70.0
|
||||||
temp_hot: float = 80.0
|
temp_hot: float = 80.0
|
||||||
temp_dangerous: float = 10.0
|
temp_dangerous: float = 100.0
|
||||||
|
|
||||||
minimum_fans: int = None
|
minimum_fans: int = None
|
||||||
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
|
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
|
||||||
@@ -185,7 +268,9 @@ class MinerConfig:
|
|||||||
asicboost: bool = None
|
asicboost: bool = None
|
||||||
|
|
||||||
autotuning_enabled: bool = True
|
autotuning_enabled: bool = True
|
||||||
autotuning_wattage: int = 900
|
autotuning_mode: Literal["power", "hashrate"] = None
|
||||||
|
autotuning_wattage: int = None
|
||||||
|
autotuning_hashrate: int = None
|
||||||
|
|
||||||
dps_enabled: bool = None
|
dps_enabled: bool = None
|
||||||
dps_power_step: int = None
|
dps_power_step: int = None
|
||||||
@@ -193,8 +278,13 @@ class MinerConfig:
|
|||||||
dps_shutdown_enabled: bool = None
|
dps_shutdown_enabled: bool = None
|
||||||
dps_shutdown_duration: float = None
|
dps_shutdown_duration: float = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fields(cls):
|
||||||
|
return fields(cls)
|
||||||
|
|
||||||
def as_dict(self) -> dict:
|
def as_dict(self) -> dict:
|
||||||
"""Convert the data in this class to a dict."""
|
"""Convert the data in this class to a dict."""
|
||||||
|
logging.debug(f"MinerConfig - (To Dict) - Dumping Dict config")
|
||||||
data_dict = asdict(self)
|
data_dict = asdict(self)
|
||||||
for key in asdict(self).keys():
|
for key in asdict(self).keys():
|
||||||
if data_dict[key] is None:
|
if data_dict[key] is None:
|
||||||
@@ -203,10 +293,12 @@ class MinerConfig:
|
|||||||
|
|
||||||
def as_toml(self) -> str:
|
def as_toml(self) -> str:
|
||||||
"""Convert the data in this class to toml."""
|
"""Convert the data in this class to toml."""
|
||||||
|
logging.debug(f"MinerConfig - (To TOML) - Dumping TOML config")
|
||||||
return toml.dumps(self.as_dict())
|
return toml.dumps(self.as_dict())
|
||||||
|
|
||||||
def as_yaml(self) -> str:
|
def as_yaml(self) -> str:
|
||||||
"""Convert the data in this class to yaml."""
|
"""Convert the data in this class to yaml."""
|
||||||
|
logging.debug(f"MinerConfig - (To YAML) - Dumping YAML config")
|
||||||
return yaml.dump(self.as_dict(), sort_keys=False)
|
return yaml.dump(self.as_dict(), sort_keys=False)
|
||||||
|
|
||||||
def from_raw(self, data: dict):
|
def from_raw(self, data: dict):
|
||||||
@@ -216,6 +308,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
data: The raw config data to convert.
|
data: The raw config data to convert.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (From Raw) - Loading raw config")
|
||||||
pool_groups = []
|
pool_groups = []
|
||||||
for key in data.keys():
|
for key in data.keys():
|
||||||
if key == "pools":
|
if key == "pools":
|
||||||
@@ -229,6 +322,12 @@ class MinerConfig:
|
|||||||
self.temp_mode = "manual"
|
self.temp_mode = "manual"
|
||||||
if data.get("bitmain-fan-pwm"):
|
if data.get("bitmain-fan-pwm"):
|
||||||
self.fan_speed = int(data["bitmain-fan-pwm"])
|
self.fan_speed = int(data["bitmain-fan-pwm"])
|
||||||
|
elif key == "bitmain-work-mode":
|
||||||
|
if data[key]:
|
||||||
|
if data[key] == 1:
|
||||||
|
self.autotuning_wattage = 0
|
||||||
|
if data[key] == 2:
|
||||||
|
self.autotuning_wattage = 1200
|
||||||
elif key == "fan_control":
|
elif key == "fan_control":
|
||||||
for _key in data[key].keys():
|
for _key in data[key].keys():
|
||||||
if _key == "min_fans":
|
if _key == "min_fans":
|
||||||
@@ -256,14 +355,20 @@ class MinerConfig:
|
|||||||
self.autotuning_enabled = data[key][_key]
|
self.autotuning_enabled = data[key][_key]
|
||||||
elif _key == "psu_power_limit":
|
elif _key == "psu_power_limit":
|
||||||
self.autotuning_wattage = data[key][_key]
|
self.autotuning_wattage = data[key][_key]
|
||||||
|
elif _key == "power_target":
|
||||||
|
self.autotuning_wattage = data[key][_key]
|
||||||
|
elif _key == "hashrate_target":
|
||||||
|
self.autotuning_hashrate = data[key][_key]
|
||||||
|
elif _key == "mode":
|
||||||
|
self.autotuning_mode = data[key][_key].replace("_target", "")
|
||||||
|
|
||||||
if key == "power_scaling":
|
if key in ["power_scaling", "performance_scaling"]:
|
||||||
for _key in data[key].keys():
|
for _key in data[key].keys():
|
||||||
if _key == "enabled":
|
if _key == "enabled":
|
||||||
self.dps_enabled = data[key][_key]
|
self.dps_enabled = data[key][_key]
|
||||||
elif _key == "power_step":
|
elif _key == "power_step":
|
||||||
self.dps_power_step = data[key][_key]
|
self.dps_power_step = data[key][_key]
|
||||||
elif _key == "min_psu_power_limit":
|
elif _key in ["min_psu_power_limit", "min_power_target"]:
|
||||||
self.dps_min_power = data[key][_key]
|
self.dps_min_power = data[key][_key]
|
||||||
elif _key == "shutdown_enabled":
|
elif _key == "shutdown_enabled":
|
||||||
self.dps_shutdown_enabled = data[key][_key]
|
self.dps_shutdown_enabled = data[key][_key]
|
||||||
@@ -273,17 +378,33 @@ class MinerConfig:
|
|||||||
self.pool_groups = pool_groups
|
self.pool_groups = pool_groups
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def from_api(self, pools: list):
|
||||||
|
"""Convert list output from the `AnyMiner.api.pools()` command into a usable data and save it to this class.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
pools: The list of pool data to convert.
|
||||||
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (From API) - Loading API config")
|
||||||
|
_pools = []
|
||||||
|
for pool in pools:
|
||||||
|
url = pool.get("URL")
|
||||||
|
user = pool.get("User")
|
||||||
|
_pools.append({"url": url, "user": user, "pass": "123"})
|
||||||
|
self.pool_groups = [_PoolGroup().from_dict({"pools": _pools})]
|
||||||
|
return self
|
||||||
|
|
||||||
def from_dict(self, data: dict):
|
def from_dict(self, data: dict):
|
||||||
"""Convert an output dict of this class back into usable data and save it to this class.
|
"""Convert an output dict of this class back into usable data and save it to this class.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
data: The dict config data to convert.
|
data: The dict config data to convert.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (From Dict) - Loading Dict config")
|
||||||
pool_groups = []
|
pool_groups = []
|
||||||
for group in data["pool_groups"]:
|
for group in data["pool_groups"]:
|
||||||
pool_groups.append(_PoolGroup().from_dict(group))
|
pool_groups.append(_PoolGroup().from_dict(group))
|
||||||
for key in data.keys():
|
for key in data:
|
||||||
if getattr(self, key) and not key == "pool_groups":
|
if hasattr(self, key) and not key == "pool_groups":
|
||||||
setattr(self, key, data[key])
|
setattr(self, key, data[key])
|
||||||
self.pool_groups = pool_groups
|
self.pool_groups = pool_groups
|
||||||
return self
|
return self
|
||||||
@@ -294,6 +415,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
data: The toml config data to convert.
|
data: The toml config data to convert.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (From TOML) - Loading TOML config")
|
||||||
return self.from_dict(toml.loads(data))
|
return self.from_dict(toml.loads(data))
|
||||||
|
|
||||||
def from_yaml(self, data: str):
|
def from_yaml(self, data: str):
|
||||||
@@ -302,19 +424,49 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
data: The yaml config data to convert.
|
data: The yaml config data to convert.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (From YAML) - Loading YAML config")
|
||||||
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
|
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
|
||||||
|
|
||||||
|
def as_wm(self, user_suffix: str = None) -> Dict[str, int]:
|
||||||
|
"""Convert the data in this class to a config usable by a Whatsminer device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (As Whatsminer) - Generating Whatsminer config")
|
||||||
|
return {
|
||||||
|
"pools": self.pool_groups[0].as_wm(user_suffix=user_suffix),
|
||||||
|
"wattage": self.autotuning_wattage,
|
||||||
|
}
|
||||||
|
|
||||||
|
def as_inno(self, user_suffix: str = None) -> dict:
|
||||||
|
"""Convert the data in this class to a config usable by an Innosilicon device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (As Inno) - Generating Innosilicon config")
|
||||||
|
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
|
||||||
|
|
||||||
def as_x19(self, user_suffix: str = None) -> str:
|
def as_x19(self, user_suffix: str = None) -> str:
|
||||||
"""Convert the data in this class to a config usable by an X19 device.
|
"""Convert the data in this class to a config usable by an X19 device.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
user_suffix: The suffix to append to username.
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (As X19) - Generating X19 config")
|
||||||
cfg = {
|
cfg = {
|
||||||
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
|
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
|
||||||
"bitmain-fan-ctrl": False,
|
"bitmain-fan-ctrl": False,
|
||||||
"bitmain-fan-pwn": 100,
|
"bitmain-fan-pwn": 100,
|
||||||
|
"miner-mode": 0, # Normal Mode
|
||||||
}
|
}
|
||||||
|
if self.autotuning_wattage:
|
||||||
|
if self.autotuning_wattage == 0:
|
||||||
|
cfg["miner-mode"] = 1 # Sleep Mode
|
||||||
|
|
||||||
|
if self.autotuning_wattage < 1800:
|
||||||
|
cfg["miner-mode"] = 2 # LPM?
|
||||||
|
|
||||||
if not self.temp_mode == "auto":
|
if not self.temp_mode == "auto":
|
||||||
cfg["bitmain-fan-ctrl"] = True
|
cfg["bitmain-fan-ctrl"] = True
|
||||||
@@ -330,6 +482,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
user_suffix: The suffix to append to username.
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (As Avalon) - Generating AvalonMiner config")
|
||||||
cfg = self.pool_groups[0].as_avalon(user_suffix=user_suffix)
|
cfg = self.pool_groups[0].as_avalon(user_suffix=user_suffix)
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
@@ -340,11 +493,12 @@ class MinerConfig:
|
|||||||
model: The model of the miner to be used in the format portion of the config.
|
model: The model of the miner to be used in the format portion of the config.
|
||||||
user_suffix: The suffix to append to username.
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (As BOS) - Generating BOSMiner config")
|
||||||
cfg = {
|
cfg = {
|
||||||
"format": {
|
"format": {
|
||||||
"version": "1.2+",
|
"version": "1.2+",
|
||||||
"model": f"Antminer {model}",
|
"model": f"Antminer {model.replace('j', 'J')}",
|
||||||
"generator": "Upstream Config Utility",
|
"generator": "pyasic",
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
},
|
},
|
||||||
"group": [
|
"group": [
|
||||||
@@ -361,9 +515,19 @@ class MinerConfig:
|
|||||||
if self.autotuning_enabled or self.autotuning_wattage:
|
if self.autotuning_enabled or self.autotuning_wattage:
|
||||||
cfg["autotuning"] = {}
|
cfg["autotuning"] = {}
|
||||||
if self.autotuning_enabled:
|
if self.autotuning_enabled:
|
||||||
cfg["autotuning"]["enabled"] = self.autotuning_enabled
|
cfg["autotuning"]["enabled"] = True
|
||||||
if self.autotuning_wattage:
|
else:
|
||||||
cfg["autotuning"]["psu_power_limit"] = self.autotuning_wattage
|
cfg["autotuning"]["enabled"] = False
|
||||||
|
if self.autotuning_mode:
|
||||||
|
cfg["format"]["version"] = "2.0"
|
||||||
|
cfg["autotuning"]["mode"] = self.autotuning_mode + "_target"
|
||||||
|
if self.autotuning_wattage:
|
||||||
|
cfg["autotuning"]["power_target"] = self.autotuning_wattage
|
||||||
|
elif self.autotuning_hashrate:
|
||||||
|
cfg["autotuning"]["hashrate_target"] = self.autotuning_hashrate
|
||||||
|
else:
|
||||||
|
if self.autotuning_wattage:
|
||||||
|
cfg["autotuning"]["psu_power_limit"] = self.autotuning_wattage
|
||||||
|
|
||||||
if self.asicboost:
|
if self.asicboost:
|
||||||
cfg["hash_chain_global"] = {}
|
cfg["hash_chain_global"] = {}
|
||||||
@@ -387,7 +551,10 @@ class MinerConfig:
|
|||||||
if self.dps_power_step:
|
if self.dps_power_step:
|
||||||
cfg["power_scaling"]["power_step"] = self.dps_power_step
|
cfg["power_scaling"]["power_step"] = self.dps_power_step
|
||||||
if self.dps_min_power:
|
if self.dps_min_power:
|
||||||
cfg["power_scaling"]["min_psu_power_limit"] = self.dps_min_power
|
if cfg["format"]["version"] == "2.0":
|
||||||
|
cfg["power_scaling"]["min_power_target"] = self.dps_min_power
|
||||||
|
else:
|
||||||
|
cfg["power_scaling"]["min_psu_power_limit"] = self.dps_min_power
|
||||||
if self.dps_shutdown_enabled:
|
if self.dps_shutdown_enabled:
|
||||||
cfg["power_scaling"]["shutdown_enabled"] = self.dps_shutdown_enabled
|
cfg["power_scaling"]["shutdown_enabled"] = self.dps_shutdown_enabled
|
||||||
if self.dps_shutdown_duration:
|
if self.dps_shutdown_duration:
|
||||||
|
|||||||
@@ -1,5 +1,62 @@
|
|||||||
from dataclasses import dataclass, field, asdict
|
# ------------------------------------------------------------------------------
|
||||||
from datetime import datetime
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from dataclasses import asdict, dataclass, field, fields
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HashBoard:
|
||||||
|
"""A Dataclass to standardize hashboard data.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
slot: The slot of the board as an int.
|
||||||
|
hashrate: The hashrate of the board in TH/s as a float.
|
||||||
|
temp: The temperature of the PCB as an int.
|
||||||
|
chip_temp: The temperature of the chips as an int.
|
||||||
|
chips: The chip count of the board as an int.
|
||||||
|
expected_chips: The ideal chip count of the board as an int.
|
||||||
|
missing: Whether the board is returned from the miners data as a bool.
|
||||||
|
"""
|
||||||
|
|
||||||
|
slot: int = 0
|
||||||
|
hashrate: float = 0.0
|
||||||
|
temp: int = -1
|
||||||
|
chip_temp: int = -1
|
||||||
|
chips: int = 0
|
||||||
|
expected_chips: int = 0
|
||||||
|
missing: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Fan:
|
||||||
|
"""A Dataclass to standardize fan data.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
speed: The speed of the fan.
|
||||||
|
"""
|
||||||
|
|
||||||
|
speed: int = -1
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -10,8 +67,12 @@ class MinerData:
|
|||||||
ip: The IP of the miner as a str.
|
ip: The IP of the miner as a str.
|
||||||
datetime: The time and date this data was generated.
|
datetime: The time and date this data was generated.
|
||||||
model: The model of the miner as a str.
|
model: The model of the miner as a str.
|
||||||
|
make: The make of the miner as a str.
|
||||||
|
api_ver: The current api version on the miner as a str.
|
||||||
|
fw_ver: The current firmware version on the miner as a str.
|
||||||
hostname: The network hostname of the miner as a str.
|
hostname: The network hostname of the miner as a str.
|
||||||
hashrate: The hashrate of the miner in TH/s as a float.
|
hashrate: The hashrate of the miner in TH/s as a float.
|
||||||
|
nominal_hashrate: The factory nominal hashrate of the miner in TH/s as a float.
|
||||||
left_board_hashrate: The hashrate of the left board of the miner in TH/s as a float.
|
left_board_hashrate: The hashrate of the left board of the miner in TH/s as a float.
|
||||||
center_board_hashrate: The hashrate of the center board of the miner in TH/s as a float.
|
center_board_hashrate: The hashrate of the center board of the miner in TH/s as a float.
|
||||||
right_board_hashrate: The hashrate of the right board of the miner in TH/s as a float.
|
right_board_hashrate: The hashrate of the right board of the miner in TH/s as a float.
|
||||||
@@ -29,47 +90,58 @@ class MinerData:
|
|||||||
fan_2: The speed of the second fan as an int.
|
fan_2: The speed of the second fan as an int.
|
||||||
fan_3: The speed of the third fan as an int.
|
fan_3: The speed of the third fan as an int.
|
||||||
fan_4: The speed of the fourth fan as an int.
|
fan_4: The speed of the fourth fan as an int.
|
||||||
|
fan_psu: The speed of the PSU on the fan if the miner collects it.
|
||||||
left_chips: The number of chips online in the left board as an int.
|
left_chips: The number of chips online in the left board as an int.
|
||||||
center_chips: The number of chips online in the left board as an int.
|
center_chips: The number of chips online in the left board as an int.
|
||||||
right_chips: The number of chips online in the left board as an int.
|
right_chips: The number of chips online in the left board as an int.
|
||||||
total_chips: The total number of chips on all boards. Calculated automatically.
|
total_chips: The total number of chips on all boards. Calculated automatically.
|
||||||
ideal_chips: The ideal number of chips in the miner as an int.
|
ideal_chips: The ideal number of chips in the miner as an int.
|
||||||
perecent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
|
percent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
|
||||||
nominal: The nominal amount of chips in the miner. Calculated automatically.
|
nominal: Whether the number of chips in the miner is nominal. Calculated automatically.
|
||||||
pool_split: The pool split as a str.
|
pool_split: The pool split as a str.
|
||||||
pool_1_url: The first pool url on the miner as a str.
|
pool_1_url: The first pool url on the miner as a str.
|
||||||
pool_1_user: The first pool user on the miner as a str.
|
pool_1_user: The first pool user on the miner as a str.
|
||||||
pool_2_url: The second pool url on the miner as a str.
|
pool_2_url: The second pool url on the miner as a str.
|
||||||
pool_2_user: The second pool user on the miner as a str.
|
pool_2_user: The second pool user on the miner as a str.
|
||||||
errors: A list of errors on the miner.
|
errors: A list of errors on the miner.
|
||||||
|
fault_light: Whether or not the fault light is on as a boolean.
|
||||||
|
efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ip: str
|
ip: str
|
||||||
datetime: datetime = None
|
datetime: datetime = None
|
||||||
mac: str = "00:00:00:00:00:00"
|
mac: str = "00:00:00:00:00:00"
|
||||||
model: str = "Unknown"
|
model: str = "Unknown"
|
||||||
|
make: str = "Unknown"
|
||||||
|
api_ver: str = "Unknown"
|
||||||
|
fw_ver: str = "Unknown"
|
||||||
hostname: str = "Unknown"
|
hostname: str = "Unknown"
|
||||||
hashrate: float = 0
|
hashrate: float = 0
|
||||||
left_board_hashrate: float = 0
|
nominal_hashrate: float = 0
|
||||||
center_board_hashrate: float = 0
|
hashboards: List[HashBoard] = field(default_factory=list)
|
||||||
right_board_hashrate: float = 0
|
ideal_hashboards: int = 1
|
||||||
|
left_board_hashrate: float = field(init=False)
|
||||||
|
center_board_hashrate: float = field(init=False)
|
||||||
|
right_board_hashrate: float = field(init=False)
|
||||||
temperature_avg: int = field(init=False)
|
temperature_avg: int = field(init=False)
|
||||||
env_temp: float = 0
|
env_temp: float = -1.0
|
||||||
left_board_temp: int = 0
|
left_board_temp: int = field(init=False)
|
||||||
left_board_chip_temp: int = 0
|
left_board_chip_temp: int = field(init=False)
|
||||||
center_board_temp: int = 0
|
center_board_temp: int = field(init=False)
|
||||||
center_board_chip_temp: int = 0
|
center_board_chip_temp: int = field(init=False)
|
||||||
right_board_temp: int = 0
|
right_board_temp: int = field(init=False)
|
||||||
right_board_chip_temp: int = 0
|
right_board_chip_temp: int = field(init=False)
|
||||||
wattage: int = 0
|
wattage: int = -1
|
||||||
wattage_limit: int = 0
|
wattage_limit: int = -1
|
||||||
fan_1: int = -1
|
fans: List[Fan] = field(default_factory=list)
|
||||||
fan_2: int = -1
|
fan_1: int = field(init=False)
|
||||||
fan_3: int = -1
|
fan_2: int = field(init=False)
|
||||||
fan_4: int = -1
|
fan_3: int = field(init=False)
|
||||||
left_chips: int = 0
|
fan_4: int = field(init=False)
|
||||||
center_chips: int = 0
|
fan_psu: int = -1
|
||||||
right_chips: int = 0
|
left_chips: int = field(init=False)
|
||||||
|
center_chips: int = field(init=False)
|
||||||
|
right_chips: int = field(init=False)
|
||||||
total_chips: int = field(init=False)
|
total_chips: int = field(init=False)
|
||||||
ideal_chips: int = 1
|
ideal_chips: int = 1
|
||||||
percent_ideal: float = field(init=False)
|
percent_ideal: float = field(init=False)
|
||||||
@@ -79,19 +151,248 @@ class MinerData:
|
|||||||
pool_1_user: str = "Unknown"
|
pool_1_user: str = "Unknown"
|
||||||
pool_2_url: str = ""
|
pool_2_url: str = ""
|
||||||
pool_2_user: str = ""
|
pool_2_user: str = ""
|
||||||
errors: list = field(default_factory=list)
|
errors: List[
|
||||||
|
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
|
||||||
|
] = field(default_factory=list)
|
||||||
|
fault_light: Union[bool, None] = None
|
||||||
|
efficiency: int = field(init=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fields(cls):
|
||||||
|
return [f.name for f in fields(cls)]
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self.datetime = datetime.now()
|
self.datetime = datetime.now(timezone.utc).astimezone()
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
try:
|
||||||
|
return getattr(self, item)
|
||||||
|
except AttributeError:
|
||||||
|
raise KeyError(f"{item}")
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
return setattr(self, key, value)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter([item for item in self.asdict()])
|
||||||
|
|
||||||
|
def __truediv__(self, other):
|
||||||
|
return self // other
|
||||||
|
|
||||||
|
def __floordiv__(self, other):
|
||||||
|
cp = copy.deepcopy(self)
|
||||||
|
for key in self:
|
||||||
|
item = getattr(self, key)
|
||||||
|
if isinstance(item, int):
|
||||||
|
setattr(cp, key, item // other)
|
||||||
|
if isinstance(item, float):
|
||||||
|
setattr(cp, key, round(item / other, 2))
|
||||||
|
return cp
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
if not isinstance(other, MinerData):
|
||||||
|
raise TypeError("Cannot add MinerData to non MinerData type.")
|
||||||
|
cp = copy.deepcopy(self)
|
||||||
|
for key in self:
|
||||||
|
item = getattr(self, key)
|
||||||
|
other_item = getattr(other, key)
|
||||||
|
if item is None:
|
||||||
|
item = 0
|
||||||
|
if other_item is None:
|
||||||
|
other_item = 0
|
||||||
|
|
||||||
|
if isinstance(item, int):
|
||||||
|
setattr(cp, key, item + other_item)
|
||||||
|
if isinstance(item, float):
|
||||||
|
setattr(cp, key, item + other_item)
|
||||||
|
if isinstance(item, str):
|
||||||
|
setattr(cp, key, "")
|
||||||
|
if isinstance(item, list):
|
||||||
|
setattr(cp, key, item + other_item)
|
||||||
|
if isinstance(item, bool):
|
||||||
|
setattr(cp, key, item & other_item)
|
||||||
|
return cp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_1(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.fans) > 0:
|
||||||
|
return self.fans[0].speed
|
||||||
|
|
||||||
|
@fan_1.setter
|
||||||
|
def fan_1(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_2(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.fans) > 1:
|
||||||
|
return self.fans[1].speed
|
||||||
|
|
||||||
|
@fan_2.setter
|
||||||
|
def fan_2(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_3(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.fans) > 2:
|
||||||
|
return self.fans[2].speed
|
||||||
|
|
||||||
|
@fan_3.setter
|
||||||
|
def fan_3(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_4(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.fans) > 3:
|
||||||
|
return self.fans[3].speed
|
||||||
|
|
||||||
|
@fan_4.setter
|
||||||
|
def fan_4(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_chips(self): # noqa - Skip PyCharm inspection
|
def total_chips(self): # noqa - Skip PyCharm inspection
|
||||||
return self.right_chips + self.center_chips + self.left_chips
|
return sum([hb.chips for hb in self.hashboards])
|
||||||
|
|
||||||
@total_chips.setter
|
@total_chips.setter
|
||||||
def total_chips(self, val):
|
def total_chips(self, val):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def left_chips(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) in [2, 3]:
|
||||||
|
return self.hashboards[0].chips
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@left_chips.setter
|
||||||
|
def left_chips(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center_chips(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) == 1:
|
||||||
|
return self.hashboards[0].chips
|
||||||
|
if len(self.hashboards) == 3:
|
||||||
|
return self.hashboards[1].chips
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@center_chips.setter
|
||||||
|
def center_chips(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def right_chips(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) == 2:
|
||||||
|
return self.hashboards[1].chips
|
||||||
|
if len(self.hashboards) == 3:
|
||||||
|
return self.hashboards[2].chips
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@right_chips.setter
|
||||||
|
def right_chips(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def left_board_hashrate(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) in [2, 3]:
|
||||||
|
return self.hashboards[0].hashrate
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@left_board_hashrate.setter
|
||||||
|
def left_board_hashrate(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center_board_hashrate(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) == 1:
|
||||||
|
return self.hashboards[0].hashrate
|
||||||
|
if len(self.hashboards) == 3:
|
||||||
|
return self.hashboards[1].hashrate
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@center_board_hashrate.setter
|
||||||
|
def center_board_hashrate(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def right_board_hashrate(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) == 2:
|
||||||
|
return self.hashboards[1].hashrate
|
||||||
|
if len(self.hashboards) == 3:
|
||||||
|
return self.hashboards[2].hashrate
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@right_board_hashrate.setter
|
||||||
|
def right_board_hashrate(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def left_board_temp(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) in [2, 3]:
|
||||||
|
return self.hashboards[0].temp
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@left_board_temp.setter
|
||||||
|
def left_board_temp(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center_board_temp(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) == 1:
|
||||||
|
return self.hashboards[0].temp
|
||||||
|
if len(self.hashboards) == 3:
|
||||||
|
return self.hashboards[1].temp
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@center_board_temp.setter
|
||||||
|
def center_board_temp(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def right_board_temp(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) == 2:
|
||||||
|
return self.hashboards[1].temp
|
||||||
|
if len(self.hashboards) == 3:
|
||||||
|
return self.hashboards[2].temp
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@right_board_temp.setter
|
||||||
|
def right_board_temp(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def left_board_chip_temp(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) in [2, 3]:
|
||||||
|
return self.hashboards[0].chip_temp
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@left_board_chip_temp.setter
|
||||||
|
def left_board_chip_temp(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center_board_chip_temp(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) == 1:
|
||||||
|
return self.hashboards[0].chip_temp
|
||||||
|
if len(self.hashboards) == 3:
|
||||||
|
return self.hashboards[1].chip_temp
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@center_board_chip_temp.setter
|
||||||
|
def center_board_chip_temp(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def right_board_chip_temp(self): # noqa - Skip PyCharm inspection
|
||||||
|
if len(self.hashboards) == 2:
|
||||||
|
return self.hashboards[1].chip_temp
|
||||||
|
if len(self.hashboards) == 3:
|
||||||
|
return self.hashboards[2].chip_temp
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@right_board_chip_temp.setter
|
||||||
|
def right_board_chip_temp(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nominal(self): # noqa - Skip PyCharm inspection
|
def nominal(self): # noqa - Skip PyCharm inspection
|
||||||
return self.ideal_chips == self.total_chips
|
return self.ideal_chips == self.total_chips
|
||||||
@@ -102,6 +403,8 @@ class MinerData:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def percent_ideal(self): # noqa - Skip PyCharm inspection
|
def percent_ideal(self): # noqa - Skip PyCharm inspection
|
||||||
|
if self.total_chips == 0 or self.ideal_chips == 0:
|
||||||
|
return 0
|
||||||
return round((self.total_chips / self.ideal_chips) * 100)
|
return round((self.total_chips / self.ideal_chips) * 100)
|
||||||
|
|
||||||
@percent_ideal.setter
|
@percent_ideal.setter
|
||||||
@@ -112,13 +415,9 @@ class MinerData:
|
|||||||
def temperature_avg(self): # noqa - Skip PyCharm inspection
|
def temperature_avg(self): # noqa - Skip PyCharm inspection
|
||||||
total_temp = 0
|
total_temp = 0
|
||||||
temp_count = 0
|
temp_count = 0
|
||||||
for temp in [
|
for hb in self.hashboards:
|
||||||
self.left_board_chip_temp,
|
if hb.temp and not hb.temp == -1:
|
||||||
self.center_board_chip_temp,
|
total_temp += hb.temp
|
||||||
self.right_board_chip_temp,
|
|
||||||
]:
|
|
||||||
if temp and not temp == 0:
|
|
||||||
total_temp += temp
|
|
||||||
temp_count += 1
|
temp_count += 1
|
||||||
if not temp_count > 0:
|
if not temp_count > 0:
|
||||||
return 0
|
return 0
|
||||||
@@ -128,5 +427,92 @@ class MinerData:
|
|||||||
def temperature_avg(self, val):
|
def temperature_avg(self, val):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def asdict(self):
|
@property
|
||||||
|
def efficiency(self): # noqa - Skip PyCharm inspection
|
||||||
|
if self.hashrate == 0:
|
||||||
|
return 0
|
||||||
|
return round(self.wattage / self.hashrate)
|
||||||
|
|
||||||
|
@efficiency.setter
|
||||||
|
def efficiency(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def asdict(self) -> dict:
|
||||||
|
"""Get this dataclass as a dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary version of this class.
|
||||||
|
"""
|
||||||
|
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
|
def as_json(self) -> str:
|
||||||
|
"""Get this dataclass as JSON.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON version of this class.
|
||||||
|
"""
|
||||||
|
logging.debug(f"MinerData - (To JSON) - Dumping JSON data")
|
||||||
|
data = self.asdict()
|
||||||
|
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
||||||
|
return json.dumps(data)
|
||||||
|
|
||||||
|
def as_csv(self) -> str:
|
||||||
|
"""Get this dataclass as CSV.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A CSV version of this class with no headers.
|
||||||
|
"""
|
||||||
|
logging.debug(f"MinerData - (To CSV) - Dumping CSV data")
|
||||||
|
data = self.asdict()
|
||||||
|
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
||||||
|
errs = []
|
||||||
|
for error in data["errors"]:
|
||||||
|
errs.append(error["error_message"])
|
||||||
|
data["errors"] = "; ".join(errs)
|
||||||
|
data_list = [str(data[item]) for item in data]
|
||||||
|
return ",".join(data_list)
|
||||||
|
|
||||||
|
def as_influxdb(self, measurement_name: str = "miner_data") -> str:
|
||||||
|
"""Get this dataclass as [influxdb line protocol](https://docs.influxdata.com/influxdb/v2.4/reference/syntax/line-protocol/).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
measurement_name: The name of the measurement to insert into in influxdb.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A influxdb line protocol version of this class.
|
||||||
|
"""
|
||||||
|
logging.debug(f"MinerData - (To InfluxDB) - Dumping InfluxDB data")
|
||||||
|
tag_data = [measurement_name]
|
||||||
|
field_data = []
|
||||||
|
|
||||||
|
tags = ["ip", "mac", "model", "hostname"]
|
||||||
|
for attribute in self:
|
||||||
|
if attribute in tags:
|
||||||
|
escaped_data = self[attribute].replace(" ", "\\ ")
|
||||||
|
tag_data.append(f"{attribute}={escaped_data}")
|
||||||
|
continue
|
||||||
|
if isinstance(self[attribute], str):
|
||||||
|
field_data.append(f'{attribute}="{self[attribute]}"')
|
||||||
|
continue
|
||||||
|
if isinstance(self[attribute], bool):
|
||||||
|
field_data.append(f"{attribute}={str(self[attribute]).lower()}")
|
||||||
|
continue
|
||||||
|
if isinstance(self[attribute], int):
|
||||||
|
field_data.append(f"{attribute}={self[attribute]}")
|
||||||
|
continue
|
||||||
|
if isinstance(self[attribute], float):
|
||||||
|
field_data.append(f"{attribute}={self[attribute]}")
|
||||||
|
continue
|
||||||
|
if attribute == "fault_light" and not self[attribute]:
|
||||||
|
field_data.append(f"{attribute}=false")
|
||||||
|
continue
|
||||||
|
if attribute == "errors":
|
||||||
|
for idx, item in enumerate(self[attribute]):
|
||||||
|
field_data.append(f'error_{idx+1}="{item.error_message}"')
|
||||||
|
|
||||||
|
tags_str = ",".join(tag_data)
|
||||||
|
field_str = ",".join(field_data)
|
||||||
|
timestamp = str(int(time.mktime(self.datetime.timetuple()) * 1e9))
|
||||||
|
|
||||||
|
return " ".join([tags_str, field_str, timestamp])
|
||||||
|
|||||||
37
pyasic/data/error_codes/X19.py
Normal file
37
pyasic/data/error_codes/X19.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, fields
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class X19Error:
|
||||||
|
"""A Dataclass to handle error codes of X19 miners.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error_message: The error message as a string.
|
||||||
|
error_code: The error code as an int. 0 if the message is not assigned a code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
error_message: str
|
||||||
|
error_code: int = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fields(cls):
|
||||||
|
return fields(cls)
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
return asdict(self)
|
||||||
@@ -1,2 +1,26 @@
|
|||||||
from .whatsminer import WhatsminerError
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
from .bos import BraiinsOSError
|
from .bos import BraiinsOSError
|
||||||
|
from .innosilicon import InnosiliconError
|
||||||
|
from .whatsminer import WhatsminerError
|
||||||
|
from .X19 import X19Error
|
||||||
|
|
||||||
|
MinerErrorData = TypeVar(
|
||||||
|
"MinerErrorData", WhatsminerError, BraiinsOSError, X19Error, InnosiliconError
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,11 +1,37 @@
|
|||||||
from dataclasses import dataclass, asdict
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, fields
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BraiinsOSError:
|
class BraiinsOSError:
|
||||||
"""A Dataclass to handle error codes of BraiinsOS+ miners."""
|
"""A Dataclass to handle error codes of BraiinsOS+ miners.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error_message: The error message as a string.
|
||||||
|
error_code: The error code as an int. 0 if the message is not assigned a code.
|
||||||
|
"""
|
||||||
|
|
||||||
error_message: str
|
error_message: str
|
||||||
|
error_code: int = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fields(cls):
|
||||||
|
return fields(cls)
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|||||||
71
pyasic/data/error_codes/innosilicon.py
Normal file
71
pyasic/data/error_codes/innosilicon.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, field, fields
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InnosiliconError:
|
||||||
|
"""A Dataclass to handle error codes of Innosilicon miners.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error_code: The error code as an int.
|
||||||
|
error_message: The error message as a string. Automatically found from the error code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
error_code: int
|
||||||
|
error_message: str = field(init=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fields(cls):
|
||||||
|
return fields(cls)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error_message(self): # noqa - Skip PyCharm inspection
|
||||||
|
if self.error_code in ERROR_CODES:
|
||||||
|
return ERROR_CODES[self.error_code]
|
||||||
|
return "Unknown error type."
|
||||||
|
|
||||||
|
@error_message.setter
|
||||||
|
def error_message(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
ERROR_CODES = {
|
||||||
|
21: "The PLUG signal of the hash board is not detected.",
|
||||||
|
22: "Power I2C communication is abnormal.",
|
||||||
|
23: "The SPI of all hash boards is blocked.",
|
||||||
|
24: "Some of the hash boards fail to connect to the SPI'.",
|
||||||
|
25: "Hashboard failed to set frequency.",
|
||||||
|
26: "Hashboard failed to set voltage.",
|
||||||
|
27: "Chip BIST test failed.",
|
||||||
|
28: "Hashboard SPI communication is abnormal.",
|
||||||
|
29: "Power I2C communication is abnormal.",
|
||||||
|
30: "Pool connection failed.",
|
||||||
|
31: "Individual chips are damaged.",
|
||||||
|
32: "Over temperature protection.",
|
||||||
|
33: "Hashboard fault.",
|
||||||
|
34: "The data cables are not connected in the correct order.",
|
||||||
|
35: "No power output.",
|
||||||
|
36: "Hashboard fault.",
|
||||||
|
37: "Control board and/or hashboard do not match.",
|
||||||
|
40: "Power output is abnormal.",
|
||||||
|
41: "Power output is abnormal.",
|
||||||
|
42: "Hashboard fault.",
|
||||||
|
}
|
||||||
@@ -1,18 +1,82 @@
|
|||||||
from dataclasses import dataclass, field, asdict
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, field, fields
|
||||||
|
|
||||||
|
C_N_CODES = ["52", "53", "54", "55"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WhatsminerError:
|
class WhatsminerError:
|
||||||
"""A Dataclass to handle error codes of Whatsminers."""
|
"""A Dataclass to handle error codes of Whatsminers.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error_code: The error code as an int.
|
||||||
|
error_message: The error message as a string. Automatically found from the error code.
|
||||||
|
"""
|
||||||
|
|
||||||
error_code: int
|
error_code: int
|
||||||
error_message: str = field(init=False)
|
error_message: str = field(init=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fields(cls):
|
||||||
|
return fields(cls)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def error_message(self): # noqa - Skip PyCharm inspection
|
def error_message(self): # noqa - Skip PyCharm inspection
|
||||||
if self.error_code in ERROR_CODES:
|
if len(str(self.error_code)) > 3 and str(self.error_code)[:2] in C_N_CODES:
|
||||||
return ERROR_CODES[self.error_code]
|
# 55 error code base has chip numbers, so the format is
|
||||||
return "Unknown error type."
|
# 55 -> board num len 1 -> chip num len 3
|
||||||
|
err_type = 55
|
||||||
|
err_subtype = int(str(self.error_code)[2:3])
|
||||||
|
err_value = int(str(self.error_code)[3:])
|
||||||
|
else:
|
||||||
|
err_type = int(str(self.error_code)[:-2])
|
||||||
|
err_subtype = int(str(self.error_code)[-2:-1])
|
||||||
|
err_value = int(str(self.error_code)[-1:])
|
||||||
|
try:
|
||||||
|
select_err_type = ERROR_CODES[err_type]
|
||||||
|
if err_subtype in select_err_type:
|
||||||
|
select_err_subtype = select_err_type[err_subtype]
|
||||||
|
if err_value in select_err_subtype:
|
||||||
|
return select_err_subtype[err_value]
|
||||||
|
elif "n" in select_err_subtype:
|
||||||
|
return select_err_subtype[
|
||||||
|
"n" # noqa: picks up `select_err_subtype["n"]` as not being numeric?
|
||||||
|
].replace("{n}", str(err_value))
|
||||||
|
else:
|
||||||
|
return "Unknown error type."
|
||||||
|
elif "n" in select_err_type:
|
||||||
|
select_err_subtype = select_err_type[
|
||||||
|
"n" # noqa: picks up `select_err_subtype["n"]` as not being numeric?
|
||||||
|
]
|
||||||
|
if err_value in select_err_subtype:
|
||||||
|
return select_err_subtype[err_value]
|
||||||
|
elif "c" in select_err_subtype:
|
||||||
|
return (
|
||||||
|
select_err_subtype["c"]
|
||||||
|
.replace( # noqa: picks up `select_err_subtype["n"]` as not being numeric?
|
||||||
|
"{n}", str(err_subtype)
|
||||||
|
)
|
||||||
|
.replace("{c}", str(err_value))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return "Unknown error type."
|
||||||
|
except KeyError:
|
||||||
|
return "Unknown error type."
|
||||||
|
|
||||||
@error_message.setter
|
@error_message.setter
|
||||||
def error_message(self, val):
|
def error_message(self, val):
|
||||||
@@ -23,130 +87,251 @@ class WhatsminerError:
|
|||||||
|
|
||||||
|
|
||||||
ERROR_CODES = {
|
ERROR_CODES = {
|
||||||
110: "Intake fan speed error.",
|
1: { # Fan error
|
||||||
111: "Exhaust fan speed error.",
|
0: {0: "Fan unknown."},
|
||||||
120: "Intake fan speed error. Fan speed deviates by more than 2000.",
|
1: { # Fan speed error of 1000+
|
||||||
121: "Exhaust fan speed error. Fan speed deviates by more than 2000.",
|
0: "Intake fan speed error.",
|
||||||
130: "Intake fan speed error. Fan speed deviates by more than 3000.",
|
1: "Exhaust fan speed error.",
|
||||||
131: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
|
},
|
||||||
140: "Fan speed too high.",
|
2: { # Fan speed error of 2000+
|
||||||
200: "Power probing error. No power found.",
|
0: "Intake fan speed error. Fan speed deviates by more than 2000.",
|
||||||
201: "Power supply and configuration file don't match.",
|
1: "Exhaust fan speed error. Fan speed deviates by more than 2000.",
|
||||||
202: "Power output voltage error.",
|
},
|
||||||
203: "Power protecting due to high environment temperature.",
|
3: { # Fan speed error of 3000+
|
||||||
204: "Power current protecting due to high environment temperature.",
|
0: "Intake fan speed error. Fan speed deviates by more than 3000.",
|
||||||
205: "Power current error.",
|
1: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
|
||||||
206: "Power input low voltage error.",
|
},
|
||||||
207: "Power input current protecting due to bad power input.",
|
4: {0: "Fan speed too high."}, # High speed
|
||||||
210: "Power error.",
|
},
|
||||||
213: "Power input voltage and current do not match power output.",
|
2: { # Power error
|
||||||
216: "Power remained unchanged for a long time.",
|
0: {
|
||||||
217: "Power set enable error.",
|
0: "Power probing error. No power found.",
|
||||||
218: "Power input voltage is lower than 230V for high power mode.",
|
1: "Power supply and configuration file don't match.",
|
||||||
233: "Power output high temperature protection error.",
|
2: "Power output voltage error.",
|
||||||
234: "Power output high temperature protection error.",
|
3: "Power protecting due to high environment temperature.",
|
||||||
235: "Power output high temperature protection error.",
|
4: "Power current protecting due to high environment temperature.",
|
||||||
236: "Power output high current protection error.",
|
5: "Power current error.",
|
||||||
237: "Power output high current protection error.",
|
6: "Power input low voltage error.",
|
||||||
238: "Power output high current protection error.",
|
7: "Power input current protecting due to bad power input.",
|
||||||
239: "Power output high voltage protection error.",
|
8: "Power power error.",
|
||||||
240: "Power output low voltage protection error.",
|
9: "Power voltage offset error.",
|
||||||
241: "Power output current imbalance error.",
|
},
|
||||||
243: "Power input high temperature protection error.",
|
1: {
|
||||||
244: "Power input high temperature protection error.",
|
0: "Power error.",
|
||||||
245: "Power input high temperature protection error.",
|
1: "Power iout error, please reboot.",
|
||||||
246: "Power input high voltage protection error.",
|
2: "Power vout error, reach vout border. Border: [1150, 1500]",
|
||||||
247: "Power input high voltage protection error.",
|
3: "Power input voltage and current do not match power output.",
|
||||||
248: "Power input high current protection error.",
|
4: "Power pin did not change.",
|
||||||
249: "Power input high current protection error.",
|
5: "Power vout set error.",
|
||||||
250: "Power input low voltage protection error.",
|
6: "Power remained unchanged for a long time.",
|
||||||
251: "Power input low voltage protection error.",
|
7: "Power set enable error.",
|
||||||
253: "Power supply fan error.",
|
8: "Power input voltage is lower than 230V for high power mode.",
|
||||||
254: "Power supply fan error.",
|
},
|
||||||
255: "Power output high power protection error.",
|
3: {
|
||||||
256: "Power output high power protection error.",
|
3: "Power output high temperature protection error.",
|
||||||
257: "Input over current protection of power supply on primary side.",
|
4: "Power output high temperature protection error.",
|
||||||
263: "Power communication warning.",
|
5: "Power output high temperature protection error.",
|
||||||
264: "Power communication error.",
|
6: "Power output high current protection error.",
|
||||||
267: "Power watchdog protection.",
|
7: "Power output high current protection error.",
|
||||||
268: "Power output high current protection.",
|
8: "Power output high current protection error.",
|
||||||
269: "Power input high current protection.",
|
9: "Power output high voltage protection error.",
|
||||||
270: "Power input high voltage protection.",
|
},
|
||||||
271: "Power input low voltage protection.",
|
4: {
|
||||||
272: "Excessive power supply output warning.",
|
0: "Power output low voltage protection error.",
|
||||||
273: "Power input too high warning.",
|
1: "Power output current imbalance error.",
|
||||||
274: "Power fan warning.",
|
3: "Power input high temperature protection error.",
|
||||||
275: "Power high temperature warning.",
|
4: "Power input high temperature protection error.",
|
||||||
300: "Right board temperature sensor detection error.",
|
5: "Power input high temperature protection error.",
|
||||||
301: "Center board temperature sensor detection error.",
|
6: "Power input high voltage protection error.",
|
||||||
302: "Left board temperature sensor detection error.",
|
7: "Power input high voltage protection error.",
|
||||||
320: "Right board temperature reading error.",
|
8: "Power input high current protection error.",
|
||||||
321: "Center board temperature reading error.",
|
9: "Power input high current protection error.",
|
||||||
322: "Left board temperature reading error.",
|
},
|
||||||
329: "Control board temperature sensor communication error.",
|
5: {
|
||||||
350: "Right board temperature protecting.",
|
0: "Power input low voltage protection error.",
|
||||||
351: "Center board temperature protecting.",
|
1: "Power input low voltage protection error.",
|
||||||
352: "Left board temperature protecting.",
|
3: "Power supply fan error.",
|
||||||
360: "Hashboard high temperature error.",
|
4: "Power supply fan error.",
|
||||||
410: "Right board eeprom detection error.",
|
5: "Power output high power protection error.",
|
||||||
411: "Center board eeprom detection error.",
|
6: "Power output high power protection error.",
|
||||||
412: "Left board eeprom detection error.",
|
7: "Input over current protection of power supply on primary side.",
|
||||||
420: "Right board eeprom parsing error.",
|
},
|
||||||
421: "Center board eeprom parsing error.",
|
6: {
|
||||||
422: "Left board eeprom parsing error.",
|
3: "Power communication warning.",
|
||||||
430: "Right board chip bin type error.",
|
4: "Power communication error.",
|
||||||
431: "Center board chip bin type error.",
|
7: "Power watchdog protection.",
|
||||||
432: "Left board chip bin type error.",
|
8: "Power output high current protection.",
|
||||||
440: "Right board eeprom chip number X error.",
|
9: "Power input high current protection.",
|
||||||
441: "Center board eeprom chip number X error.",
|
},
|
||||||
442: "Left board eeprom chip number X error.",
|
7: {
|
||||||
450: "Right board eeprom xfer error.",
|
0: "Power input high voltage protection.",
|
||||||
451: "Center board eeprom xfer error.",
|
1: "Power input low voltage protection.",
|
||||||
452: "Left board eeprom xfer error.",
|
2: "Excessive power supply output warning.",
|
||||||
510: "Right board miner type error.",
|
3: "Power input too high warning.",
|
||||||
511: "Center board miner type error.",
|
4: "Power fan warning.",
|
||||||
512: "Left board miner type error.",
|
5: "Power high temperature warning.",
|
||||||
520: "Right board bin type error.",
|
},
|
||||||
521: "Center board bin type error.",
|
},
|
||||||
522: "Left board bin type error.",
|
3: { # temperature error
|
||||||
530: "Right board not found.",
|
0: { # sensor detection error
|
||||||
531: "Center board not found.",
|
"n": "Slot {n} temperature sensor detection error."
|
||||||
532: "Left board not found.",
|
},
|
||||||
540: "Right board error reading chip id.",
|
2: { # temperature reading error
|
||||||
541: "Center board error reading chip id.",
|
"n": "Slot {n} temperature reading error.",
|
||||||
542: "Left board error reading chip id.",
|
9: "Control board temperature sensor communication error.",
|
||||||
550: "Right board has bad chips.",
|
},
|
||||||
551: "Center board has bad chips.",
|
5: {"n": "Slot {n} temperature protecting."}, # temperature protection
|
||||||
552: "Left board has bad chips.",
|
6: {0: "Hashboard high temperature error."}, # high temp
|
||||||
560: "Right board loss of balance error.",
|
8: {
|
||||||
561: "Center board loss of balance error.",
|
0: "Humidity sensor not found.",
|
||||||
562: "Left board loss of balance error.",
|
1: "Humidity sensor read error.",
|
||||||
600: "Environment temperature is too high.",
|
2: "Humidity sensor read error.",
|
||||||
610: "Environment temperature is too high for high performance mode.",
|
3: "Humidity sensor protecting.",
|
||||||
701: "Control board no support chip.",
|
},
|
||||||
710: "Control board rebooted as an exception.",
|
},
|
||||||
712: "Control board rebooted as an exception.",
|
4: { # EEPROM error
|
||||||
800: "CGMiner checksum error.",
|
0: {0: "Eeprom unknown error."},
|
||||||
801: "System monitor checksum error.",
|
1: {"n": "Slot {n} eeprom detection error."}, # EEPROM detection error
|
||||||
802: "Remote daemon checksum error.",
|
2: {"n": "Slot {n} eeprom parsing error."}, # EEPROM parsing error
|
||||||
2010: "All pools are disabled.",
|
3: {"n": "Slot {n} chip bin type error."}, # chip bin error
|
||||||
2020: "Pool 0 connection failed.",
|
4: {"n": "Slot {n} eeprom chip number X error."}, # EEPROM chip number error
|
||||||
2021: "Pool 1 connection failed.",
|
5: {"n": "Slot {n} eeprom xfer error."}, # EEPROM xfer error
|
||||||
2022: "Pool 2 connection failed.",
|
},
|
||||||
2030: "High rejection rate on pool.",
|
5: { # hashboard error
|
||||||
2040: "The pool does not support asicboost mode.",
|
0: {0: "Board unknown error."},
|
||||||
2310: "Hashrate is too low.",
|
1: {"n": "Slot {n} miner type error."}, # board miner type error
|
||||||
2320: "Hashrate is too low.",
|
2: {"n": "Slot {n} bin type error."}, # chip bin type error
|
||||||
2340: "Hashrate loss is too high.",
|
3: {"n": "Slot {n} not found."}, # board not found error
|
||||||
2350: "Hashrate loss is too high.",
|
4: {"n": "Slot {n} error reading chip id."}, # reading chip id error
|
||||||
5070: "Right hashboard water velocity is abnormal.",
|
5: {"n": "Slot {n} has bad chips."}, # board has bad chips error
|
||||||
5071: "Center hashboard water velocity is abnormal.",
|
6: {"n": "Slot {n} loss of balance error."}, # loss of balance error
|
||||||
5072: "Left hashboard water velocity is abnormal.",
|
7: {"n": "Slot {n} xfer error chip."}, # xfer error
|
||||||
5110: "Right hashboard frequency up timeout.",
|
8: {"n": "Slot {n} reset error."}, # reset error
|
||||||
5111: "Center hashboard frequency up timeout.",
|
9: {"n": "Slot {n} frequency too low."}, # freq error
|
||||||
5112: "Left hashboard frequency up timeout.",
|
},
|
||||||
8410: "Software version error.",
|
6: { # env temp error
|
||||||
100001: "/antiv/signature illegal.",
|
0: {0: "Environment temperature is too high."}, # normal env temp error
|
||||||
100002: "/antiv/dig/init.d illegal.",
|
1: { # high power env temp error
|
||||||
100003: "/antiv/dig/pf_partial.dig illegal.",
|
0: "Environment temperature is too high for high performance mode."
|
||||||
|
},
|
||||||
|
},
|
||||||
|
7: { # control board error
|
||||||
|
0: {0: "MAC address invalid", 1: "Control board no support chip."},
|
||||||
|
1: {
|
||||||
|
0: "Control board rebooted as an exception.",
|
||||||
|
1: "Control board rebooted as exception and cpufreq reduced, please upgrade the firmware",
|
||||||
|
2: "Control board rebooted as an exception.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
8: { # checksum error
|
||||||
|
0: {
|
||||||
|
0: "CGMiner checksum error.",
|
||||||
|
1: "System monitor checksum error.",
|
||||||
|
2: "Remote daemon checksum error.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
9: {0: {1: "Power rate error."}}, # power rate error
|
||||||
|
20: { # pool error
|
||||||
|
1: {0: "All pools are disabled."}, # all disabled error
|
||||||
|
2: {"n": "Pool {n} connection failed."}, # pool connection failed error
|
||||||
|
3: {0: "High rejection rate on pool."}, # rejection rate error
|
||||||
|
4: { # asicboost not supported error
|
||||||
|
0: "The pool does not support asicboost mode."
|
||||||
|
},
|
||||||
|
},
|
||||||
|
21: {1: {"n": "Slot {n} factory test step failed."}},
|
||||||
|
23: { # hashrate error
|
||||||
|
1: {0: "Hashrate is too low."},
|
||||||
|
2: {0: "Hashrate is too low."},
|
||||||
|
3: {0: "Hashrate loss is too high."},
|
||||||
|
4: {0: "Hashrate loss is too high."},
|
||||||
|
5: {0: "Hashrate loss."},
|
||||||
|
},
|
||||||
|
50: { # water velocity error/voltage error
|
||||||
|
1: {"n": "Slot {n} chip voltage too low."},
|
||||||
|
2: {"n": "Slot {n} chip voltage changed."},
|
||||||
|
3: {"n": "Slot {n} chip temperature difference is too large."},
|
||||||
|
4: {"n": "Slot {n} chip hottest temperature difference is too large."},
|
||||||
|
7: {"n": "Slot {n} water velocity is abnormal."}, # abnormal water velocity
|
||||||
|
8: {0: "Chip temp calibration failed, please restore factory settings."},
|
||||||
|
9: {"n": "Slot {n} chip temp calibration check no balance."},
|
||||||
|
},
|
||||||
|
51: { # frequency error
|
||||||
|
1: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
|
||||||
|
7: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
|
||||||
|
},
|
||||||
|
52: {"n": {"c": "Slot {n} chip {c} error nonce."}},
|
||||||
|
53: {"n": {"c": "Slot {n} chip {c} too few nonce."}},
|
||||||
|
54: {"n": {"c": "Slot {n} chip {c} temp protected."}},
|
||||||
|
55: {"n": {"c": "Slot {n} chip {c} has been reset."}},
|
||||||
|
80: {
|
||||||
|
0: {0: "The tool version is too low, please update."},
|
||||||
|
1: {0: "Low freq."},
|
||||||
|
2: {0: "Low hashrate."},
|
||||||
|
3: {5: "High env temp."},
|
||||||
|
},
|
||||||
|
81: {
|
||||||
|
0: {0: "Chip data error."},
|
||||||
|
},
|
||||||
|
82: {
|
||||||
|
0: {0: "Power version error."},
|
||||||
|
1: {0: "Miner type error."},
|
||||||
|
2: {0: "Version info error."},
|
||||||
|
},
|
||||||
|
83: {
|
||||||
|
0: {0: "Empty level error."},
|
||||||
|
},
|
||||||
|
84: {
|
||||||
|
0: {0: "Old firmware."},
|
||||||
|
1: {0: "Software version error."},
|
||||||
|
},
|
||||||
|
85: {
|
||||||
|
"n": {
|
||||||
|
0: "Hashrate substandard L{n}.",
|
||||||
|
1: "Power consumption substandard L{n}.",
|
||||||
|
2: "Fan speed substandard L{n}.",
|
||||||
|
3: "Fan speed substandard L{n}.",
|
||||||
|
4: "Voltage substandard L{n}.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
86: {
|
||||||
|
0: {0: "Missing product serial #."},
|
||||||
|
1: {0: "Missing product type."},
|
||||||
|
2: {
|
||||||
|
0: "Missing miner serial #.",
|
||||||
|
1: "Wrong miner serial # length.",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
0: "Missing power serial #.",
|
||||||
|
1: "Wrong power serial #.",
|
||||||
|
2: "Fault miner serial #.",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
0: "Missing power model.",
|
||||||
|
1: "Wrong power model name.",
|
||||||
|
2: "Wrong power model vout.",
|
||||||
|
3: "Wrong power model rate.",
|
||||||
|
4: "Wrong power model format.",
|
||||||
|
},
|
||||||
|
5: {0: "Wrong hash board struct."},
|
||||||
|
6: {0: "Wrong miner cooling type."},
|
||||||
|
7: {0: "Missing PCB serial #."},
|
||||||
|
},
|
||||||
|
87: {0: {0: "Miner power mismatch."}},
|
||||||
|
99: {9: {9: "Miner unknown error."}},
|
||||||
|
1000: {
|
||||||
|
0: {
|
||||||
|
0: "Security library error, please upgrade firmware",
|
||||||
|
1: "/antiv/signature illegal.",
|
||||||
|
2: "/antiv/dig/init.d illegal.",
|
||||||
|
3: "/antiv/dig/pf_partial.dig illegal.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1001: {0: {0: "Security BTMiner removed, please upgrade firmware."}},
|
||||||
|
1100: {
|
||||||
|
0: {
|
||||||
|
0: "Security illegal file, please upgrade firmware.",
|
||||||
|
1: "Security virus 0001 is removed, please upgrade firmware.",
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
59
pyasic/errors/__init__.py
Normal file
59
pyasic/errors/__init__.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(Exception):
|
||||||
|
def __init__(self, *args):
|
||||||
|
if args:
|
||||||
|
self.message = args[0]
|
||||||
|
else:
|
||||||
|
self.message = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.message:
|
||||||
|
if self.message == "can't access write cmd":
|
||||||
|
return f"{self.message}, please make sure your miner has been unlocked."
|
||||||
|
return f"{self.message}"
|
||||||
|
else:
|
||||||
|
return "Incorrect API parameters."
|
||||||
|
|
||||||
|
|
||||||
|
class PhaseBalancingError(Exception):
|
||||||
|
def __init__(self, *args):
|
||||||
|
if args:
|
||||||
|
self.message = args[0]
|
||||||
|
else:
|
||||||
|
self.message = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.message:
|
||||||
|
return f"{self.message}"
|
||||||
|
else:
|
||||||
|
return "Failed to balance phase."
|
||||||
|
|
||||||
|
|
||||||
|
class APIWarning(Warning):
|
||||||
|
def __init__(self, *args):
|
||||||
|
if args:
|
||||||
|
self.message = args[0]
|
||||||
|
else:
|
||||||
|
self.message = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.message:
|
||||||
|
return f"{self.message}"
|
||||||
|
else:
|
||||||
|
return "Incorrect API parameters."
|
||||||
336
pyasic/load/__init__.py
Normal file
336
pyasic/load/__init__.py
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
# from pyasic.errors import PhaseBalancingError
|
||||||
|
from pyasic.errors import APIError
|
||||||
|
from pyasic.miners import AnyMiner
|
||||||
|
from pyasic.miners._backends import X19, BOSMiner, BTMiner
|
||||||
|
from pyasic.miners._types import S9, S17, T17, S17e, S17Plus, S17Pro, T17e, T17Plus
|
||||||
|
|
||||||
|
# from pprint import pprint as print
|
||||||
|
|
||||||
|
|
||||||
|
FAN_USAGE = 50 # 50 W per fan
|
||||||
|
|
||||||
|
|
||||||
|
class MinerLoadBalancer:
|
||||||
|
"""A load balancer for miners. Can be passed a list of `AnyMiner`, or a list of phases (lists of `AnyMiner`)."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
phases: Union[List[List[AnyMiner]], None] = None,
|
||||||
|
):
|
||||||
|
self.phases = [_MinerPhaseBalancer(phase) for phase in phases]
|
||||||
|
|
||||||
|
async def balance(self, wattage: int) -> int:
|
||||||
|
phase_wattage = wattage // len(self.phases)
|
||||||
|
setpoints = await asyncio.gather(
|
||||||
|
*[phase.get_balance_setpoints(phase_wattage) for phase in self.phases]
|
||||||
|
)
|
||||||
|
tasks = []
|
||||||
|
total_wattage = 0
|
||||||
|
for setpoint in setpoints:
|
||||||
|
wattage_set = 0
|
||||||
|
for miner in setpoint:
|
||||||
|
if setpoint[miner]["set"] == "on":
|
||||||
|
wattage_set += setpoint[miner]["max"]
|
||||||
|
tasks.append(setpoint[miner]["miner"].resume_mining())
|
||||||
|
elif setpoint[miner]["set"] == "off":
|
||||||
|
wattage_set += setpoint[miner]["min"]
|
||||||
|
tasks.append(setpoint[miner]["miner"].stop_mining())
|
||||||
|
else:
|
||||||
|
wattage_set += setpoint[miner]["set"]
|
||||||
|
tasks.append(
|
||||||
|
setpoint[miner]["miner"].set_power_limit(setpoint[miner]["set"])
|
||||||
|
)
|
||||||
|
total_wattage += wattage_set
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
return total_wattage
|
||||||
|
|
||||||
|
|
||||||
|
class _MinerPhaseBalancer:
|
||||||
|
def __init__(self, miners: List[AnyMiner]):
|
||||||
|
self.miners = {
|
||||||
|
str(miner.ip): {
|
||||||
|
"miner": miner,
|
||||||
|
"set": 0,
|
||||||
|
"min": miner.fan_count * FAN_USAGE,
|
||||||
|
}
|
||||||
|
for miner in miners
|
||||||
|
}
|
||||||
|
for miner in miners:
|
||||||
|
if (
|
||||||
|
isinstance(miner, BTMiner)
|
||||||
|
and not (miner.model.startswith("M2") if miner.model else True)
|
||||||
|
) or isinstance(miner, BOSMiner):
|
||||||
|
if isinstance(miner, S9):
|
||||||
|
self.miners[str(miner.ip)]["tune"] = True
|
||||||
|
self.miners[str(miner.ip)]["shutdown"] = True
|
||||||
|
self.miners[str(miner.ip)]["max"] = 1400
|
||||||
|
elif True in [
|
||||||
|
isinstance(miner, x)
|
||||||
|
for x in [S17, S17Plus, S17Pro, S17e, T17, T17Plus, T17e]
|
||||||
|
]:
|
||||||
|
self.miners[str(miner.ip)]["tune"] = True
|
||||||
|
self.miners[str(miner.ip)]["shutdown"] = True
|
||||||
|
self.miners[str(miner.ip)]["max"] = 2400
|
||||||
|
else:
|
||||||
|
self.miners[str(miner.ip)]["tune"] = True
|
||||||
|
self.miners[str(miner.ip)]["shutdown"] = True
|
||||||
|
self.miners[str(miner.ip)]["max"] = 3600
|
||||||
|
elif isinstance(miner, X19):
|
||||||
|
self.miners[str(miner.ip)]["tune"] = False
|
||||||
|
self.miners[str(miner.ip)]["shutdown"] = True
|
||||||
|
self.miners[str(miner.ip)]["max"] = 3600
|
||||||
|
elif isinstance(miner, BTMiner):
|
||||||
|
self.miners[str(miner.ip)]["tune"] = False
|
||||||
|
self.miners[str(miner.ip)]["shutdown"] = True
|
||||||
|
self.miners[str(miner.ip)]["max"] = 3600
|
||||||
|
if miner.model:
|
||||||
|
if miner.model.startswith("M2"):
|
||||||
|
self.miners[str(miner.ip)]["tune"] = False
|
||||||
|
self.miners[str(miner.ip)]["shutdown"] = True
|
||||||
|
self.miners[str(miner.ip)]["max"] = 2400
|
||||||
|
else:
|
||||||
|
self.miners[str(miner.ip)]["tune"] = False
|
||||||
|
self.miners[str(miner.ip)]["shutdown"] = False
|
||||||
|
self.miners[str(miner.ip)]["max"] = 3600
|
||||||
|
self.miners[str(miner.ip)]["min"] = 3600
|
||||||
|
|
||||||
|
async def balance(self, wattage: int) -> int:
|
||||||
|
setpoint = await self.get_balance_setpoints(wattage)
|
||||||
|
wattage_set = 0
|
||||||
|
tasks = []
|
||||||
|
for miner in setpoint:
|
||||||
|
if setpoint[miner]["set"] == "on":
|
||||||
|
wattage_set += setpoint[miner]["max"]
|
||||||
|
tasks.append(setpoint[miner]["miner"].resume_mining())
|
||||||
|
elif setpoint[miner]["set"] == "off":
|
||||||
|
wattage_set += setpoint[miner]["min"]
|
||||||
|
tasks.append(setpoint[miner]["miner"].stop_mining())
|
||||||
|
else:
|
||||||
|
wattage_set += setpoint[miner]["set"]
|
||||||
|
tasks.append(
|
||||||
|
setpoint[miner]["miner"].set_power_limit(setpoint[miner]["set"])
|
||||||
|
)
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
return wattage_set
|
||||||
|
|
||||||
|
async def get_balance_setpoints(self, wattage: int) -> dict:
|
||||||
|
# gather data needed to optimize shutdown only miners
|
||||||
|
dp = ["hashrate", "wattage", "wattage_limit"]
|
||||||
|
data = await asyncio.gather(
|
||||||
|
*[
|
||||||
|
self.miners[miner]["miner"].get_data(data_to_get=dp)
|
||||||
|
for miner in self.miners
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for data_point in data:
|
||||||
|
if (not self.miners[data_point.ip]["tune"]) and (
|
||||||
|
not self.miners[data_point.ip]["shutdown"]
|
||||||
|
):
|
||||||
|
# cant do anything with it so need to find a semi-accurate power limit
|
||||||
|
if not data_point.wattage_limit == -1:
|
||||||
|
self.miners[data_point.ip]["max"] = int(data_point.wattage_limit)
|
||||||
|
self.miners[data_point.ip]["min"] = int(data_point.wattage_limit)
|
||||||
|
elif not data_point.wattage == -1:
|
||||||
|
self.miners[data_point.ip]["max"] = int(data_point.wattage)
|
||||||
|
self.miners[data_point.ip]["min"] = int(data_point.wattage)
|
||||||
|
|
||||||
|
max_tune_wattage = sum(
|
||||||
|
[miner["max"] for miner in self.miners.values() if miner["tune"]]
|
||||||
|
)
|
||||||
|
max_shutdown_wattage = sum(
|
||||||
|
[
|
||||||
|
miner["max"]
|
||||||
|
for miner in self.miners.values()
|
||||||
|
if (not miner["tune"]) and (miner["shutdown"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
max_other_wattage = sum(
|
||||||
|
[
|
||||||
|
miner["max"]
|
||||||
|
for miner in self.miners.values()
|
||||||
|
if (not miner["tune"]) and (not miner["shutdown"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
min_tune_wattage = sum(
|
||||||
|
[miner["min"] for miner in self.miners.values() if miner["tune"]]
|
||||||
|
)
|
||||||
|
min_shutdown_wattage = sum(
|
||||||
|
[
|
||||||
|
miner["min"]
|
||||||
|
for miner in self.miners.values()
|
||||||
|
if (not miner["tune"]) and (miner["shutdown"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# min_other_wattage = sum([miner["min"] for miner in self.miners.values() if (not miner["tune"]) and (not miner["shutdown"])])
|
||||||
|
|
||||||
|
# make sure wattage isnt set too high
|
||||||
|
if wattage > (max_tune_wattage + max_shutdown_wattage + max_other_wattage):
|
||||||
|
raise APIError(
|
||||||
|
f"Wattage setpoint is too high, setpoint: {wattage}W, max: {max_tune_wattage + max_shutdown_wattage + max_other_wattage}W"
|
||||||
|
) # PhaseBalancingError(f"Wattage setpoint is too high, setpoint: {wattage}W, max: {max_tune_wattage + max_shutdown_wattage + max_other_wattage}W")
|
||||||
|
|
||||||
|
# should now know wattage limits and which can be tuned/shutdown
|
||||||
|
# check if 1/2 max of the miners which can be tuned is low enough
|
||||||
|
if (max_tune_wattage / 2) + max_shutdown_wattage + max_other_wattage < wattage:
|
||||||
|
useable_wattage = wattage - (max_other_wattage + max_shutdown_wattage)
|
||||||
|
useable_miners = len(
|
||||||
|
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
|
||||||
|
)
|
||||||
|
if not useable_miners == 0:
|
||||||
|
watts_per_miner = useable_wattage // useable_miners
|
||||||
|
# loop through and set useable miners to wattage
|
||||||
|
for miner in self.miners:
|
||||||
|
if (self.miners[miner]["set"] == 0) and (
|
||||||
|
self.miners[miner]["tune"]
|
||||||
|
):
|
||||||
|
self.miners[miner]["set"] = watts_per_miner
|
||||||
|
elif self.miners[miner]["set"] == 0 and (
|
||||||
|
self.miners[miner]["shutdown"]
|
||||||
|
):
|
||||||
|
self.miners[miner]["set"] = "on"
|
||||||
|
|
||||||
|
# check if shutting down miners will help
|
||||||
|
elif (
|
||||||
|
max_tune_wattage / 2
|
||||||
|
) + min_shutdown_wattage + max_other_wattage < wattage:
|
||||||
|
# tuneable inclusive since could be S9 BOS+ and S19 Stock, would rather shut down the S9, tuneable should always support shutdown
|
||||||
|
useable_wattage = wattage - (
|
||||||
|
min_tune_wattage + max_other_wattage + min_shutdown_wattage
|
||||||
|
)
|
||||||
|
for miner in sorted(
|
||||||
|
[miner for miner in self.miners.values() if miner["shutdown"]],
|
||||||
|
key=lambda x: x["max"],
|
||||||
|
reverse=True,
|
||||||
|
):
|
||||||
|
if miner["tune"]:
|
||||||
|
miner_min_watt_use = miner["max"] / 2
|
||||||
|
useable_wattage -= miner_min_watt_use - miner["min"]
|
||||||
|
if useable_wattage < 0:
|
||||||
|
useable_wattage += miner_min_watt_use - miner["min"]
|
||||||
|
self.miners[str(miner["miner"].ip)]["set"] = "off"
|
||||||
|
else:
|
||||||
|
miner_min_watt_use = miner["max"]
|
||||||
|
useable_wattage -= miner_min_watt_use - miner["min"]
|
||||||
|
if useable_wattage < 0:
|
||||||
|
useable_wattage += miner_min_watt_use - miner["min"]
|
||||||
|
self.miners[str(miner["miner"].ip)]["set"] = "off"
|
||||||
|
|
||||||
|
new_shutdown_wattage = sum(
|
||||||
|
[
|
||||||
|
miner["max"] if miner["set"] == 0 else miner["min"]
|
||||||
|
for miner in self.miners.values()
|
||||||
|
if miner["shutdown"] and not miner["tune"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
new_tune_wattage = sum(
|
||||||
|
[
|
||||||
|
miner["min"]
|
||||||
|
for miner in self.miners.values()
|
||||||
|
if miner["tune"] and miner["set"] == "off"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
useable_wattage = wattage - (
|
||||||
|
new_tune_wattage + max_other_wattage + new_shutdown_wattage
|
||||||
|
)
|
||||||
|
useable_miners = len(
|
||||||
|
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not useable_miners == 0:
|
||||||
|
watts_per_miner = useable_wattage // useable_miners
|
||||||
|
# loop through and set useable miners to wattage
|
||||||
|
for miner in self.miners:
|
||||||
|
if (self.miners[miner]["set"] == 0) and (
|
||||||
|
self.miners[miner]["tune"]
|
||||||
|
):
|
||||||
|
self.miners[miner]["set"] = watts_per_miner
|
||||||
|
elif self.miners[miner]["set"] == 0 and (
|
||||||
|
self.miners[miner]["shutdown"]
|
||||||
|
):
|
||||||
|
self.miners[miner]["set"] = "on"
|
||||||
|
|
||||||
|
# check if shutting down tuneable miners will do it
|
||||||
|
elif min_tune_wattage + min_shutdown_wattage + max_other_wattage < wattage:
|
||||||
|
# all miners that can be shutdown need to be
|
||||||
|
for miner in self.miners:
|
||||||
|
if (not self.miners[miner]["tune"]) and (
|
||||||
|
self.miners[miner]["shutdown"]
|
||||||
|
):
|
||||||
|
self.miners[miner]["set"] = "off"
|
||||||
|
# calculate wattage usable by tuneable miners
|
||||||
|
useable_wattage = wattage - (
|
||||||
|
min_tune_wattage + max_other_wattage + min_shutdown_wattage
|
||||||
|
)
|
||||||
|
|
||||||
|
# loop through miners to see how much is actually useable
|
||||||
|
# sort the largest first
|
||||||
|
for miner in sorted(
|
||||||
|
[
|
||||||
|
miner
|
||||||
|
for miner in self.miners.values()
|
||||||
|
if miner["tune"] and miner["shutdown"]
|
||||||
|
],
|
||||||
|
key=lambda x: x["max"],
|
||||||
|
reverse=True,
|
||||||
|
):
|
||||||
|
# add min to useable wattage since it was removed earlier, and remove 1/2 tuner max
|
||||||
|
useable_wattage -= (miner["max"] / 2) - miner["min"]
|
||||||
|
if useable_wattage < 0:
|
||||||
|
useable_wattage += (miner["max"] / 2) - miner["min"]
|
||||||
|
self.miners[str(miner["miner"].ip)]["set"] = "off"
|
||||||
|
|
||||||
|
new_tune_wattage = sum(
|
||||||
|
[
|
||||||
|
miner["min"]
|
||||||
|
for miner in self.miners.values()
|
||||||
|
if miner["tune"] and miner["set"] == "off"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
useable_wattage = wattage - (
|
||||||
|
new_tune_wattage + max_other_wattage + min_shutdown_wattage
|
||||||
|
)
|
||||||
|
useable_miners = len(
|
||||||
|
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not useable_miners == 0:
|
||||||
|
watts_per_miner = useable_wattage // useable_miners
|
||||||
|
# loop through and set useable miners to wattage
|
||||||
|
for miner in self.miners:
|
||||||
|
if (self.miners[miner]["set"] == 0) and (
|
||||||
|
self.miners[miner]["tune"]
|
||||||
|
):
|
||||||
|
self.miners[miner]["set"] = watts_per_miner
|
||||||
|
elif self.miners[miner]["set"] == 0 and (
|
||||||
|
self.miners[miner]["shutdown"]
|
||||||
|
):
|
||||||
|
self.miners[miner]["set"] = "on"
|
||||||
|
else:
|
||||||
|
raise APIError(
|
||||||
|
f"Wattage setpoint is too low, setpoint: {wattage}W, min: {min_tune_wattage + min_shutdown_wattage + max_other_wattage}W"
|
||||||
|
) # PhaseBalancingError(f"Wattage setpoint is too low, setpoint: {wattage}W, min: {min_tune_wattage + min_shutdown_wattage + max_other_wattage}W")
|
||||||
|
|
||||||
|
return self.miners
|
||||||
@@ -1,9 +1,26 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pyasic.settings import DEBUG, LOGFILE
|
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
|
||||||
def init_logger():
|
def init_logger():
|
||||||
if LOGFILE:
|
if PyasicSettings().logfile:
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
filename="logfile.txt",
|
filename="logfile.txt",
|
||||||
filemode="a",
|
filemode="a",
|
||||||
@@ -18,7 +35,7 @@ def init_logger():
|
|||||||
|
|
||||||
_logger = logging.getLogger()
|
_logger = logging.getLogger()
|
||||||
|
|
||||||
if DEBUG:
|
if PyasicSettings().debug:
|
||||||
_logger.setLevel(logging.DEBUG)
|
_logger.setLevel(logging.DEBUG)
|
||||||
logging.getLogger("asyncssh").setLevel(logging.DEBUG)
|
logging.getLogger("asyncssh").setLevel(logging.DEBUG)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,104 +1,26 @@
|
|||||||
import asyncssh
|
# ------------------------------------------------------------------------------
|
||||||
import logging
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from pyasic.data import MinerData
|
from pyasic.miners.base import AnyMiner, BaseMiner
|
||||||
|
from pyasic.miners.miner_factory import MinerFactory
|
||||||
|
|
||||||
|
|
||||||
class BaseMiner:
|
# abstracted version of get miner that is easier to access
|
||||||
def __init__(self, *args) -> None:
|
async def get_miner(ip: Union[ipaddress.ip_address, str]) -> AnyMiner:
|
||||||
self.ip = None
|
return await MinerFactory().get_miner(ip)
|
||||||
self.uname = "root"
|
|
||||||
self.pwd = "admin"
|
|
||||||
self.api = None
|
|
||||||
self.api_type = None
|
|
||||||
self.model = None
|
|
||||||
self.light = None
|
|
||||||
self.hostname = None
|
|
||||||
self.nominal_chips = 1
|
|
||||||
self.version = None
|
|
||||||
self.fan_count = 2
|
|
||||||
self.config = None
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
|
|
||||||
|
|
||||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
|
||||||
"""Create a new asyncssh connection"""
|
|
||||||
try:
|
|
||||||
conn = await asyncssh.connect(
|
|
||||||
str(self.ip),
|
|
||||||
known_hosts=None,
|
|
||||||
username=self.uname,
|
|
||||||
password=self.pwd,
|
|
||||||
server_host_key_algs=["ssh-rsa"],
|
|
||||||
)
|
|
||||||
return conn
|
|
||||||
except asyncssh.misc.PermissionDenied:
|
|
||||||
try:
|
|
||||||
conn = await asyncssh.connect(
|
|
||||||
str(self.ip),
|
|
||||||
known_hosts=None,
|
|
||||||
username="root",
|
|
||||||
password="admin",
|
|
||||||
server_host_key_algs=["ssh-rsa"],
|
|
||||||
)
|
|
||||||
return conn
|
|
||||||
except Exception as e:
|
|
||||||
# logging.warning(f"{self} raised an exception: {e}")
|
|
||||||
raise e
|
|
||||||
except OSError as e:
|
|
||||||
logging.warning(f"Connection refused: {self}")
|
|
||||||
raise e
|
|
||||||
except Exception as e:
|
|
||||||
# logging.warning(f"{self} raised an exception: {e}")
|
|
||||||
raise e
|
|
||||||
|
|
||||||
async def fault_light_on(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def fault_light_off(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def send_file(self, src, dest):
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
await asyncssh.scp(src, (conn, dest))
|
|
||||||
|
|
||||||
async def check_light(self):
|
|
||||||
return self.light
|
|
||||||
|
|
||||||
async def get_board_info(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_hostname(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_model(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def restart_backend(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def send_config(self, *args, **kwargs):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_mac(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_data(self) -> MinerData:
|
|
||||||
return MinerData(ip=str(self.ip))
|
|
||||||
|
|||||||
234
pyasic/miners/_backends/X19.py
Normal file
234
pyasic/miners/_backends/X19.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from pyasic.API import APIError
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
from pyasic.data.error_codes import MinerErrorData, X19Error
|
||||||
|
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
|
||||||
|
class X19(BMMiner):
|
||||||
|
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
|
||||||
|
super().__init__(ip, api_ver=api_ver)
|
||||||
|
self.ip = ip
|
||||||
|
self.uname = "root"
|
||||||
|
self.pwd = PyasicSettings().global_x19_password
|
||||||
|
|
||||||
|
async def send_web_command(
|
||||||
|
self, command: str, params: dict = None
|
||||||
|
) -> Optional[dict]:
|
||||||
|
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
|
||||||
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
if params:
|
||||||
|
data = await client.post(url, data=params, auth=auth)
|
||||||
|
else:
|
||||||
|
data = await client.get(url, auth=auth)
|
||||||
|
except httpx.HTTPError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if data.status_code == 200:
|
||||||
|
try:
|
||||||
|
return data.json()
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_config(self) -> MinerConfig:
|
||||||
|
data = await self.send_web_command("get_miner_conf")
|
||||||
|
if data:
|
||||||
|
self.config = MinerConfig().from_raw(data)
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
self.config = config
|
||||||
|
conf = config.as_x19(user_suffix=user_suffix)
|
||||||
|
await self.send_web_command(
|
||||||
|
"set_miner_conf", params=conf # noqa: ignore conf being a str
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(7):
|
||||||
|
data = await self.get_config()
|
||||||
|
if data.as_x19() == conf:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
data = await self.send_web_command(
|
||||||
|
"blink",
|
||||||
|
params=json.dumps({"blink": "true"}), # noqa - ignore params being a str
|
||||||
|
)
|
||||||
|
if data:
|
||||||
|
if data.get("code") == "B000":
|
||||||
|
self.light = True
|
||||||
|
return self.light
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
data = await self.send_web_command(
|
||||||
|
"blink",
|
||||||
|
params=json.dumps({"blink": "false"}), # noqa - ignore params being a str
|
||||||
|
)
|
||||||
|
if data:
|
||||||
|
if data.get("code") == "B100":
|
||||||
|
self.light = True
|
||||||
|
return self.light
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
data = await self.send_web_command("reboot")
|
||||||
|
if data:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def stop_mining(self) -> bool:
|
||||||
|
cfg = await self.get_config()
|
||||||
|
cfg.autotuning_wattage = 0
|
||||||
|
await self.send_config(cfg)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def resume_mining(self) -> bool:
|
||||||
|
cfg = await self.get_config()
|
||||||
|
cfg.autotuning_wattage = 3600
|
||||||
|
await self.send_config(cfg)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def get_hostname(self) -> Union[str, None]:
|
||||||
|
try:
|
||||||
|
data = await self.send_web_command("get_system_info")
|
||||||
|
if data:
|
||||||
|
return data["hostname"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_mac(self) -> Union[str, None]:
|
||||||
|
try:
|
||||||
|
data = await self.send_web_command("get_system_info")
|
||||||
|
if data:
|
||||||
|
return data["macaddr"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await self.send_web_command("get_network_info")
|
||||||
|
if data:
|
||||||
|
return data["macaddr"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_errors(self) -> List[MinerErrorData]:
|
||||||
|
errors = []
|
||||||
|
data = await self.send_web_command("summary")
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
for item in data["SUMMARY"][0]["status"]:
|
||||||
|
try:
|
||||||
|
if not item["status"] == "s":
|
||||||
|
errors.append(X19Error(item["msg"]))
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
return errors
|
||||||
|
|
||||||
|
async def get_fault_light(self) -> bool:
|
||||||
|
if self.light:
|
||||||
|
return self.light
|
||||||
|
try:
|
||||||
|
data = await self.send_web_command("get_blink_status")
|
||||||
|
if data:
|
||||||
|
self.light = data["blink"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return self.light
|
||||||
|
|
||||||
|
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
|
||||||
|
try:
|
||||||
|
rate_unit = api_stats["STATS"][1]["rate_unit"]
|
||||||
|
except KeyError:
|
||||||
|
rate_unit = "GH"
|
||||||
|
if rate_unit == "GH":
|
||||||
|
return round(ideal_rate / 1000, 2)
|
||||||
|
if rate_unit == "MH":
|
||||||
|
return round(ideal_rate / 1000000, 2)
|
||||||
|
else:
|
||||||
|
return round(ideal_rate, 2)
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def set_static_ip(
|
||||||
|
self,
|
||||||
|
ip: str,
|
||||||
|
dns: str,
|
||||||
|
gateway: str,
|
||||||
|
subnet_mask: str = "255.255.255.0",
|
||||||
|
hostname: str = None,
|
||||||
|
):
|
||||||
|
if not hostname:
|
||||||
|
hostname = await self.get_hostname()
|
||||||
|
payload = {
|
||||||
|
"ipAddress": ip,
|
||||||
|
"ipDns": dns,
|
||||||
|
"ipGateway": gateway,
|
||||||
|
"ipHost": hostname,
|
||||||
|
"ipPro": 2, # static
|
||||||
|
"ipSub": subnet_mask,
|
||||||
|
}
|
||||||
|
await self.send_web_command("set_network_conf", params=payload)
|
||||||
|
|
||||||
|
async def set_dhcp(self, hostname: str = None):
|
||||||
|
if not hostname:
|
||||||
|
hostname = await self.get_hostname()
|
||||||
|
payload = {
|
||||||
|
"ipAddress": "",
|
||||||
|
"ipDns": "",
|
||||||
|
"ipGateway": "",
|
||||||
|
"ipHost": hostname,
|
||||||
|
"ipPro": 1, # DHCP
|
||||||
|
"ipSub": "",
|
||||||
|
}
|
||||||
|
await self.send_web_command("set_network_conf", params=payload)
|
||||||
|
|
||||||
|
async def set_hostname(self, hostname: str):
|
||||||
|
cfg = await self.send_web_command("get_network_info")
|
||||||
|
dns = cfg["conf_dnsservers"]
|
||||||
|
gateway = cfg["conf_gateway"]
|
||||||
|
ip = cfg["conf_ipaddress"]
|
||||||
|
subnet_mask = cfg["conf_netmask"]
|
||||||
|
protocol = 1 if cfg["conf_nettype"] == "DHCP" else 2
|
||||||
|
payload = {
|
||||||
|
"ipAddress": ip,
|
||||||
|
"ipDns": dns,
|
||||||
|
"ipGateway": gateway,
|
||||||
|
"ipHost": hostname,
|
||||||
|
"ipPro": protocol,
|
||||||
|
"ipSub": subnet_mask,
|
||||||
|
}
|
||||||
|
await self.send_web_command("set_network_conf", params=payload)
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .bmminer import BMMiner
|
from .bmminer import BMMiner
|
||||||
from .bosminer import BOSMiner
|
from .bosminer import BOSMiner
|
||||||
from .btminer import BTMiner
|
from .btminer import BTMiner
|
||||||
from .cgminer import CGMiner
|
from .cgminer import CGMiner
|
||||||
|
from .cgminer_avalon import CGMinerAvalon
|
||||||
from .hiveon import Hiveon
|
from .hiveon import Hiveon
|
||||||
|
from .vnish import VNish
|
||||||
|
from .X19 import X19
|
||||||
|
|||||||
@@ -1,88 +1,57 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
|
from collections import namedtuple
|
||||||
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import asyncssh
|
||||||
|
|
||||||
from pyasic.API.bmminer import BMMinerAPI
|
from pyasic.API.bmminer import BMMinerAPI
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.config import MinerConfig
|
||||||
|
from pyasic.data import Fan, HashBoard, MinerData
|
||||||
from pyasic.data import MinerData
|
from pyasic.data.error_codes import MinerErrorData
|
||||||
|
from pyasic.errors import APIError
|
||||||
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
from pyasic.miners.base import BaseMiner
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
|
||||||
class BMMiner(BaseMiner):
|
class BMMiner(BaseMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
"""Base handler for BMMiner based miners."""
|
||||||
|
|
||||||
|
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
|
||||||
super().__init__(ip)
|
super().__init__(ip)
|
||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ipaddress.ip_address(ip)
|
||||||
self.api = BMMinerAPI(ip)
|
self.api = BMMinerAPI(ip, api_ver)
|
||||||
self.api_type = "BMMiner"
|
self.api_type = "BMMiner"
|
||||||
|
self.api_ver = api_ver
|
||||||
self.uname = "root"
|
self.uname = "root"
|
||||||
self.pwd = "admin"
|
self.pwd = "admin"
|
||||||
|
|
||||||
async def get_model(self) -> str or None:
|
async def send_ssh_command(self, cmd: str) -> Optional[str]:
|
||||||
"""Get miner model.
|
|
||||||
|
|
||||||
:return: Miner model or None.
|
|
||||||
"""
|
|
||||||
# check if model is cached
|
|
||||||
if self.model:
|
|
||||||
logging.debug(f"Found model for {self.ip}: {self.model}")
|
|
||||||
return self.model
|
|
||||||
|
|
||||||
# get devdetails data
|
|
||||||
version_data = await self.api.devdetails()
|
|
||||||
|
|
||||||
# if we get data back, parse it for model
|
|
||||||
if version_data:
|
|
||||||
# handle Antminer BMMiner as a base
|
|
||||||
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
|
|
||||||
logging.debug(f"Found model for {self.ip}: {self.model}")
|
|
||||||
return self.model
|
|
||||||
|
|
||||||
# if we don't get devdetails, log a failed attempt
|
|
||||||
logging.warning(f"Failed to get model for miner: {self}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
"""Get miner hostname.
|
|
||||||
|
|
||||||
:return: The hostname of the miner as a string or "?"
|
|
||||||
"""
|
|
||||||
if self.hostname:
|
|
||||||
return self.hostname
|
|
||||||
try:
|
|
||||||
# open an ssh connection
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
# if we get the connection, check hostname
|
|
||||||
if conn is not None:
|
|
||||||
# get output of the hostname file
|
|
||||||
data = await conn.run("cat /proc/sys/kernel/hostname")
|
|
||||||
host = data.stdout.strip()
|
|
||||||
|
|
||||||
# return hostname data
|
|
||||||
logging.debug(f"Found hostname for {self.ip}: {host}")
|
|
||||||
self.hostname = host
|
|
||||||
return self.hostname
|
|
||||||
else:
|
|
||||||
# return ? if we fail to get hostname with no ssh connection
|
|
||||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
|
||||||
return "?"
|
|
||||||
except Exception:
|
|
||||||
# return ? if we fail to get hostname with an exception
|
|
||||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
|
||||||
return "?"
|
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd: str) -> str or None:
|
|
||||||
"""Send a command to the miner over ssh.
|
|
||||||
|
|
||||||
:param cmd: The command to run.
|
|
||||||
|
|
||||||
:return: Result of the command or None.
|
|
||||||
"""
|
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = await self._get_ssh_connection()
|
||||||
|
except (asyncssh.Error, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
# open an ssh connection
|
# open an ssh connection
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
async with conn:
|
||||||
# 3 retries
|
# 3 retries
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
try:
|
try:
|
||||||
@@ -101,23 +70,15 @@ class BMMiner(BaseMiner):
|
|||||||
# return the result, either command output or None
|
# return the result, either command output or None
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def get_config(self) -> list or None:
|
async def get_config(self) -> MinerConfig:
|
||||||
"""Get the pool configuration of the miner.
|
|
||||||
|
|
||||||
:return: Pool config data or None.
|
|
||||||
"""
|
|
||||||
# get pool data
|
# get pool data
|
||||||
pools = await self.api.pools()
|
try:
|
||||||
pool_data = []
|
pools = await self.api.pools()
|
||||||
|
except APIError:
|
||||||
|
return self.config
|
||||||
|
|
||||||
# ensure we got pool data
|
self.config = MinerConfig().from_api(pools["POOLS"])
|
||||||
if not pools:
|
return self.config
|
||||||
return
|
|
||||||
|
|
||||||
# parse all the pools
|
|
||||||
for pool in pools["POOLS"]:
|
|
||||||
pool_data.append({"url": pool["URL"], "user": pool["User"], "pwd": "123"})
|
|
||||||
return pool_data
|
|
||||||
|
|
||||||
async def reboot(self) -> bool:
|
async def reboot(self) -> bool:
|
||||||
logging.debug(f"{self}: Sending reboot command.")
|
logging.debug(f"{self}: Sending reboot command.")
|
||||||
@@ -127,52 +88,143 @@ class BMMiner(BaseMiner):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_data(self) -> MinerData:
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
return None
|
||||||
|
|
||||||
board_offset = -1
|
async def fault_light_off(self) -> bool:
|
||||||
fan_offset = -1
|
return False
|
||||||
|
|
||||||
model = await self.get_model()
|
async def fault_light_on(self) -> bool:
|
||||||
hostname = await self.get_hostname()
|
return False
|
||||||
mac = await self.get_mac()
|
|
||||||
|
|
||||||
if model:
|
async def restart_backend(self) -> bool:
|
||||||
data.model = model
|
return False
|
||||||
|
|
||||||
if hostname:
|
async def stop_mining(self) -> bool:
|
||||||
data.hostname = hostname
|
return False
|
||||||
|
|
||||||
if mac:
|
async def resume_mining(self) -> bool:
|
||||||
data.mac = mac
|
return False
|
||||||
|
|
||||||
miner_data = None
|
async def set_power_limit(self, wattage: int) -> bool:
|
||||||
for i in range(DATA_RETRIES):
|
return False
|
||||||
miner_data = await self.api.multicommand(
|
|
||||||
"summary", "pools", "stats", ignore_x19_error=True
|
|
||||||
)
|
|
||||||
if miner_data:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not miner_data:
|
##################################################
|
||||||
return data
|
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||||
|
##################################################
|
||||||
|
|
||||||
summary = miner_data.get("summary")[0]
|
async def get_mac(self) -> str:
|
||||||
pools = miner_data.get("pools")[0]
|
return "00:00:00:00:00:00"
|
||||||
stats = miner_data.get("stats")[0]
|
|
||||||
|
|
||||||
if summary:
|
async def get_model(self, api_devdetails: dict = None) -> Optional[str]:
|
||||||
hr = summary.get("SUMMARY")
|
if self.model:
|
||||||
if hr:
|
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||||
if len(hr) > 0:
|
return self.model
|
||||||
hr = hr[0].get("GHS av")
|
|
||||||
if hr:
|
|
||||||
data.hashrate = round(hr / 1000, 2)
|
|
||||||
|
|
||||||
if stats:
|
if not api_devdetails:
|
||||||
boards = stats.get("STATS")
|
try:
|
||||||
if boards:
|
api_devdetails = await self.api.devdetails()
|
||||||
if len(boards) > 0:
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_devdetails:
|
||||||
|
try:
|
||||||
|
self.model = api_devdetails["DEVDETAILS"][0]["Model"].replace(
|
||||||
|
"Antminer ", ""
|
||||||
|
)
|
||||||
|
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||||
|
return self.model
|
||||||
|
except (TypeError, IndexError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
logging.warning(f"Failed to get model for miner: {self}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_api_ver(self, api_version: dict = None) -> Optional[str]:
|
||||||
|
# Check to see if the version info is already cached
|
||||||
|
if self.api_ver:
|
||||||
|
return self.api_ver
|
||||||
|
|
||||||
|
if not api_version:
|
||||||
|
try:
|
||||||
|
api_version = await self.api.version()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_version:
|
||||||
|
try:
|
||||||
|
self.api_ver = api_version["VERSION"][0]["API"]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self.api_ver
|
||||||
|
|
||||||
|
async def get_fw_ver(self, api_version: dict = None) -> Optional[str]:
|
||||||
|
# Check to see if the version info is already cached
|
||||||
|
if self.fw_ver:
|
||||||
|
return self.fw_ver
|
||||||
|
|
||||||
|
if not api_version:
|
||||||
|
try:
|
||||||
|
api_version = await self.api.version()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_version:
|
||||||
|
try:
|
||||||
|
self.fw_ver = api_version["VERSION"][0]["CompileTime"]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self.fw_ver
|
||||||
|
|
||||||
|
async def get_version(
|
||||||
|
self, api_version: dict = None
|
||||||
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
# check if version is cached
|
||||||
|
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
|
||||||
|
return miner_version(
|
||||||
|
api_ver=await self.get_api_ver(api_version),
|
||||||
|
fw_ver=await self.get_fw_ver(api_version=api_version),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_fan_psu(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_hostname(self) -> Optional[str]:
|
||||||
|
hn = await self.send_ssh_command("cat /proc/sys/kernel/hostname")
|
||||||
|
if hn:
|
||||||
|
self.hostname = hn
|
||||||
|
return self.hostname
|
||||||
|
|
||||||
|
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
|
||||||
|
# get hr from API
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
return round(float(api_summary["SUMMARY"][0]["GHS 5s"] / 1000), 2)
|
||||||
|
except (IndexError, KeyError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
|
||||||
|
hashboards = []
|
||||||
|
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
board_offset = -1
|
||||||
|
boards = api_stats["STATS"]
|
||||||
|
if len(boards) > 1:
|
||||||
for board_num in range(1, 16, 5):
|
for board_num in range(1, 16, 5):
|
||||||
for _b_num in range(5):
|
for _b_num in range(5):
|
||||||
b = boards[1].get(f"chain_acn{board_num + _b_num}")
|
b = boards[1].get(f"chain_acn{board_num + _b_num}")
|
||||||
@@ -182,93 +234,124 @@ class BMMiner(BaseMiner):
|
|||||||
if board_offset == -1:
|
if board_offset == -1:
|
||||||
board_offset = 1
|
board_offset = 1
|
||||||
|
|
||||||
data.left_chips = boards[1].get(f"chain_acn{board_offset}")
|
for i in range(board_offset, board_offset + self.ideal_hashboards):
|
||||||
data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
|
hashboard = HashBoard(
|
||||||
data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
|
slot=i - board_offset, expected_chips=self.nominal_chips
|
||||||
|
|
||||||
data.left_board_hashrate = round(
|
|
||||||
float(boards[1].get(f"chain_rate{board_offset}")) / 1000, 2
|
|
||||||
)
|
|
||||||
data.center_board_hashrate = round(
|
|
||||||
float(boards[1].get(f"chain_rate{board_offset+1}")) / 1000, 2
|
|
||||||
)
|
|
||||||
data.right_board_hashrate = round(
|
|
||||||
float(boards[1].get(f"chain_rate{board_offset+2}")) / 1000, 2
|
|
||||||
)
|
|
||||||
|
|
||||||
if stats:
|
|
||||||
temp = stats.get("STATS")
|
|
||||||
if temp:
|
|
||||||
if len(temp) > 1:
|
|
||||||
for fan_num in range(1, 8, 4):
|
|
||||||
for _f_num in range(4):
|
|
||||||
f = temp[1].get(f"fan{fan_num + _f_num}")
|
|
||||||
if f and not f == 0 and fan_offset == -1:
|
|
||||||
fan_offset = fan_num
|
|
||||||
if fan_offset == -1:
|
|
||||||
fan_offset = 1
|
|
||||||
for fan in range(self.fan_count):
|
|
||||||
setattr(
|
|
||||||
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
|
chip_temp = boards[1].get(f"temp{i}")
|
||||||
env_temp_list = []
|
if chip_temp:
|
||||||
for item in range(3):
|
hashboard.chip_temp = round(chip_temp)
|
||||||
board_temp = temp[1].get(f"temp{item + board_offset}")
|
|
||||||
chip_temp = temp[1].get(f"temp2_{item + board_offset}")
|
|
||||||
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
|
|
||||||
setattr(data, f"{board_map[item]}_temp", board_temp)
|
|
||||||
if f"temp_pcb{item}" in temp[1].keys():
|
|
||||||
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
|
|
||||||
if not env_temp == 0:
|
|
||||||
env_temp_list.append(int(env_temp))
|
|
||||||
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
|
||||||
|
|
||||||
if pools:
|
temp = boards[1].get(f"temp2_{i}")
|
||||||
pool_1 = None
|
if temp:
|
||||||
pool_2 = None
|
hashboard.temp = round(temp)
|
||||||
pool_1_user = None
|
|
||||||
pool_2_user = None
|
|
||||||
pool_1_quota = 1
|
|
||||||
pool_2_quota = 1
|
|
||||||
quota = 0
|
|
||||||
for pool in pools.get("POOLS"):
|
|
||||||
if not pool_1_user:
|
|
||||||
pool_1_user = pool.get("User")
|
|
||||||
pool_1 = pool["URL"]
|
|
||||||
pool_1_quota = pool["Quota"]
|
|
||||||
elif not pool_2_user:
|
|
||||||
pool_2_user = pool.get("User")
|
|
||||||
pool_2 = pool["URL"]
|
|
||||||
pool_2_quota = pool["Quota"]
|
|
||||||
if not pool.get("User") == pool_1_user:
|
|
||||||
if not pool_2_user == pool.get("User"):
|
|
||||||
pool_2_user = pool.get("User")
|
|
||||||
pool_2 = pool["URL"]
|
|
||||||
pool_2_quota = pool["Quota"]
|
|
||||||
if pool_2_user and not pool_2_user == pool_1_user:
|
|
||||||
quota = f"{pool_1_quota}/{pool_2_quota}"
|
|
||||||
|
|
||||||
if pool_1:
|
hashrate = boards[1].get(f"chain_rate{i}")
|
||||||
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
|
if hashrate:
|
||||||
"stratum2+tcp://", ""
|
hashboard.hashrate = round(float(hashrate) / 1000, 2)
|
||||||
)
|
|
||||||
data.pool_1_url = pool_1
|
|
||||||
|
|
||||||
if pool_1_user:
|
chips = boards[1].get(f"chain_acn{i}")
|
||||||
data.pool_1_user = pool_1_user
|
if chips:
|
||||||
|
hashboard.chips = chips
|
||||||
|
hashboard.missing = False
|
||||||
|
if (not chips) or (not chips > 0):
|
||||||
|
hashboard.missing = True
|
||||||
|
hashboards.append(hashboard)
|
||||||
|
except (IndexError, KeyError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
if pool_2:
|
return hashboards
|
||||||
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
|
|
||||||
"stratum2+tcp://", ""
|
|
||||||
)
|
|
||||||
data.pool_2_url = pool_2
|
|
||||||
|
|
||||||
if pool_2_user:
|
async def get_env_temp(self) -> Optional[float]:
|
||||||
data.pool_2_user = pool_2_user
|
return None
|
||||||
|
|
||||||
if quota:
|
async def get_wattage(self) -> Optional[int]:
|
||||||
data.pool_split = str(quota)
|
return None
|
||||||
|
|
||||||
return data
|
async def get_wattage_limit(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_fans(self, api_stats: dict = None) -> List[Fan]:
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
fans_data = [None, None, None, None]
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
fan_offset = -1
|
||||||
|
|
||||||
|
for fan_num in range(1, 8, 4):
|
||||||
|
for _f_num in range(4):
|
||||||
|
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}")
|
||||||
|
if f and not f == 0 and fan_offset == -1:
|
||||||
|
fan_offset = fan_num
|
||||||
|
if fan_offset == -1:
|
||||||
|
fan_offset = 1
|
||||||
|
|
||||||
|
for fan in range(self.fan_count):
|
||||||
|
fans_data[fan] = api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
fans = [Fan(speed=d) if d else Fan() for d in fans_data]
|
||||||
|
|
||||||
|
return fans
|
||||||
|
|
||||||
|
async def get_pools(self, api_pools: dict = None) -> List[dict]:
|
||||||
|
groups = []
|
||||||
|
|
||||||
|
if not api_pools:
|
||||||
|
try:
|
||||||
|
api_pools = await self.api.pools()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_pools:
|
||||||
|
try:
|
||||||
|
pools = {}
|
||||||
|
for i, pool in enumerate(api_pools["POOLS"]):
|
||||||
|
pools[f"pool_{i + 1}_url"] = (
|
||||||
|
pool["URL"]
|
||||||
|
.replace("stratum+tcp://", "")
|
||||||
|
.replace("stratum2+tcp://", "")
|
||||||
|
)
|
||||||
|
pools[f"pool_{i + 1}_user"] = pool["User"]
|
||||||
|
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
|
||||||
|
|
||||||
|
groups.append(pools)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return groups
|
||||||
|
|
||||||
|
async def get_errors(self) -> List[MinerErrorData]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_fault_light(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
|
||||||
|
# X19 method, not sure compatibility
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
|
||||||
|
try:
|
||||||
|
rate_unit = api_stats["STATS"][1]["rate_unit"]
|
||||||
|
except KeyError:
|
||||||
|
rate_unit = "GH"
|
||||||
|
if rate_unit == "GH":
|
||||||
|
return round(ideal_rate / 1000, 2)
|
||||||
|
if rate_unit == "MH":
|
||||||
|
return round(ideal_rate / 1000000, 2)
|
||||||
|
else:
|
||||||
|
return round(ideal_rate, 2)
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,61 @@
|
|||||||
import logging
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
from collections import namedtuple
|
||||||
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import asyncssh
|
||||||
|
|
||||||
from pyasic.API.bosminer import BOSMinerAPI
|
from pyasic.API.bosminer import BOSMinerAPI
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.config import MinerConfig
|
||||||
|
from pyasic.data import Fan, HashBoard, MinerData
|
||||||
|
from pyasic.data.error_codes import MinerErrorData
|
||||||
|
from pyasic.errors import APIError
|
||||||
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class BOSMinerOld(BaseMiner):
|
class BOSMinerOld(BaseMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
|
||||||
super().__init__(ip)
|
super().__init__(ip)
|
||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ipaddress.ip_address(ip)
|
||||||
self.api = BOSMinerAPI(ip)
|
self.api = BOSMinerAPI(ip, api_ver)
|
||||||
self.api_type = "BOSMiner"
|
self.api_type = "BOSMiner"
|
||||||
self.uname = "root"
|
self.uname = "root"
|
||||||
self.pwd = "admin"
|
self.pwd = "admin"
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd: str) -> str or None:
|
async def send_ssh_command(self, cmd: str) -> Optional[str]:
|
||||||
"""Send a command to the miner over ssh.
|
|
||||||
|
|
||||||
:return: Result of the command or None.
|
|
||||||
"""
|
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = await self._get_ssh_connection()
|
||||||
|
except (asyncssh.Error, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
# open an ssh connection
|
# open an ssh connection
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
async with conn:
|
||||||
# 3 retries
|
# 3 retries
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
try:
|
try:
|
||||||
# run the command and get the result
|
# run the command and get the result
|
||||||
result = await conn.run(cmd)
|
result = await conn.run(cmd)
|
||||||
if result.stdout:
|
result = result.stdout
|
||||||
result = result.stdout
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if e == "SSH connection closed":
|
|
||||||
return "Update completed."
|
|
||||||
# if the command fails, log it
|
# if the command fails, log it
|
||||||
logging.warning(f"{self} command {cmd} error: {e}")
|
logging.warning(f"{self} command {cmd} error: {e}")
|
||||||
|
|
||||||
@@ -42,9 +64,121 @@ class BOSMinerOld(BaseMiner):
|
|||||||
return
|
return
|
||||||
continue
|
continue
|
||||||
# return the result, either command output or None
|
# return the result, either command output or None
|
||||||
return str(result)
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def update_to_plus(self):
|
async def update_to_plus(self):
|
||||||
result = await self.send_ssh_command("opkg update && opkg install bos_plus")
|
result = await self.send_ssh_command("opkg update && opkg install bos_plus")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def check_light(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_config(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def stop_mining(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def resume_mining(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def set_power_limit(self, wattage: int) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
async def get_mac(self) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_model(self) -> str:
|
||||||
|
return "S9"
|
||||||
|
|
||||||
|
async def get_version(self) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
async def get_hostname(self) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_hashrate(self) -> Optional[float]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_hashboards(self) -> List[HashBoard]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_env_temp(self) -> Optional[float]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_wattage(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_wattage_limit(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_fans(
|
||||||
|
self,
|
||||||
|
) -> List[Fan]:
|
||||||
|
return [Fan(), Fan(), Fan(), Fan()]
|
||||||
|
|
||||||
|
async def get_fan_psu(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_api_ver(self) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_fw_ver(self) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_pools(self, api_pools: dict = None) -> List[dict]:
|
||||||
|
groups = []
|
||||||
|
|
||||||
|
if not api_pools:
|
||||||
|
try:
|
||||||
|
api_pools = await self.api.pools()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_pools:
|
||||||
|
try:
|
||||||
|
pools = {}
|
||||||
|
for i, pool in enumerate(api_pools["POOLS"]):
|
||||||
|
pools[f"pool_{i + 1}_url"] = (
|
||||||
|
pool["URL"]
|
||||||
|
.replace("stratum+tcp://", "")
|
||||||
|
.replace("stratum2+tcp://", "")
|
||||||
|
)
|
||||||
|
pools[f"pool_{i + 1}_user"] = pool["User"]
|
||||||
|
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
|
||||||
|
|
||||||
|
groups.append(pools)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return groups
|
||||||
|
|
||||||
|
async def get_errors(self) -> List[MinerErrorData]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_fault_light(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_nominal_hashrate(self) -> Optional[float]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_data(self, allow_warning: bool = False) -> MinerData:
|
||||||
|
return MinerData(ip=str(self.ip))
|
||||||
|
|||||||
@@ -1,267 +1,586 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
|
from collections import namedtuple
|
||||||
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
from pyasic.API.btminer import BTMinerAPI
|
from pyasic.API.btminer import BTMinerAPI
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.config import MinerConfig
|
||||||
from pyasic.API import APIError
|
from pyasic.data import Fan, HashBoard, MinerData
|
||||||
|
from pyasic.data.error_codes import MinerErrorData, WhatsminerError
|
||||||
from pyasic.data import MinerData
|
from pyasic.errors import APIError
|
||||||
from pyasic.data.error_codes import WhatsminerError
|
from pyasic.miners.base import BaseMiner
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
|
||||||
|
|
||||||
|
|
||||||
class BTMiner(BaseMiner):
|
class BTMiner(BaseMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
|
||||||
super().__init__(ip)
|
super().__init__(ip)
|
||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ipaddress.ip_address(ip)
|
||||||
self.api = BTMinerAPI(ip)
|
self.api = BTMinerAPI(ip, api_ver)
|
||||||
self.api_type = "BTMiner"
|
self.api_type = "BTMiner"
|
||||||
|
self.api_ver = api_ver
|
||||||
|
|
||||||
async def get_model(self):
|
async def _reset_api_pwd_to_admin(self, pwd: str):
|
||||||
|
try:
|
||||||
|
data = await self.api.update_pwd(pwd, "admin")
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data:
|
||||||
|
if "Code" in data.keys():
|
||||||
|
if data["Code"] == 131:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.set_led(auto=True)
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data:
|
||||||
|
if "Code" in data.keys():
|
||||||
|
if data["Code"] == 131:
|
||||||
|
self.light = False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.set_led(auto=False)
|
||||||
|
await self.api.set_led(
|
||||||
|
auto=False, color="green", start=0, period=1, duration=0
|
||||||
|
)
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data:
|
||||||
|
if "Code" in data.keys():
|
||||||
|
if data["Code"] == 131:
|
||||||
|
self.light = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.reboot()
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data.get("Msg"):
|
||||||
|
if data["Msg"] == "API command OK":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.restart()
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data.get("Msg"):
|
||||||
|
if data["Msg"] == "API command OK":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def stop_mining(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.power_off(respbefore=True)
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data.get("Msg"):
|
||||||
|
if data["Msg"] == "API command OK":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def resume_mining(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.power_on()
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data.get("Msg"):
|
||||||
|
if data["Msg"] == "API command OK":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
conf = config.as_wm(user_suffix=user_suffix)
|
||||||
|
pools_conf = conf["pools"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.api.update_pools(
|
||||||
|
pools_conf[0]["url"],
|
||||||
|
pools_conf[0]["user"],
|
||||||
|
pools_conf[0]["pass"],
|
||||||
|
pools_conf[1]["url"],
|
||||||
|
pools_conf[1]["user"],
|
||||||
|
pools_conf[1]["pass"],
|
||||||
|
pools_conf[2]["url"],
|
||||||
|
pools_conf[2]["user"],
|
||||||
|
pools_conf[2]["pass"],
|
||||||
|
)
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
await self.api.adjust_power_limit(conf["wattage"])
|
||||||
|
except APIError:
|
||||||
|
# cannot set wattage
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_config(self) -> MinerConfig:
|
||||||
|
pools = None
|
||||||
|
summary = None
|
||||||
|
cfg = MinerConfig()
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await self.api.multicommand("pools", "summary")
|
||||||
|
pools = data["pools"][0]
|
||||||
|
summary = data["summary"][0]
|
||||||
|
except APIError as e:
|
||||||
|
logging.warning(e)
|
||||||
|
except LookupError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if pools:
|
||||||
|
if "POOLS" in pools:
|
||||||
|
cfg = cfg.from_api(pools["POOLS"])
|
||||||
|
else:
|
||||||
|
# somethings wrong with the miner
|
||||||
|
warnings.warn(
|
||||||
|
f"Failed to gather pool config for miner: {self}, miner did not return pool information."
|
||||||
|
)
|
||||||
|
if summary:
|
||||||
|
if "SUMMARY" in summary:
|
||||||
|
if wattage := summary["SUMMARY"][0].get("Power Limit"):
|
||||||
|
cfg.autotuning_wattage = wattage
|
||||||
|
|
||||||
|
self.config = cfg
|
||||||
|
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
async def set_power_limit(self, wattage: int) -> bool:
|
||||||
|
try:
|
||||||
|
await self.api.adjust_power_limit(wattage)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"{self} set_power_limit: {e}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
async def get_mac(
|
||||||
|
self, api_summary: dict = None, api_get_miner_info: dict = None
|
||||||
|
) -> Optional[str]:
|
||||||
|
if not api_get_miner_info:
|
||||||
|
try:
|
||||||
|
api_get_miner_info = await self.api.get_miner_info()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_get_miner_info:
|
||||||
|
try:
|
||||||
|
mac = api_get_miner_info["Msg"]["mac"]
|
||||||
|
return str(mac).upper()
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
mac = api_summary["SUMMARY"][0]["MAC"]
|
||||||
|
return str(mac).upper()
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_model(self, api_devdetails: dict = None) -> Optional[str]:
|
||||||
if self.model:
|
if self.model:
|
||||||
logging.debug(f"Found model for {self.ip}: {self.model}")
|
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||||
return self.model
|
return self.model
|
||||||
version_data = await self.api.devdetails()
|
|
||||||
if version_data:
|
if not api_devdetails:
|
||||||
self.model = version_data["DEVDETAILS"][0]["Model"].split("V")[0]
|
try:
|
||||||
logging.debug(f"Found model for {self.ip}: {self.model}")
|
api_devdetails = await self.api.devdetails()
|
||||||
return self.model
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_devdetails:
|
||||||
|
try:
|
||||||
|
self.model = api_devdetails["DEVDETAILS"][0]["Model"].split("V")[0]
|
||||||
|
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||||
|
return self.model
|
||||||
|
except (TypeError, IndexError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
logging.warning(f"Failed to get model for miner: {self}")
|
logging.warning(f"Failed to get model for miner: {self}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_hostname(self) -> str or None:
|
async def get_version(
|
||||||
|
self, api_get_version: dict = None, api_summary: dict = None
|
||||||
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
|
||||||
|
api_ver = await self.get_api_ver(api_get_version=api_get_version)
|
||||||
|
fw_ver = await self.get_fw_ver(
|
||||||
|
api_get_version=api_get_version, api_summary=api_summary
|
||||||
|
)
|
||||||
|
return miner_version(api_ver, fw_ver)
|
||||||
|
|
||||||
|
async def get_api_ver(self, api_get_version: dict = None) -> Optional[str]:
|
||||||
|
# Check to see if the version info is already cached
|
||||||
|
if self.api_ver:
|
||||||
|
return self.api_ver
|
||||||
|
|
||||||
|
if not api_get_version:
|
||||||
|
try:
|
||||||
|
api_get_version = await self.api.get_version()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_get_version:
|
||||||
|
if "Code" in api_get_version.keys():
|
||||||
|
if api_get_version["Code"] == 131:
|
||||||
|
try:
|
||||||
|
api_ver = api_get_version["Msg"]
|
||||||
|
if not isinstance(api_ver, str):
|
||||||
|
api_ver = api_ver["api_ver"]
|
||||||
|
self.api_ver = api_ver.replace("whatsminer v", "")
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.api.api_ver = self.api_ver
|
||||||
|
return self.api_ver
|
||||||
|
|
||||||
|
return self.api_ver
|
||||||
|
|
||||||
|
async def get_fw_ver(
|
||||||
|
self, api_get_version: dict = None, api_summary: dict = None
|
||||||
|
) -> Optional[str]:
|
||||||
|
# Check to see if the version info is already cached
|
||||||
|
if self.fw_ver:
|
||||||
|
return self.fw_ver
|
||||||
|
|
||||||
|
if not api_get_version:
|
||||||
|
try:
|
||||||
|
api_get_version = await self.api.get_version()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_get_version:
|
||||||
|
if "Code" in api_get_version.keys():
|
||||||
|
if api_get_version["Code"] == 131:
|
||||||
|
try:
|
||||||
|
self.fw_ver = api_get_version["Msg"]["fw_ver"]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return self.fw_ver
|
||||||
|
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
self.fw_ver = api_summary["SUMMARY"][0]["Firmware Version"].replace(
|
||||||
|
"'", ""
|
||||||
|
)
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self.fw_ver
|
||||||
|
|
||||||
|
async def get_hostname(self, api_get_miner_info: dict = None) -> Optional[str]:
|
||||||
if self.hostname:
|
if self.hostname:
|
||||||
return self.hostname
|
return self.hostname
|
||||||
try:
|
|
||||||
host_data = await self.api.get_miner_info()
|
|
||||||
if host_data:
|
|
||||||
host = host_data["Msg"]["hostname"]
|
|
||||||
logging.debug(f"Found hostname for {self.ip}: {host}")
|
|
||||||
self.hostname = host
|
|
||||||
return self.hostname
|
|
||||||
except APIError:
|
|
||||||
logging.info(f"Failed to get hostname for miner: {self}")
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_board_info(self) -> dict:
|
if not api_get_miner_info:
|
||||||
"""Gets data on each board and chain in the miner."""
|
try:
|
||||||
logging.debug(f"{self}: Getting board info.")
|
api_get_miner_info = await self.api.get_miner_info()
|
||||||
devs = await self.api.devs()
|
except APIError:
|
||||||
if not devs.get("DEVS"):
|
return None # only one way to get this
|
||||||
print("devs error", devs)
|
|
||||||
return {0: [], 1: [], 2: []}
|
if api_get_miner_info:
|
||||||
devs = devs["DEVS"]
|
try:
|
||||||
boards = {}
|
self.hostname = api_get_miner_info["Msg"]["hostname"]
|
||||||
offset = devs[0]["ID"]
|
except KeyError:
|
||||||
for board in devs:
|
return None
|
||||||
boards[board["ID"] - offset] = []
|
|
||||||
if "Effective Chips" in board.keys():
|
return self.hostname
|
||||||
if not board["Effective Chips"] in self.nominal_chips:
|
|
||||||
nominal = False
|
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
|
||||||
|
# get hr from API
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
return round(float(api_summary["SUMMARY"][0]["MHS 1m"] / 1000000), 2)
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_hashboards(self, api_devs: dict = None) -> List[HashBoard]:
|
||||||
|
|
||||||
|
hashboards = [
|
||||||
|
HashBoard(slot=i, expected_chips=self.nominal_chips)
|
||||||
|
for i in range(self.ideal_hashboards)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not api_devs:
|
||||||
|
try:
|
||||||
|
api_devs = await self.api.devs()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_devs:
|
||||||
|
try:
|
||||||
|
for board in api_devs["DEVS"]:
|
||||||
|
if len(hashboards) < board["ASC"] + 1:
|
||||||
|
hashboards.append(
|
||||||
|
HashBoard(
|
||||||
|
slot=board["ASC"], expected_chips=self.nominal_chips
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.ideal_hashboards += 1
|
||||||
|
hashboards[board["ASC"]].chip_temp = round(board["Chip Temp Avg"])
|
||||||
|
hashboards[board["ASC"]].temp = round(board["Temperature"])
|
||||||
|
hashboards[board["ASC"]].hashrate = round(
|
||||||
|
float(board["MHS 1m"] / 1000000), 2
|
||||||
|
)
|
||||||
|
hashboards[board["ASC"]].chips = board["Effective Chips"]
|
||||||
|
hashboards[board["ASC"]].missing = False
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return hashboards
|
||||||
|
|
||||||
|
async def get_env_temp(self, api_summary: dict = None) -> Optional[float]:
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
return api_summary["SUMMARY"][0]["Env Temp"]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_wattage(self, api_summary: dict = None) -> Optional[int]:
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
return api_summary["SUMMARY"][0]["Power"]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_wattage_limit(self, api_summary: dict = None) -> Optional[int]:
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
return api_summary["SUMMARY"][0]["Power Limit"]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_fans(
|
||||||
|
self, api_summary: dict = None, api_get_psu: dict = None
|
||||||
|
) -> List[Fan]:
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
fans = [Fan(), Fan(), Fan(), Fan()]
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
if self.fan_count > 0:
|
||||||
|
fans = [
|
||||||
|
Fan(api_summary["SUMMARY"][0]["Fan Speed In"]),
|
||||||
|
Fan(api_summary["SUMMARY"][0]["Fan Speed Out"]),
|
||||||
|
Fan(),
|
||||||
|
Fan(),
|
||||||
|
]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return fans
|
||||||
|
|
||||||
|
async def get_fan_psu(
|
||||||
|
self, api_summary: dict = None, api_get_psu: dict = None
|
||||||
|
) -> Optional[int]:
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
return int(api_summary["SUMMARY"][0]["Power Fanspeed"])
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not api_get_psu:
|
||||||
|
try:
|
||||||
|
api_get_psu = await self.api.get_psu()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_get_psu:
|
||||||
|
try:
|
||||||
|
return int(api_get_psu["Msg"]["fan_speed"])
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_pools(self, api_pools: dict = None) -> List[dict]:
|
||||||
|
groups = []
|
||||||
|
|
||||||
|
if not api_pools:
|
||||||
|
try:
|
||||||
|
api_pools = await self.api.pools()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_pools:
|
||||||
|
try:
|
||||||
|
pools = {}
|
||||||
|
for i, pool in enumerate(api_pools["POOLS"]):
|
||||||
|
pools[f"pool_{i + 1}_url"] = (
|
||||||
|
pool["URL"]
|
||||||
|
.replace("stratum+tcp://", "")
|
||||||
|
.replace("stratum2+tcp://", "")
|
||||||
|
)
|
||||||
|
pools[f"pool_{i + 1}_user"] = pool["User"]
|
||||||
|
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
|
||||||
|
|
||||||
|
groups.append(pools)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return groups
|
||||||
|
|
||||||
|
async def get_errors(
|
||||||
|
self, api_summary: dict = None, api_get_error_code: dict = None
|
||||||
|
) -> List[MinerErrorData]:
|
||||||
|
errors = []
|
||||||
|
if not api_summary and not api_get_error_code:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
for i in range(api_summary["SUMMARY"][0]["Error Code Count"]):
|
||||||
|
err = api_summary["SUMMARY"][0].get(f"Error Code {i}")
|
||||||
|
if err:
|
||||||
|
errors.append(WhatsminerError(error_code=err))
|
||||||
|
except (KeyError, IndexError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not api_get_error_code:
|
||||||
|
try:
|
||||||
|
api_get_error_code = await self.api.get_error_code()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_get_error_code:
|
||||||
|
for err in api_get_error_code["Msg"]["error_code"]:
|
||||||
|
if isinstance(err, dict):
|
||||||
|
for code in err:
|
||||||
|
errors.append(WhatsminerError(error_code=int(code)))
|
||||||
else:
|
else:
|
||||||
nominal = True
|
errors.append(WhatsminerError(error_code=int(err)))
|
||||||
boards[board["ID"] - offset].append(
|
|
||||||
{
|
|
||||||
"chain": board["ID"] - offset,
|
|
||||||
"chip_count": board["Effective Chips"],
|
|
||||||
"chip_status": "o" * board["Effective Chips"],
|
|
||||||
"nominal": nominal,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logging.warning(f"Incorrect board data from {self}: {board}")
|
|
||||||
print(board)
|
|
||||||
logging.debug(f"Found board data for {self}: {boards}")
|
|
||||||
return boards
|
|
||||||
|
|
||||||
async def get_mac(self):
|
return errors
|
||||||
mac = ""
|
|
||||||
data = await self.api.summary()
|
async def get_nominal_hashrate(self, api_summary: dict = None):
|
||||||
if data:
|
if not api_summary:
|
||||||
if data.get("SUMMARY"):
|
|
||||||
if len(data["SUMMARY"]) > 0:
|
|
||||||
_mac = data["SUMMARY"][0].get("MAC")
|
|
||||||
if _mac:
|
|
||||||
mac = _mac
|
|
||||||
if mac == "":
|
|
||||||
try:
|
try:
|
||||||
data = await self.api.get_miner_info()
|
api_summary = await self.api.summary()
|
||||||
if data:
|
|
||||||
if "Msg" in data.keys():
|
|
||||||
if "mac" in data["Msg"].keys():
|
|
||||||
mac = data["Msg"]["mac"]
|
|
||||||
except APIError:
|
except APIError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return str(mac).upper()
|
if api_summary:
|
||||||
|
try:
|
||||||
|
nominal_hashrate = api_summary["SUMMARY"][0]["Factory GHS"]
|
||||||
|
if nominal_hashrate:
|
||||||
|
return round(nominal_hashrate / 1000, 2)
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
async def get_data(self):
|
async def get_fault_light(self, api_get_miner_info: dict = None) -> bool:
|
||||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
data = None
|
||||||
|
|
||||||
mac = None
|
if not api_get_miner_info:
|
||||||
|
try:
|
||||||
|
api_get_miner_info = await self.api.get_miner_info()
|
||||||
|
except APIError:
|
||||||
|
if not self.light:
|
||||||
|
self.light = False
|
||||||
|
|
||||||
try:
|
if api_get_miner_info:
|
||||||
model = await self.get_model()
|
try:
|
||||||
except APIError:
|
self.light = not (api_get_miner_info["Msg"]["ledstat"] == "auto")
|
||||||
logging.info(f"Failed to get model: {self}")
|
except KeyError:
|
||||||
model = None
|
pass
|
||||||
data.model = "Whatsminer"
|
|
||||||
|
|
||||||
try:
|
return self.light if self.light else False
|
||||||
|
|
||||||
|
async def set_static_ip(
|
||||||
|
self,
|
||||||
|
ip: str,
|
||||||
|
dns: str,
|
||||||
|
gateway: str,
|
||||||
|
subnet_mask: str = "255.255.255.0",
|
||||||
|
hostname: str = None,
|
||||||
|
):
|
||||||
|
if not hostname:
|
||||||
hostname = await self.get_hostname()
|
hostname = await self.get_hostname()
|
||||||
except APIError:
|
await self.api.net_config(
|
||||||
logging.info(f"Failed to get hostname: {self}")
|
ip=ip, mask=subnet_mask, dns=dns, gate=gateway, host=hostname, dhcp=False
|
||||||
hostname = None
|
)
|
||||||
data.hostname = "Whatsminer"
|
|
||||||
|
|
||||||
if model:
|
|
||||||
data.model = model
|
|
||||||
|
|
||||||
|
async def set_dhcp(self, hostname: str = None):
|
||||||
if hostname:
|
if hostname:
|
||||||
data.hostname = hostname
|
await self.set_hostname(hostname)
|
||||||
|
await self.api.net_config()
|
||||||
|
|
||||||
miner_data = None
|
async def set_hostname(self, hostname: str):
|
||||||
for i in range(DATA_RETRIES):
|
await self.api.set_hostname(hostname)
|
||||||
try:
|
|
||||||
miner_data = await self.api.multicommand("summary", "devs", "pools")
|
|
||||||
if miner_data:
|
|
||||||
break
|
|
||||||
except APIError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not miner_data:
|
|
||||||
return data
|
|
||||||
|
|
||||||
summary = miner_data.get("summary")[0]
|
|
||||||
devs = miner_data.get("devs")[0]
|
|
||||||
pools = miner_data.get("pools")[0]
|
|
||||||
|
|
||||||
if summary:
|
|
||||||
summary_data = summary.get("SUMMARY")
|
|
||||||
if summary_data:
|
|
||||||
if len(summary_data) > 0:
|
|
||||||
wattage_limit = None
|
|
||||||
if summary_data[0].get("MAC"):
|
|
||||||
mac = summary_data[0]["MAC"]
|
|
||||||
|
|
||||||
if summary_data[0].get("Env Temp"):
|
|
||||||
data.env_temp = summary_data[0]["Env Temp"]
|
|
||||||
|
|
||||||
if summary_data[0].get("Power Limit"):
|
|
||||||
wattage_limit = summary_data[0]["Power Limit"]
|
|
||||||
|
|
||||||
data.fan_1 = summary_data[0]["Fan Speed In"]
|
|
||||||
data.fan_2 = summary_data[0]["Fan Speed Out"]
|
|
||||||
|
|
||||||
hr = summary_data[0].get("MHS 1m")
|
|
||||||
if hr:
|
|
||||||
data.hashrate = round(hr / 1000000, 2)
|
|
||||||
|
|
||||||
wattage = summary_data[0].get("Power")
|
|
||||||
if wattage:
|
|
||||||
data.wattage = round(wattage)
|
|
||||||
|
|
||||||
if not wattage_limit:
|
|
||||||
wattage_limit = round(wattage)
|
|
||||||
|
|
||||||
data.wattage_limit = wattage_limit
|
|
||||||
|
|
||||||
if summary_data[0].get("Error Code Count"):
|
|
||||||
for i in range(summary_data[0]["Error Code Count"]):
|
|
||||||
if summary_data[0].get(f"Error Code {i}"):
|
|
||||||
data.errors.append(
|
|
||||||
WhatsminerError(
|
|
||||||
error_code=summary_data[0][f"Error Code {i}"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if devs:
|
|
||||||
temp_data = devs.get("DEVS")
|
|
||||||
if temp_data:
|
|
||||||
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
|
|
||||||
for board in temp_data:
|
|
||||||
_id = board["ASC"]
|
|
||||||
chip_temp = round(board["Chip Temp Avg"])
|
|
||||||
board_temp = round(board["Temperature"])
|
|
||||||
hashrate = round(board["MHS 1m"] / 1000000, 2)
|
|
||||||
setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
|
|
||||||
setattr(data, f"{board_map[_id]}_temp", board_temp)
|
|
||||||
setattr(data, f"{board_map[_id]}_hashrate", hashrate)
|
|
||||||
|
|
||||||
if devs:
|
|
||||||
boards = devs.get("DEVS")
|
|
||||||
if boards:
|
|
||||||
if len(boards) > 0:
|
|
||||||
board_map = {0: "left_chips", 1: "center_chips", 2: "right_chips"}
|
|
||||||
if "ID" in boards[0].keys():
|
|
||||||
id_key = "ID"
|
|
||||||
else:
|
|
||||||
id_key = "ASC"
|
|
||||||
offset = boards[0][id_key]
|
|
||||||
for board in boards:
|
|
||||||
_id = board[id_key] - offset
|
|
||||||
chips = board["Effective Chips"]
|
|
||||||
setattr(data, board_map[_id], chips)
|
|
||||||
|
|
||||||
if pools:
|
|
||||||
pool_1 = None
|
|
||||||
pool_2 = None
|
|
||||||
pool_1_user = None
|
|
||||||
pool_2_user = None
|
|
||||||
pool_1_quota = 1
|
|
||||||
pool_2_quota = 1
|
|
||||||
quota = 0
|
|
||||||
for pool in pools.get("POOLS"):
|
|
||||||
if not pool_1_user:
|
|
||||||
pool_1_user = pool.get("User")
|
|
||||||
pool_1 = pool["URL"]
|
|
||||||
pool_1_quota = pool["Quota"]
|
|
||||||
elif not pool_2_user:
|
|
||||||
pool_2_user = pool.get("User")
|
|
||||||
pool_2 = pool["URL"]
|
|
||||||
pool_2_quota = pool["Quota"]
|
|
||||||
if not pool.get("User") == pool_1_user:
|
|
||||||
if not pool_2_user == pool.get("User"):
|
|
||||||
pool_2_user = pool.get("User")
|
|
||||||
pool_2 = pool["URL"]
|
|
||||||
pool_2_quota = pool["Quota"]
|
|
||||||
if pool_2_user and not pool_2_user == pool_1_user:
|
|
||||||
quota = f"{pool_1_quota}/{pool_2_quota}"
|
|
||||||
|
|
||||||
if pool_1:
|
|
||||||
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
|
|
||||||
"stratum2+tcp://", ""
|
|
||||||
)
|
|
||||||
data.pool_1_url = pool_1
|
|
||||||
|
|
||||||
if pool_1_user:
|
|
||||||
data.pool_1_user = pool_1_user
|
|
||||||
|
|
||||||
if pool_2:
|
|
||||||
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
|
|
||||||
"stratum2+tcp://", ""
|
|
||||||
)
|
|
||||||
data.pool_2_url = pool_2
|
|
||||||
|
|
||||||
if pool_2_user:
|
|
||||||
data.pool_2_user = pool_2_user
|
|
||||||
|
|
||||||
if quota:
|
|
||||||
data.pool_split = str(quota)
|
|
||||||
|
|
||||||
if not mac:
|
|
||||||
try:
|
|
||||||
mac = await self.get_mac()
|
|
||||||
except APIError:
|
|
||||||
logging.info(f"Failed to get mac: {self}")
|
|
||||||
mac = None
|
|
||||||
|
|
||||||
if mac:
|
|
||||||
data.mac = mac
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|||||||
@@ -1,159 +1,272 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
|
from collections import namedtuple
|
||||||
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import asyncssh
|
||||||
|
|
||||||
from pyasic.API.cgminer import CGMinerAPI
|
from pyasic.API.cgminer import CGMinerAPI
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.config import MinerConfig
|
||||||
from pyasic.API import APIError
|
from pyasic.data import Fan, HashBoard, MinerData
|
||||||
|
from pyasic.data.error_codes import MinerErrorData
|
||||||
from pyasic.data import MinerData
|
from pyasic.errors import APIError
|
||||||
|
from pyasic.miners.base import BaseMiner
|
||||||
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
|
||||||
class CGMiner(BaseMiner):
|
class CGMiner(BaseMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
|
||||||
super().__init__(ip)
|
super().__init__(ip)
|
||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ipaddress.ip_address(ip)
|
||||||
self.api = CGMinerAPI(ip)
|
self.api = CGMinerAPI(ip, api_ver)
|
||||||
|
self.api_ver = api_ver
|
||||||
self.api_type = "CGMiner"
|
self.api_type = "CGMiner"
|
||||||
self.uname = "root"
|
self.uname = "root"
|
||||||
self.pwd = "admin"
|
self.pwd = "admin"
|
||||||
self.config = None
|
self.config = None
|
||||||
|
|
||||||
async def get_model(self):
|
async def send_ssh_command(self, cmd: str) -> Optional[str]:
|
||||||
if self.model:
|
|
||||||
return self.model
|
|
||||||
try:
|
|
||||||
version_data = await self.api.devdetails()
|
|
||||||
except APIError:
|
|
||||||
return None
|
|
||||||
if version_data:
|
|
||||||
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
|
|
||||||
return self.model
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str or None:
|
|
||||||
if self.hostname:
|
|
||||||
return self.hostname
|
|
||||||
try:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
if conn is not None:
|
|
||||||
data = await conn.run("cat /proc/sys/kernel/hostname")
|
|
||||||
host = data.stdout.strip()
|
|
||||||
self.hostname = host
|
|
||||||
return self.hostname
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd):
|
|
||||||
result = None
|
result = None
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
|
try:
|
||||||
|
conn = await self._get_ssh_connection()
|
||||||
|
except (asyncssh.Error, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# open an ssh connection
|
||||||
|
async with conn:
|
||||||
|
# 3 retries
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
try:
|
try:
|
||||||
|
# run the command and get the result
|
||||||
result = await conn.run(cmd)
|
result = await conn.run(cmd)
|
||||||
result = result.stdout
|
result = result.stdout
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{cmd} error: {e}")
|
# if the command fails, log it
|
||||||
|
logging.warning(f"{self} command {cmd} error: {e}")
|
||||||
|
|
||||||
|
# on the 3rd retry, return None
|
||||||
if i == 3:
|
if i == 3:
|
||||||
return
|
return
|
||||||
continue
|
continue
|
||||||
|
# return the result, either command output or None
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def restart_backend(self) -> bool:
|
async def restart_backend(self) -> bool:
|
||||||
|
"""Restart cgminer hashing process. Wraps [`restart_cgminer`][pyasic.miners._backends.cgminer.CGMiner.restart_cgminer] to standardize."""
|
||||||
return await self.restart_cgminer()
|
return await self.restart_cgminer()
|
||||||
|
|
||||||
async def restart_cgminer(self) -> bool:
|
async def restart_cgminer(self) -> bool:
|
||||||
|
"""Restart cgminer hashing process."""
|
||||||
commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"]
|
commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"]
|
||||||
commands = ";".join(commands)
|
commands = ";".join(commands)
|
||||||
_ret = await self.send_ssh_command(commands)
|
try:
|
||||||
if isinstance(_ret, str):
|
_ret = await self.send_ssh_command(commands)
|
||||||
return True
|
except (asyncssh.Error, OSError):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if isinstance(_ret, str):
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def reboot(self) -> bool:
|
async def reboot(self) -> bool:
|
||||||
|
"""Reboots power to the physical miner."""
|
||||||
logging.debug(f"{self}: Sending reboot command.")
|
logging.debug(f"{self}: Sending reboot command.")
|
||||||
_ret = await self.send_ssh_command("reboot")
|
try:
|
||||||
logging.debug(f"{self}: Reboot command completed.")
|
_ret = await self.send_ssh_command("reboot")
|
||||||
if isinstance(_ret, str):
|
except (asyncssh.Error, OSError):
|
||||||
return True
|
return False
|
||||||
|
else:
|
||||||
|
logging.debug(f"{self}: Reboot command completed.")
|
||||||
|
if isinstance(_ret, str):
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def start_cgminer(self) -> None:
|
async def resume_mining(self) -> bool:
|
||||||
commands = [
|
try:
|
||||||
"mkdir -p /etc/tmp/",
|
commands = [
|
||||||
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
|
"mkdir -p /etc/tmp/",
|
||||||
"crontab -u root /etc/tmp/root",
|
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
|
||||||
"/usr/bin/cgminer-monitor >/dev/null 2>&1",
|
"crontab -u root /etc/tmp/root",
|
||||||
]
|
"/usr/bin/cgminer-monitor >/dev/null 2>&1",
|
||||||
commands = ";".join(commands)
|
]
|
||||||
await self.send_ssh_command(commands)
|
commands = ";".join(commands)
|
||||||
|
await self.send_ssh_command(commands)
|
||||||
|
except (asyncssh.Error, OSError):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
async def stop_cgminer(self) -> None:
|
async def stop_mining(self) -> bool:
|
||||||
commands = [
|
try:
|
||||||
"mkdir -p /etc/tmp/",
|
commands = [
|
||||||
'echo "" > /etc/tmp/root',
|
"mkdir -p /etc/tmp/",
|
||||||
"crontab -u root /etc/tmp/root",
|
'echo "" > /etc/tmp/root',
|
||||||
"killall cgminer",
|
"crontab -u root /etc/tmp/root",
|
||||||
]
|
"killall cgminer",
|
||||||
commands = ";".join(commands)
|
]
|
||||||
await self.send_ssh_command(commands)
|
commands = ";".join(commands)
|
||||||
|
await self.send_ssh_command(commands)
|
||||||
|
except (asyncssh.Error, OSError):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
async def get_config(self, api_pools: dict = None) -> MinerConfig:
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
# get pool data
|
||||||
command = "cat /etc/config/cgminer"
|
try:
|
||||||
result = await conn.run(command, check=True)
|
api_pools = await self.api.pools()
|
||||||
self.config = result.stdout
|
except APIError:
|
||||||
print(str(self.config))
|
pass
|
||||||
|
|
||||||
async def get_data(self):
|
if api_pools:
|
||||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
self.config = MinerConfig().from_api(api_pools["POOLS"])
|
||||||
|
return self.config
|
||||||
|
|
||||||
board_offset = -1
|
async def fault_light_off(self) -> bool:
|
||||||
fan_offset = -1
|
return False
|
||||||
|
|
||||||
model = await self.get_model()
|
async def fault_light_on(self) -> bool:
|
||||||
hostname = await self.get_hostname()
|
return False
|
||||||
mac = await self.get_mac()
|
|
||||||
|
|
||||||
if model:
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
data.model = model
|
return None
|
||||||
|
|
||||||
if hostname:
|
async def set_power_limit(self, wattage: int) -> bool:
|
||||||
data.hostname = hostname
|
return False
|
||||||
|
|
||||||
if mac:
|
##################################################
|
||||||
data.mac = mac
|
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||||
|
##################################################
|
||||||
|
|
||||||
miner_data = None
|
async def get_mac(self) -> Optional[str]:
|
||||||
for i in range(DATA_RETRIES):
|
return None
|
||||||
miner_data = await self.api.multicommand(
|
|
||||||
"summary", "pools", "stats", ignore_x19_error=True
|
|
||||||
)
|
|
||||||
if miner_data:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not miner_data:
|
async def get_model(self, api_devdetails: dict = None) -> Optional[str]:
|
||||||
return data
|
if self.model:
|
||||||
|
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||||
|
return self.model
|
||||||
|
|
||||||
summary = miner_data.get("summary")[0]
|
if not api_devdetails:
|
||||||
pools = miner_data.get("pools")[0]
|
try:
|
||||||
stats = miner_data.get("stats")[0]
|
api_devdetails = await self.api.devdetails()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
if summary:
|
if api_devdetails:
|
||||||
hr = summary.get("SUMMARY")
|
try:
|
||||||
if hr:
|
self.model = api_devdetails["DEVDETAILS"][0]["Model"].replace(
|
||||||
if len(hr) > 0:
|
"Antminer ", ""
|
||||||
hr = hr[0].get("GHS av")
|
)
|
||||||
if hr:
|
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||||
data.hashrate = round(hr / 1000, 2)
|
return self.model
|
||||||
|
except (TypeError, IndexError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
if stats:
|
logging.warning(f"Failed to get model for miner: {self}")
|
||||||
boards = stats.get("STATS")
|
return None
|
||||||
if boards:
|
|
||||||
if len(boards) > 0:
|
async def get_version(
|
||||||
|
self, api_version: dict = None
|
||||||
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
|
||||||
|
return miner_version(
|
||||||
|
api_ver=await self.get_api_ver(api_version=api_version),
|
||||||
|
fw_ver=await self.get_fw_ver(api_version=api_version),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_api_ver(self, api_version: dict = None) -> Optional[str]:
|
||||||
|
if self.api_ver:
|
||||||
|
return self.api_ver
|
||||||
|
|
||||||
|
if not api_version:
|
||||||
|
try:
|
||||||
|
api_version = await self.api.version()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_version:
|
||||||
|
try:
|
||||||
|
self.api_ver = api_version["VERSION"][0]["API"]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self.api_ver
|
||||||
|
|
||||||
|
async def get_fw_ver(self, api_version: dict = None) -> Optional[str]:
|
||||||
|
if self.fw_ver:
|
||||||
|
return self.fw_ver
|
||||||
|
|
||||||
|
if not api_version:
|
||||||
|
try:
|
||||||
|
api_version = await self.api.version()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_version:
|
||||||
|
try:
|
||||||
|
self.fw_ver = api_version["VERSION"][0]["CGMiner"]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self.fw_ver
|
||||||
|
|
||||||
|
async def get_hostname(self) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
hn = await self.send_ssh_command("cat /proc/sys/kernel/hostname")
|
||||||
|
except (asyncssh.Error, OSError):
|
||||||
|
return None
|
||||||
|
if hn:
|
||||||
|
self.hostname = hn
|
||||||
|
return self.hostname
|
||||||
|
|
||||||
|
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
|
||||||
|
# get hr from API
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
return round(
|
||||||
|
float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2
|
||||||
|
)
|
||||||
|
except (IndexError, KeyError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
|
||||||
|
hashboards = []
|
||||||
|
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
board_offset = -1
|
||||||
|
boards = api_stats["STATS"]
|
||||||
|
if len(boards) > 1:
|
||||||
for board_num in range(1, 16, 5):
|
for board_num in range(1, 16, 5):
|
||||||
for _b_num in range(5):
|
for _b_num in range(5):
|
||||||
b = boards[1].get(f"chain_acn{board_num + _b_num}")
|
b = boards[1].get(f"chain_acn{board_num + _b_num}")
|
||||||
@@ -163,96 +276,127 @@ class CGMiner(BaseMiner):
|
|||||||
if board_offset == -1:
|
if board_offset == -1:
|
||||||
board_offset = 1
|
board_offset = 1
|
||||||
|
|
||||||
data.left_chips = boards[1].get(f"chain_acn{board_offset}")
|
for i in range(board_offset, board_offset + self.ideal_hashboards):
|
||||||
data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
|
hashboard = HashBoard(
|
||||||
data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
|
slot=i - board_offset, expected_chips=self.nominal_chips
|
||||||
|
|
||||||
data.left_board_hashrate = round(
|
|
||||||
float(boards[1].get(f"chain_rate{board_offset}")) / 1000, 2
|
|
||||||
)
|
|
||||||
data.center_board_hashrate = round(
|
|
||||||
float(boards[1].get(f"chain_rate{board_offset+1}")) / 1000, 2
|
|
||||||
)
|
|
||||||
data.right_board_hashrate = round(
|
|
||||||
float(boards[1].get(f"chain_rate{board_offset+2}")) / 1000, 2
|
|
||||||
)
|
|
||||||
|
|
||||||
if stats:
|
|
||||||
temp = stats.get("STATS")
|
|
||||||
if temp:
|
|
||||||
if len(temp) > 1:
|
|
||||||
for fan_num in range(1, 8, 4):
|
|
||||||
for _f_num in range(4):
|
|
||||||
f = temp[1].get(f"fan{fan_num + _f_num}")
|
|
||||||
if f and not f == 0 and fan_offset == -1:
|
|
||||||
fan_offset = fan_num
|
|
||||||
if fan_offset == -1:
|
|
||||||
fan_offset = 1
|
|
||||||
for fan in range(self.fan_count):
|
|
||||||
setattr(
|
|
||||||
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
|
chip_temp = boards[1].get(f"temp{i}")
|
||||||
env_temp_list = []
|
if chip_temp:
|
||||||
for item in range(3):
|
hashboard.chip_temp = round(chip_temp)
|
||||||
board_temp = temp[1].get(f"temp{item + board_offset}")
|
|
||||||
chip_temp = temp[1].get(f"temp2_{item + board_offset}")
|
|
||||||
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
|
|
||||||
setattr(data, f"{board_map[item]}_temp", board_temp)
|
|
||||||
if f"temp_pcb{item}" in temp[1].keys():
|
|
||||||
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
|
|
||||||
if not env_temp == 0:
|
|
||||||
env_temp_list.append(int(env_temp))
|
|
||||||
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
|
||||||
|
|
||||||
if pools:
|
temp = boards[1].get(f"temp2_{i}")
|
||||||
pool_1 = None
|
if temp:
|
||||||
pool_2 = None
|
hashboard.temp = round(temp)
|
||||||
pool_1_user = None
|
|
||||||
pool_2_user = None
|
|
||||||
pool_1_quota = 1
|
|
||||||
pool_2_quota = 1
|
|
||||||
quota = 0
|
|
||||||
for pool in pools.get("POOLS"):
|
|
||||||
if not pool_1_user:
|
|
||||||
pool_1_user = pool.get("User")
|
|
||||||
pool_1 = pool["URL"]
|
|
||||||
if pool.get("Quota"):
|
|
||||||
pool_2_quota = pool.get("Quota")
|
|
||||||
elif not pool_2_user:
|
|
||||||
pool_2_user = pool.get("User")
|
|
||||||
pool_2 = pool["URL"]
|
|
||||||
if pool.get("Quota"):
|
|
||||||
pool_2_quota = pool.get("Quota")
|
|
||||||
if not pool.get("User") == pool_1_user:
|
|
||||||
if not pool_2_user == pool.get("User"):
|
|
||||||
pool_2_user = pool.get("User")
|
|
||||||
pool_2 = pool["URL"]
|
|
||||||
if pool.get("Quota"):
|
|
||||||
pool_2_quota = pool.get("Quota")
|
|
||||||
if pool_2_user and not pool_2_user == pool_1_user:
|
|
||||||
quota = f"{pool_1_quota}/{pool_2_quota}"
|
|
||||||
|
|
||||||
if pool_1:
|
hashrate = boards[1].get(f"chain_rate{i}")
|
||||||
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
|
if hashrate:
|
||||||
"stratum2+tcp://", ""
|
hashboard.hashrate = round(float(hashrate) / 1000, 2)
|
||||||
)
|
|
||||||
data.pool_1_url = pool_1
|
|
||||||
|
|
||||||
if pool_1_user:
|
chips = boards[1].get(f"chain_acn{i}")
|
||||||
data.pool_1_user = pool_1_user
|
if chips:
|
||||||
|
hashboard.chips = chips
|
||||||
|
hashboard.missing = False
|
||||||
|
if (not chips) or (not chips > 0):
|
||||||
|
hashboard.missing = True
|
||||||
|
hashboards.append(hashboard)
|
||||||
|
except (IndexError, KeyError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
if pool_2:
|
return hashboards
|
||||||
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
|
|
||||||
"stratum2+tcp://", ""
|
|
||||||
)
|
|
||||||
data.pool_2_url = pool_2
|
|
||||||
|
|
||||||
if pool_2_user:
|
async def get_env_temp(self) -> Optional[float]:
|
||||||
data.pool_2_user = pool_2_user
|
return None
|
||||||
|
|
||||||
if quota:
|
async def get_wattage(self) -> Optional[int]:
|
||||||
data.pool_split = str(quota)
|
return None
|
||||||
|
|
||||||
return data
|
async def get_wattage_limit(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_fans(self, api_stats: dict = None) -> List[Fan]:
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
fans_data = [Fan(), Fan(), Fan(), Fan()]
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
fan_offset = -1
|
||||||
|
|
||||||
|
for fan_num in range(1, 8, 4):
|
||||||
|
for _f_num in range(4):
|
||||||
|
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}")
|
||||||
|
if f and not f == 0 and fan_offset == -1:
|
||||||
|
fan_offset = fan_num
|
||||||
|
if fan_offset == -1:
|
||||||
|
fan_offset = 1
|
||||||
|
|
||||||
|
for fan in range(self.fan_count):
|
||||||
|
fans_data[fan] = Fan(
|
||||||
|
api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
|
||||||
|
)
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
return fans_data
|
||||||
|
|
||||||
|
async def get_fan_psu(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_pools(self, api_pools: dict = None) -> List[dict]:
|
||||||
|
groups = []
|
||||||
|
|
||||||
|
if not api_pools:
|
||||||
|
try:
|
||||||
|
api_pools = await self.api.pools()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_pools:
|
||||||
|
try:
|
||||||
|
pools = {}
|
||||||
|
for i, pool in enumerate(api_pools["POOLS"]):
|
||||||
|
pools[f"pool_{i + 1}_url"] = (
|
||||||
|
pool["URL"]
|
||||||
|
.replace("stratum+tcp://", "")
|
||||||
|
.replace("stratum2+tcp://", "")
|
||||||
|
)
|
||||||
|
pools[f"pool_{i + 1}_user"] = pool["User"]
|
||||||
|
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
|
||||||
|
|
||||||
|
groups.append(pools)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return groups
|
||||||
|
|
||||||
|
async def get_errors(self) -> List[MinerErrorData]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_fault_light(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
|
||||||
|
# X19 method, not sure compatibility
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
|
||||||
|
try:
|
||||||
|
rate_unit = api_stats["STATS"][1]["rate_unit"]
|
||||||
|
except KeyError:
|
||||||
|
rate_unit = "GH"
|
||||||
|
if rate_unit == "GH":
|
||||||
|
return round(ideal_rate / 1000, 2)
|
||||||
|
if rate_unit == "MH":
|
||||||
|
return round(ideal_rate / 1000000, 2)
|
||||||
|
else:
|
||||||
|
return round(ideal_rate, 2)
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|||||||
272
pyasic/miners/_backends/cgminer_avalon.py
Normal file
272
pyasic/miners/_backends/cgminer_avalon.py
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from collections import namedtuple
|
||||||
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from pyasic.API.cgminer import CGMinerAPI
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
from pyasic.data import Fan, HashBoard, MinerData
|
||||||
|
from pyasic.data.error_codes import MinerErrorData
|
||||||
|
from pyasic.errors import APIError
|
||||||
|
from pyasic.miners._backends import CGMiner
|
||||||
|
from pyasic.miners.base import BaseMiner
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
|
||||||
|
class CGMinerAvalon(CGMiner):
|
||||||
|
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
|
||||||
|
super().__init__(ip, api_ver)
|
||||||
|
self.ip = ip
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.ascset(0, "led", "1-1")
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data["STATUS"][0]["Msg"] == "ASC 0 set OK":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.ascset(0, "led", "1-0")
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data["STATUS"][0]["Msg"] == "ASC 0 set OK":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.restart()
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if data["STATUS"] == "RESTART":
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def stop_mining(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def resume_mining(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
"""Configures miner with yaml config."""
|
||||||
|
self.config = config
|
||||||
|
return None
|
||||||
|
logging.debug(f"{self}: Sending config.") # noqa - This doesnt work...
|
||||||
|
conf = config.as_avalon(user_suffix=user_suffix)
|
||||||
|
try:
|
||||||
|
data = await self.api.ascset(
|
||||||
|
0, "setpool", f"root,root,{conf}"
|
||||||
|
) # this should work but doesn't
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
# return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_stats(stats):
|
||||||
|
_stats_items = re.findall(".+?\[*?]", stats)
|
||||||
|
stats_items = []
|
||||||
|
stats_dict = {}
|
||||||
|
for item in _stats_items:
|
||||||
|
if ":" in item:
|
||||||
|
data = item.replace("]", "").split("[")
|
||||||
|
data_list = [i.split(": ") for i in data[1].strip().split(", ")]
|
||||||
|
data_dict = {}
|
||||||
|
for key, val in [tuple(item) for item in data_list]:
|
||||||
|
data_dict[key] = val
|
||||||
|
raw_data = [data[0].strip(), data_dict]
|
||||||
|
else:
|
||||||
|
raw_data = [
|
||||||
|
value
|
||||||
|
for value in item.replace("[", " ")
|
||||||
|
.replace("]", " ")
|
||||||
|
.split(" ")[:-1]
|
||||||
|
if value != ""
|
||||||
|
]
|
||||||
|
if len(raw_data) == 1:
|
||||||
|
raw_data.append("")
|
||||||
|
if raw_data[0] == "":
|
||||||
|
raw_data = raw_data[1:]
|
||||||
|
|
||||||
|
if len(raw_data) == 2:
|
||||||
|
stats_dict[raw_data[0]] = raw_data[1]
|
||||||
|
else:
|
||||||
|
stats_dict[raw_data[0]] = raw_data[1:]
|
||||||
|
stats_items.append(raw_data)
|
||||||
|
|
||||||
|
return stats_dict
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
async def get_mac(self, api_version: dict = None) -> Optional[str]:
|
||||||
|
if not api_version:
|
||||||
|
try:
|
||||||
|
api_version = await self.api.version()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_version:
|
||||||
|
try:
|
||||||
|
base_mac = api_version["VERSION"][0]["MAC"]
|
||||||
|
base_mac = base_mac.upper()
|
||||||
|
mac = ":".join(
|
||||||
|
[base_mac[i : (i + 2)] for i in range(0, len(base_mac), 2)]
|
||||||
|
)
|
||||||
|
return mac
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_hostname(self, mac: str = None) -> Optional[str]:
|
||||||
|
if not mac:
|
||||||
|
mac = await self.get_mac()
|
||||||
|
|
||||||
|
if mac:
|
||||||
|
return f"Avalon{mac.replace(':', '')[-6:]}"
|
||||||
|
|
||||||
|
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
return round(float(api_summary["SUMMARY"][0]["MHS 1m"] / 1000000), 2)
|
||||||
|
except (KeyError, IndexError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
|
||||||
|
hashboards = [
|
||||||
|
HashBoard(slot=i, expected_chips=self.nominal_chips)
|
||||||
|
for i in range(self.ideal_hashboards)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
stats_data = api_stats[0].get("STATS")
|
||||||
|
if stats_data:
|
||||||
|
for key in stats_data[0].keys():
|
||||||
|
if key.startswith("MM ID"):
|
||||||
|
raw_data = self.parse_stats(stats_data[0][key])
|
||||||
|
for board in range(self.ideal_hashboards):
|
||||||
|
chip_temp = raw_data.get("MTmax")
|
||||||
|
if chip_temp:
|
||||||
|
hashboards[board].chip_temp = chip_temp[board]
|
||||||
|
|
||||||
|
temp = raw_data.get("MTavg")
|
||||||
|
if temp:
|
||||||
|
hashboards[board].temp = temp[board]
|
||||||
|
|
||||||
|
chips = raw_data.get(f"PVT_T{board}")
|
||||||
|
if chips:
|
||||||
|
hashboards[board].chips = len(
|
||||||
|
[item for item in chips if not item == "0"]
|
||||||
|
)
|
||||||
|
except (IndexError, KeyError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return hashboards
|
||||||
|
|
||||||
|
async def get_env_temp(self) -> Optional[float]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_wattage(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_wattage_limit(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_fans(self, api_stats: dict = None) -> List[Fan]:
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
fans_data = [Fan(), Fan(), Fan(), Fan()]
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
stats_data = api_stats[0].get("STATS")
|
||||||
|
if stats_data:
|
||||||
|
for key in stats_data[0].keys():
|
||||||
|
if key.startswith("MM ID"):
|
||||||
|
raw_data = self.parse_stats(stats_data[0][key])
|
||||||
|
for fan in range(self.fan_count):
|
||||||
|
fans_data[fan] = Fan(int(raw_data[f"Fan{fan + 1}"]))
|
||||||
|
except (KeyError, IndexError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return fans_data
|
||||||
|
|
||||||
|
async def get_pools(self, api_pools: dict = None) -> List[dict]:
|
||||||
|
groups = []
|
||||||
|
|
||||||
|
if not api_pools:
|
||||||
|
try:
|
||||||
|
api_pools = await self.api.pools()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_pools:
|
||||||
|
try:
|
||||||
|
pools = {}
|
||||||
|
for i, pool in enumerate(api_pools["POOLS"]):
|
||||||
|
pools[f"pool_{i + 1}_url"] = (
|
||||||
|
pool["URL"]
|
||||||
|
.replace("stratum+tcp://", "")
|
||||||
|
.replace("stratum2+tcp://", "")
|
||||||
|
)
|
||||||
|
pools[f"pool_{i + 1}_user"] = pool["User"]
|
||||||
|
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
|
||||||
|
|
||||||
|
groups.append(pools)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return groups
|
||||||
|
|
||||||
|
async def get_errors(self) -> List[MinerErrorData]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_fault_light(self) -> bool:
|
||||||
|
if self.light:
|
||||||
|
return self.light
|
||||||
|
try:
|
||||||
|
data = await self.api.ascset(0, "led", "1-255")
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data["STATUS"][0]["Msg"] == "ASC 0 set info: LED[1]":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
@@ -1,48 +1,28 @@
|
|||||||
from pyasic.miners._backends import BMMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
|
||||||
|
from pyasic.miners._backends import BMMiner
|
||||||
|
|
||||||
|
|
||||||
class Hiveon(BMMiner):
|
class Hiveon(BMMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
|
||||||
super().__init__(ip)
|
super().__init__(ip, api_ver)
|
||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ipaddress.ip_address(ip)
|
||||||
self.api_type = "Hiveon"
|
self.api_type = "Hiveon"
|
||||||
self.uname = "root"
|
self.uname = "root"
|
||||||
self.pwd = "admin"
|
self.pwd = "admin"
|
||||||
|
|
||||||
async def get_board_info(self) -> dict:
|
|
||||||
"""Gets data on each board and chain in the miner."""
|
|
||||||
board_stats = await self.api.stats()
|
|
||||||
stats = board_stats["STATS"][1]
|
|
||||||
boards = {}
|
|
||||||
board_chains = {0: [2, 9, 10], 1: [3, 11, 12], 2: [4, 13, 14]}
|
|
||||||
for idx, board in enumerate(board_chains):
|
|
||||||
boards[board] = []
|
|
||||||
for chain in board_chains[board]:
|
|
||||||
count = stats[f"chain_acn{chain}"]
|
|
||||||
chips = stats[f"chain_acs{chain}"].replace(" ", "")
|
|
||||||
if not count == 18 or "x" in chips:
|
|
||||||
nominal = False
|
|
||||||
else:
|
|
||||||
nominal = True
|
|
||||||
boards[board].append(
|
|
||||||
{
|
|
||||||
"chain": chain,
|
|
||||||
"chip_count": count,
|
|
||||||
"chip_status": chips,
|
|
||||||
"nominal": nominal,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return boards
|
|
||||||
|
|
||||||
async def get_bad_boards(self) -> dict:
|
|
||||||
"""Checks for and provides list of non working boards."""
|
|
||||||
boards = await self.get_board_info()
|
|
||||||
bad_boards = {}
|
|
||||||
for board in boards.keys():
|
|
||||||
for chain in boards[board]:
|
|
||||||
if not chain["chip_count"] == 18 or "x" in chain["chip_status"]:
|
|
||||||
if board not in bad_boards.keys():
|
|
||||||
bad_boards[board] = []
|
|
||||||
bad_boards[board].append(chain)
|
|
||||||
return bad_boards
|
|
||||||
|
|||||||
218
pyasic/miners/_backends/vnish.py
Normal file
218
pyasic/miners/_backends/vnish.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import warnings
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from pyasic.errors import APIError
|
||||||
|
from pyasic.miners._backends.bmminer import BMMiner
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
|
||||||
|
class VNish(BMMiner):
|
||||||
|
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
|
||||||
|
super().__init__(ip, api_ver)
|
||||||
|
self.api_type = "VNish"
|
||||||
|
self.uname = "root"
|
||||||
|
self.pwd = PyasicSettings().global_vnish_password
|
||||||
|
self.jwt = None
|
||||||
|
|
||||||
|
async def auth(self):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
auth = await client.post(
|
||||||
|
f"http://{self.ip}/api/v1/unlock",
|
||||||
|
json={"pw": self.pwd},
|
||||||
|
)
|
||||||
|
except httpx.HTTPError:
|
||||||
|
warnings.warn(f"Could not authenticate web token with miner: {self}")
|
||||||
|
else:
|
||||||
|
if not auth.status_code == 200:
|
||||||
|
warnings.warn(
|
||||||
|
f"Could not authenticate web token with miner: {self}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
json_auth = auth.json()
|
||||||
|
self.jwt = json_auth["token"]
|
||||||
|
return self.jwt
|
||||||
|
|
||||||
|
async def send_web_command(
|
||||||
|
self, command: str, data: Union[dict, None] = None, method: str = "GET"
|
||||||
|
):
|
||||||
|
if not self.jwt:
|
||||||
|
await self.auth()
|
||||||
|
if not data:
|
||||||
|
data = {}
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
for i in range(PyasicSettings().miner_get_data_retries):
|
||||||
|
try:
|
||||||
|
auth = self.jwt
|
||||||
|
if command.startswith("system"):
|
||||||
|
auth = "Bearer " + self.jwt
|
||||||
|
if method == "GET":
|
||||||
|
response = await client.get(
|
||||||
|
f"http://{self.ip}/api/v1/{command}",
|
||||||
|
headers={"Authorization": auth},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
elif method == "POST":
|
||||||
|
if data:
|
||||||
|
response = await client.post(
|
||||||
|
f"http://{self.ip}/api/v1/{command}",
|
||||||
|
headers={"Authorization": auth},
|
||||||
|
timeout=5,
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = await client.post(
|
||||||
|
f"http://{self.ip}/api/v1/{command}",
|
||||||
|
headers={"Authorization": auth},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise APIError("Bad method type.")
|
||||||
|
if not response.status_code == 200:
|
||||||
|
# refresh the token, retry
|
||||||
|
await self.auth()
|
||||||
|
continue
|
||||||
|
json_data = response.json()
|
||||||
|
if json_data:
|
||||||
|
return json_data
|
||||||
|
return True
|
||||||
|
except httpx.HTTPError:
|
||||||
|
pass
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_model(self, api_stats: dict = None) -> Optional[str]:
|
||||||
|
# check if model is cached
|
||||||
|
if self.model:
|
||||||
|
logging.debug(f"Found model for {self.ip}: {self.model} (VNish)")
|
||||||
|
return self.model + " (VNish)"
|
||||||
|
|
||||||
|
if not api_stats:
|
||||||
|
try:
|
||||||
|
api_stats = await self.api.stats()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_stats:
|
||||||
|
try:
|
||||||
|
m_type = api_stats["STATS"][0]["Type"]
|
||||||
|
self.model = m_type.split(" ")[1]
|
||||||
|
return self.model
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
data = await self.send_web_command("mining/restart", method="POST")
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
data = await self.send_web_command("system/reboot", method="POST")
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def get_mac(self, web_summary: dict = None) -> str:
|
||||||
|
if not web_summary:
|
||||||
|
web_info = await self.send_web_command("info")
|
||||||
|
|
||||||
|
if web_info:
|
||||||
|
try:
|
||||||
|
mac = web_info["system"]["network_status"]["mac"]
|
||||||
|
return mac
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if web_summary:
|
||||||
|
try:
|
||||||
|
mac = web_summary["system"]["network_status"]["mac"]
|
||||||
|
return mac
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_hostname(self, web_summary: dict = None) -> str:
|
||||||
|
if not web_summary:
|
||||||
|
web_info = await self.send_web_command("info")
|
||||||
|
|
||||||
|
if web_info:
|
||||||
|
try:
|
||||||
|
hostname = web_info["system"]["network_status"]["hostname"]
|
||||||
|
return hostname
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if web_summary:
|
||||||
|
try:
|
||||||
|
hostname = web_summary["system"]["network_status"]["hostname"]
|
||||||
|
return hostname
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_wattage(self, web_summary: dict = None) -> Optional[int]:
|
||||||
|
if not web_summary:
|
||||||
|
web_summary = await self.send_web_command("summary")
|
||||||
|
|
||||||
|
if web_summary:
|
||||||
|
try:
|
||||||
|
wattage = web_summary["miner"]["power_usage"]
|
||||||
|
wattage = round(wattage * 1000)
|
||||||
|
return wattage
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
|
||||||
|
# get hr from API
|
||||||
|
if not api_summary:
|
||||||
|
try:
|
||||||
|
api_summary = await self.api.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if api_summary:
|
||||||
|
try:
|
||||||
|
return round(
|
||||||
|
float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2
|
||||||
|
)
|
||||||
|
except (IndexError, KeyError, ValueError, TypeError) as e:
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_wattage_limit(self, web_settings: dict = None) -> Optional[int]:
|
||||||
|
if not web_settings:
|
||||||
|
web_settings = await self.send_web_command("summary")
|
||||||
|
|
||||||
|
if web_settings:
|
||||||
|
try:
|
||||||
|
wattage_limit = web_settings["miner"]["overclock"]["preset"]
|
||||||
|
return int(wattage_limit)
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_fw_ver(self, web_summary: dict = None) -> Optional[str]:
|
||||||
|
if not web_summary:
|
||||||
|
web_summary = await self.send_web_command("summary")
|
||||||
|
|
||||||
|
if web_summary:
|
||||||
|
try:
|
||||||
|
fw_ver = web_summary["miner"]["miner_type"]
|
||||||
|
fw_ver = fw_ver.split("(Vnish ")[1].replace(")", "")
|
||||||
|
return fw_ver
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
@@ -1,3 +1,20 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .antminer import *
|
from .antminer import *
|
||||||
from .avalonminer import *
|
from .avalonminer import *
|
||||||
|
from .innosilicon import *
|
||||||
from .whatsminer import *
|
from .whatsminer import *
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S17(BaseMiner):
|
class S17(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S17Plus(BaseMiner):
|
class S17Plus(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S17Pro(BaseMiner):
|
class S17Pro(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S17e(BaseMiner):
|
class S17e(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class T17(BaseMiner):
|
class T17(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class T17Plus(BaseMiner):
|
class T17Plus(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class T17e(BaseMiner):
|
class T17e(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .S17 import S17
|
from .S17 import S17
|
||||||
from .S17_Plus import S17Plus
|
from .S17_Plus import S17Plus
|
||||||
from .S17_Pro import S17Pro
|
from .S17_Pro import S17Pro
|
||||||
from .S17e import S17e
|
from .S17e import S17e
|
||||||
|
|
||||||
from .T17 import T17
|
from .T17 import T17
|
||||||
from .T17_Plus import T17Plus
|
from .T17_Plus import T17Plus
|
||||||
from .T17e import T17e
|
from .T17e import T17e
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S19(BaseMiner):
|
class S19(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S19Pro(BaseMiner):
|
class S19Pro(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
26
pyasic/miners/_types/antminer/X19/S19_XP.py
Normal file
26
pyasic/miners/_types/antminer/X19/S19_XP.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S19XP(AntMiner): # noqa - ignore ABC method implementation
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S19 XP"
|
||||||
|
self.nominal_chips = 110
|
||||||
|
self.fan_count = 4
|
||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S19a(BaseMiner):
|
class S19a(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
26
pyasic/miners/_types/antminer/X19/S19a_Pro.py
Normal file
26
pyasic/miners/_types/antminer/X19/S19a_Pro.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S19aPro(AntMiner): # noqa - ignore ABC method implementation
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S19a Pro"
|
||||||
|
self.nominal_chips = 100
|
||||||
|
self.fan_count = 4
|
||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S19j(BaseMiner):
|
class S19j(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S19jPro(BaseMiner):
|
class S19jPro(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class T19(BaseMiner):
|
class T19(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .S19 import S19
|
from .S19 import S19
|
||||||
from .S19_Pro import S19Pro
|
from .S19_Pro import S19Pro
|
||||||
|
from .S19_XP import S19XP
|
||||||
|
from .S19a import S19a
|
||||||
|
from .S19a_Pro import S19aPro
|
||||||
from .S19j import S19j
|
from .S19j import S19j
|
||||||
from .S19j_Pro import S19jPro
|
from .S19j_Pro import S19jPro
|
||||||
|
|
||||||
from .S19a import S19a
|
|
||||||
|
|
||||||
from .T19 import T19
|
from .T19 import T19
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S9(BaseMiner):
|
class S9(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class S9i(BaseMiner):
|
class S9i(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AntMiner
|
||||||
|
|
||||||
|
|
||||||
class T9(BaseMiner):
|
class T9(AntMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "T9"
|
self.model = "T9"
|
||||||
self.nominal_chips = 57
|
self.nominal_chips = 54
|
||||||
self.fan_count = 2
|
self.fan_count = 2
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .S9 import S9
|
from .S9 import S9
|
||||||
from .S9i import S9i
|
from .S9i import S9i
|
||||||
from .T9 import T9
|
from .T9 import T9
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .X9 import *
|
from .X9 import *
|
||||||
from .X17 import *
|
from .X17 import *
|
||||||
from .X19 import *
|
from .X19 import *
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon1026(BaseMiner):
|
class Avalon1026(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon1047(BaseMiner):
|
class Avalon1047(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon1066(BaseMiner):
|
class Avalon1066(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .A1026 import Avalon1026
|
from .A1026 import Avalon1026
|
||||||
from .A1047 import Avalon1047
|
from .A1047 import Avalon1047
|
||||||
from .A1066 import Avalon1066
|
from .A1066 import Avalon1066
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon721(BaseMiner):
|
class Avalon721(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "Avalon 721"
|
self.model = "Avalon 721"
|
||||||
self.chip_count = 18 # This miner has 4 boards totaling 72
|
self.ideal_hashboards = 4
|
||||||
self.fan_count = 1 # also only 1 fan
|
self.chip_count = 18
|
||||||
|
self.fan_count = 1
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon741(BaseMiner):
|
class Avalon741(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "Avalon 741"
|
self.model = "Avalon 741"
|
||||||
self.chip_count = 22 # This miner has 4 boards totaling 88
|
self.ideal_hashboards = 4
|
||||||
self.fan_count = 1 # also only 1 fan
|
self.chip_count = 22
|
||||||
|
self.fan_count = 1
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon761(BaseMiner):
|
class Avalon761(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "Avalon 761"
|
self.model = "Avalon 761"
|
||||||
self.chip_count = 18 # This miner has 4 boards totaling 72
|
self.ideal_hashboards = 4
|
||||||
self.fan_count = 1 # also only 1 fan
|
self.chip_count = 18
|
||||||
|
self.fan_count = 1
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .A721 import Avalon721
|
from .A721 import Avalon721
|
||||||
from .A741 import Avalon741
|
from .A741 import Avalon741
|
||||||
from .A761 import Avalon761
|
from .A761 import Avalon761
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon821(BaseMiner):
|
class Avalon821(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "Avalon 821"
|
self.model = "Avalon 821"
|
||||||
self.chip_count = 26 # This miner has 4 boards totaling 104
|
self.ideal_hashboards = 4
|
||||||
self.fan_count = 1 # also only 1 fan
|
self.chip_count = 26
|
||||||
|
self.fan_count = 1
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon841(BaseMiner):
|
class Avalon841(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "Avalon 841"
|
self.model = "Avalon 841"
|
||||||
self.chip_count = 26 # This miner has 4 boards totaling 104
|
self.ideal_hashboards = 4
|
||||||
self.fan_count = 1 # also only 1 fan
|
self.chip_count = 26
|
||||||
|
self.fan_count = 1
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon851(BaseMiner):
|
class Avalon851(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "Avalon 851"
|
self.model = "Avalon 851"
|
||||||
self.chip_count = 26 # This miner has 4 boards totaling 104
|
self.ideal_hashboards = 4
|
||||||
self.fan_count = 1 # also only 1 fan
|
self.chip_count = 26
|
||||||
|
self.fan_count = 1
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .A821 import Avalon821
|
from .A821 import Avalon821
|
||||||
from .A841 import Avalon841
|
from .A841 import Avalon841
|
||||||
from .A851 import Avalon851
|
from .A851 import Avalon851
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
from pyasic.miners import BaseMiner
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import AvalonMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon921(BaseMiner):
|
class Avalon921(AvalonMiner): # noqa - ignore ABC method implementation
|
||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "Avalon 921"
|
self.model = "Avalon 921"
|
||||||
self.chip_count = 26 # This miner has 4 boards totaling 104
|
self.ideal_hashboards = 4
|
||||||
self.fan_count = 1 # also only 1 fan
|
self.chip_count = 26
|
||||||
|
self.fan_count = 1
|
||||||
|
|||||||
@@ -1 +1,17 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .A921 import Avalon921
|
from .A921 import Avalon921
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
from .A7X import *
|
from .A7X import *
|
||||||
from .A8X import *
|
from .A8X import *
|
||||||
from .A9X import *
|
from .A9X import *
|
||||||
|
|||||||
26
pyasic/miners/_types/innosilicon/T3X/T3H_Plus.py
Normal file
26
pyasic/miners/_types/innosilicon/T3X/T3H_Plus.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from pyasic.miners._types.makes import InnosiliconMiner
|
||||||
|
|
||||||
|
|
||||||
|
class InnosiliconT3HPlus(InnosiliconMiner): # noqa - ignore ABC method implementation
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "T3H+"
|
||||||
|
self.nominal_chips = 114
|
||||||
|
self.fan_count = 4
|
||||||
17
pyasic/miners/_types/innosilicon/T3X/__init__.py
Normal file
17
pyasic/miners/_types/innosilicon/T3X/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright 2022 Upstream Data Inc -
|
||||||
|
# -
|
||||||
|
# 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. -
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from .T3H_Plus import InnosiliconT3HPlus
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user