Compare commits
510 Commits
first-rele
...
v0.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6c87a864d | ||
|
|
ed17a0f436 | ||
|
|
36fead3dd1 | ||
|
|
ecb16c10ca | ||
|
|
a540db3246 | ||
|
|
81a2f99fbf | ||
|
|
1dd9f742ad | ||
|
|
7bd6a0f136 | ||
|
|
7297f12e88 | ||
|
|
0e009c3a16 | ||
|
|
95b0cc364b | ||
|
|
2dcc4f0cfc | ||
|
|
d7e9498018 | ||
|
|
0324a21e79 | ||
|
|
5700bd1c9c | ||
|
|
abc6494f18 | ||
|
|
5de8fc064e | ||
|
|
c9d620105b | ||
|
|
5d6fc5b26d | ||
|
|
6bd319355d | ||
|
|
31827e7dd1 | ||
|
|
26961a5d8c | ||
|
|
2ff09a3765 | ||
|
|
18c26adbb6 | ||
|
|
4bfafabe9d | ||
|
|
19e6ed90ec | ||
|
|
eca60d1eae | ||
|
|
8b8a592308 | ||
|
|
c3de4188d6 | ||
|
|
490138fd1a | ||
|
|
f566b7fcb9 | ||
|
|
7fb4237e51 | ||
|
|
eeffdecde1 | ||
|
|
477a411c87 | ||
|
|
ca77573624 | ||
|
|
3ec147990b | ||
|
|
082240bdb6 | ||
|
|
7a7fc2c5a6 | ||
|
|
dcc3e07998 | ||
|
|
5261b00aad | ||
|
|
f18d37a19e | ||
|
|
7c3af3da41 | ||
|
|
8948af55f2 | ||
|
|
dd8fe41ad1 | ||
|
|
198eedcd43 | ||
|
|
f7309decdb | ||
|
|
078579d8e1 | ||
|
|
39eeb13409 | ||
|
|
dfccd67ccb | ||
|
|
10949225c0 | ||
|
|
3a60a3584a | ||
|
|
480aab550c | ||
|
|
fa83e61249 | ||
|
|
2f3411e12d | ||
|
|
3e7311687e | ||
|
|
bc2d549ce5 | ||
|
|
3d31d89c9e | ||
|
|
15fc27e6fa | ||
|
|
943ebc77a1 | ||
|
|
733437ef03 | ||
|
|
b444245e98 | ||
|
|
481d31a0f1 | ||
|
|
264db3bdd6 | ||
|
|
d292b9c195 | ||
|
|
dce25a679f | ||
|
|
c903631742 | ||
|
|
e70bfdc886 | ||
|
|
8e1803add1 | ||
|
|
7d61056ea3 | ||
|
|
0d497baa45 | ||
|
|
d3a71c5a93 | ||
|
|
895a5b7ac8 | ||
|
|
7a5a0b287c | ||
|
|
c7d73276c8 | ||
|
|
4bbb9d0b08 | ||
|
|
3ee49e6fd7 | ||
|
|
dcd3e99d73 | ||
|
|
64018cdad8 | ||
|
|
e7d269008c | ||
|
|
7dfe25e5d2 | ||
|
|
382f9cff76 | ||
|
|
a5195ff1db | ||
|
|
b1ec726d18 | ||
|
|
5ae2cb2b22 | ||
|
|
472a15f4ca | ||
|
|
7cc7973587 | ||
|
|
ab964e4c88 | ||
|
|
4087874b4a | ||
|
|
844deec0d3 | ||
|
|
d36eef4c33 | ||
|
|
69d4ee5570 | ||
|
|
e6d3ec01fe | ||
|
|
e7b01ccdab | ||
|
|
38506903ea | ||
|
|
c9a1560052 | ||
|
|
88f8ff10b7 | ||
|
|
11d38c9c3b | ||
|
|
0082037f45 | ||
|
|
dd5ccafa1e | ||
|
|
739126935a | ||
|
|
5c850a43a9 | ||
|
|
24b037f273 | ||
|
|
f847700c05 | ||
|
|
69820dd9d2 | ||
|
|
ad4b710cb7 | ||
|
|
c53c18654b | ||
|
|
18797f4b56 | ||
|
|
e86c93e287 | ||
|
|
89cfde28f5 | ||
|
|
0f2a867828 | ||
|
|
4f5aef2d45 | ||
|
|
96801f93d1 | ||
|
|
a8ce73c3d6 | ||
|
|
513dd2b981 | ||
|
|
c35b30e949 | ||
|
|
942f2a1c8d | ||
|
|
9078df680e | ||
|
|
527997cc58 | ||
|
|
41433bcaf5 | ||
|
|
3451b88669 | ||
|
|
a42af2764e | ||
|
|
baaad73eb8 | ||
|
|
34c9f85098 | ||
|
|
d6638fa4d2 | ||
|
|
0f51487d3f | ||
|
|
3a11b173c3 | ||
|
|
568f86700b | ||
|
|
3b702aac2c | ||
|
|
6fbd9faffd | ||
|
|
9eb2259aae | ||
|
|
149c386a4c | ||
|
|
726e7ff0f0 | ||
|
|
87a690eb00 | ||
|
|
fd5dba4036 | ||
|
|
e54847337a | ||
|
|
3ff43c3ccd | ||
|
|
ec5563f2f0 | ||
|
|
40f14876cc | ||
|
|
6abfe8a503 | ||
|
|
0a4d52ef03 | ||
|
|
e4207e0120 | ||
|
|
ed89476866 | ||
|
|
7f7964526c | ||
|
|
85b282740a | ||
|
|
8cbf3a20a3 | ||
|
|
8ebcbd3c33 | ||
|
|
c3e285a9ee | ||
|
|
9f19b42de5 | ||
|
|
3d265e823b | ||
|
|
5e6bc8c8ef | ||
|
|
871499b77f | ||
|
|
117a161fd5 | ||
|
|
40bacbf41c | ||
|
|
e091863aa7 | ||
|
|
85e8ac63f1 | ||
|
|
a5252e3a84 | ||
|
|
404d6590db | ||
|
|
1d04399daf | ||
|
|
03ebcacca5 | ||
|
|
75934fd7fe | ||
|
|
bbeca15799 | ||
|
|
45befb569b | ||
|
|
61334ed99e | ||
|
|
2bf059df01 | ||
|
|
9c2de26182 | ||
|
|
714983cddc | ||
|
|
191f1d24b9 | ||
|
|
5a0bafb964 | ||
|
|
67aedd319d | ||
|
|
44012c50d6 | ||
|
|
06540efc98 | ||
|
|
9d0d1a24d9 | ||
|
|
8568f91482 | ||
|
|
64918e5552 | ||
|
|
53d5ecd04a | ||
|
|
1b0e80a418 | ||
|
|
9ad506a313 | ||
|
|
18c4bbd09c | ||
|
|
0d123d5dd8 | ||
|
|
b9b91293fe | ||
|
|
47a702c94c | ||
|
|
6d5a288120 | ||
|
|
038aae95ac | ||
|
|
dd84aede25 | ||
|
|
dc8ad271de | ||
|
|
b78c1cdca5 | ||
|
|
0eb7ced932 | ||
|
|
8e58f4492f | ||
|
|
95fb32de19 | ||
|
|
5145dc19f8 | ||
|
|
1808d62bba | ||
|
|
97ef4dfe37 | ||
|
|
174a132e75 | ||
|
|
84d6e58ebe | ||
|
|
e9a1483e5f | ||
|
|
4eb51eed20 | ||
|
|
066fc1a4b3 | ||
|
|
cc24236c0a | ||
|
|
564cd42eae | ||
|
|
8677eff491 | ||
|
|
63a21ea9aa | ||
|
|
1c9d3dc84d | ||
|
|
0dacd3d294 | ||
|
|
6fa74613b4 | ||
|
|
f7fb7a3acb | ||
|
|
666c5bfc64 | ||
|
|
1f8d92f6bb | ||
|
|
ef336a9e23 | ||
|
|
7fe6fd47fb | ||
|
|
91a0298d96 | ||
|
|
ed3d8fc815 | ||
|
|
4f2d630746 | ||
|
|
a8c685a883 | ||
|
|
09660e1934 | ||
|
|
c01908ff9a | ||
|
|
267c388a95 | ||
|
|
8215d33241 | ||
|
|
f4258a304a | ||
|
|
514fafea58 | ||
|
|
e324369fe0 | ||
|
|
3bc9287668 | ||
|
|
d90bf190c5 | ||
|
|
8cc6f66458 | ||
|
|
a2b071af4f | ||
|
|
b7b589802f | ||
|
|
93912a6df6 | ||
|
|
ffce15f653 | ||
|
|
725b14e583 | ||
|
|
26c6e47f1e | ||
|
|
51dae7375f | ||
|
|
801cfc4ff8 | ||
|
|
ac3ff7a63e | ||
|
|
1b22810f4b | ||
|
|
b756c9e4a1 | ||
|
|
64b5e6c032 | ||
|
|
a13f5dd2d1 | ||
|
|
e6ea8d3e16 | ||
|
|
af37850289 | ||
|
|
6ecdfa1cf8 | ||
|
|
c0b21ebc23 | ||
|
|
184ada417f | ||
|
|
b636860ecb | ||
|
|
0107fdacde | ||
|
|
ce5e1cad40 | ||
|
|
d877ba01a0 | ||
|
|
b0ed990d5a | ||
|
|
89c8a16900 | ||
|
|
247cf0ccc2 | ||
|
|
d0aa219a7a | ||
|
|
87291e2a89 | ||
|
|
9c88d21db6 | ||
|
|
8b7415042f | ||
|
|
59ab6e6c8a | ||
|
|
0724a376ea | ||
|
|
f9f26a5587 | ||
|
|
ed4122fb21 | ||
|
|
0739a7f689 | ||
|
|
c7b7a6e7c5 | ||
|
|
2a132c8325 | ||
|
|
154882a668 | ||
|
|
3f64c9dd67 | ||
|
|
d8d66e4244 | ||
|
|
a9cdefcd43 | ||
|
|
029d3ef596 | ||
|
|
0e474402c0 | ||
|
|
b6560cdedb | ||
|
|
767575703e | ||
|
|
4b4d9060ed | ||
|
|
ad75b1d25c | ||
|
|
4b767c5427 | ||
|
|
a6df7a83d6 | ||
|
|
93f2990399 | ||
|
|
e74f67089e | ||
|
|
41a6078790 | ||
|
|
4d93926fee | ||
|
|
03f5cafe76 | ||
|
|
4f6ebff880 | ||
|
|
af27cbbe2c | ||
|
|
3604957c83 | ||
|
|
3670a02aec | ||
|
|
7ebfdb3f33 | ||
|
|
b9b7da8746 | ||
|
|
eaaf137b9b | ||
|
|
a0311e3ce3 | ||
|
|
8864aa7b4b | ||
|
|
4d58129eee | ||
|
|
4468fe9fbb | ||
|
|
3b716a044b | ||
|
|
25e657729c | ||
|
|
cace399ed2 | ||
|
|
045e1ca6ba | ||
|
|
4f86dec560 | ||
|
|
13f033440d | ||
|
|
b5c455ffa4 | ||
|
|
eb5a00b706 | ||
|
|
3a560472e6 | ||
|
|
4776dce038 | ||
|
|
2d6891c6d2 | ||
|
|
f5a41f7b13 | ||
|
|
4a2926df94 | ||
|
|
8736f33a56 | ||
|
|
89eb77588f | ||
|
|
c930510226 | ||
|
|
b7c58e5d34 | ||
|
|
ce48ae020b | ||
|
|
7809bfc0d1 | ||
|
|
d84fcaafdf | ||
|
|
a9f600b797 | ||
|
|
f0a8e7ba9f | ||
|
|
c57a523553 | ||
|
|
d905f6f414 | ||
|
|
22f78ac405 | ||
|
|
7a098b1c7e | ||
|
|
e1383f2002 | ||
|
|
c3b23313ba | ||
|
|
02581e917d | ||
|
|
e267073f76 | ||
|
|
4038dae446 | ||
|
|
134b5fe0ff | ||
|
|
d452ca36b7 | ||
|
|
fdec35cd2e | ||
|
|
d488c8458c | ||
|
|
6d2e40c81d | ||
|
|
594b5d0448 | ||
|
|
1be12e5d4c | ||
|
|
bae2ee4245 | ||
|
|
57bd606f21 | ||
|
|
eb8cefa461 | ||
|
|
9edcd866bb | ||
|
|
07a8b00a93 | ||
|
|
c22be7ded8 | ||
|
|
2380b94db1 | ||
|
|
d8e59afee0 | ||
|
|
05e14baa68 | ||
|
|
ff56148732 | ||
|
|
bfc5668d24 | ||
|
|
b3103ae700 | ||
|
|
43834203a8 | ||
|
|
7ba8044564 | ||
|
|
7e91fe12e7 | ||
|
|
02114aac65 | ||
|
|
244dac76af | ||
|
|
2bd25c3f35 | ||
|
|
23350ea4b6 | ||
|
|
8a6917878e | ||
|
|
7dd00954e4 | ||
|
|
f3710f618e | ||
|
|
8ecdb6f5e8 | ||
|
|
309b4d44fc | ||
|
|
80f941d912 | ||
|
|
4534b09532 | ||
|
|
97a9b59acc | ||
|
|
87b8de9029 | ||
|
|
42f5146632 | ||
|
|
f613cc039f | ||
|
|
e974c77359 | ||
|
|
0f324177cb | ||
|
|
46a4508cd7 | ||
|
|
d4d9b1ad3c | ||
|
|
322ee05fdf | ||
|
|
85569366a2 | ||
|
|
dea6ff2a96 | ||
|
|
3fcd2edf6f | ||
|
|
16b84310ec | ||
|
|
f8899521bc | ||
|
|
3558a1a6b1 | ||
|
|
385943755d | ||
|
|
3002cb4e97 | ||
|
|
6d711520fc | ||
|
|
584de40983 | ||
|
|
81911ba549 | ||
|
|
e37e9e2251 | ||
|
|
92a65c8977 | ||
|
|
ae8b2cbd07 | ||
|
|
cda13edf85 | ||
|
|
610ee57963 | ||
|
|
2ef809db54 | ||
|
|
f315c0c051 | ||
|
|
936c230aa3 | ||
|
|
2c93f1f395 | ||
|
|
727ebd9c42 | ||
|
|
1e4fc897e3 | ||
|
|
3945a86004 | ||
|
|
58cc64d17b | ||
|
|
b66cf6f0ba | ||
|
|
1db15a741e | ||
|
|
5f355c833b | ||
|
|
a76b32e3ff | ||
|
|
f2c01dca25 | ||
|
|
abc542a0ca | ||
|
|
9e598ebd8c | ||
|
|
7801ca5819 | ||
|
|
482edabd27 | ||
|
|
3e5998de6e | ||
|
|
c3d19607f6 | ||
|
|
2c2648cbe7 | ||
|
|
a72c4f7797 | ||
|
|
19ee9eb18f | ||
|
|
3ae29c3883 | ||
|
|
d9f8f53a10 | ||
|
|
6b3e525f45 | ||
|
|
c8824f86af | ||
|
|
cf3163dccf | ||
|
|
da5a784214 | ||
|
|
30b3315084 | ||
|
|
5a7dcc7fcf | ||
|
|
c6305c57cf | ||
|
|
d330e2e978 | ||
|
|
1ec2a2a4a6 | ||
|
|
c97d384cf4 | ||
|
|
ca52e40a6a | ||
|
|
4a10efd7a4 | ||
|
|
128aab1b88 | ||
|
|
bb89be64f4 | ||
|
|
ef0a507306 | ||
|
|
908594970e | ||
|
|
36ff5e96a4 | ||
|
|
9bf9f8342a | ||
|
|
f3660c1f68 | ||
|
|
d58aa871b5 | ||
|
|
4f90eb65ad | ||
|
|
b50da98322 | ||
|
|
ca47f2817f | ||
|
|
c489a4bed9 | ||
|
|
54c7e996db | ||
|
|
0426bb289e | ||
|
|
8e253ffa05 | ||
|
|
102f365003 | ||
|
|
48d2f6ec07 | ||
|
|
58f0ce8e2d | ||
|
|
3178083533 | ||
|
|
516075db6d | ||
|
|
d6c8335162 | ||
|
|
e7a45efe15 | ||
|
|
1c0b5e6441 | ||
|
|
66792e1ab9 | ||
|
|
6fd631df5b | ||
|
|
dcf1a805c5 | ||
|
|
8edfde96dc | ||
|
|
ae911ec775 | ||
|
|
465d0e6f1c | ||
|
|
6d9de87fb8 | ||
|
|
a93027369e | ||
|
|
a1839aae46 | ||
|
|
d5fc7650ef | ||
|
|
cdc6c898ae | ||
|
|
574432ec0d | ||
|
|
a89486d6ad | ||
|
|
56d7234ccb | ||
|
|
c7f1b00e13 | ||
|
|
60f5137115 | ||
|
|
a90239e3c5 | ||
|
|
a105429d99 | ||
|
|
8e73b8f7a1 | ||
|
|
53af55a87d | ||
|
|
0711bcb259 | ||
|
|
282e00f93a | ||
|
|
2e11527416 | ||
|
|
01a64e63c6 | ||
|
|
2610d642fa | ||
|
|
e1e93aea66 | ||
|
|
ab208d0d2f | ||
|
|
e9210eb37d | ||
|
|
93665772c3 | ||
|
|
44bcc30130 | ||
|
|
d8bccbccaa | ||
|
|
2734caa9da | ||
|
|
d9ecdfc9d7 | ||
|
|
fa88bea376 | ||
|
|
25803b856d | ||
|
|
88539650ca | ||
|
|
3cf0162892 | ||
|
|
51e9e19409 | ||
|
|
c93d99b27c | ||
|
|
770b17c86b | ||
|
|
4e8ff9ea74 | ||
|
|
8ec8c57e31 | ||
|
|
48aa7232b1 | ||
|
|
1f3ffe96a1 | ||
|
|
e0505a31ca | ||
|
|
3ecc27b3f9 | ||
|
|
5d66c539d4 | ||
|
|
c751d53398 | ||
|
|
ea1e8abeac | ||
|
|
8d3f6a3c06 | ||
|
|
f35adf0ae4 | ||
|
|
848ac6ef7c | ||
|
|
6db7cd4a1f | ||
|
|
23d465a733 | ||
|
|
1148946a29 | ||
|
|
e77cbc5415 | ||
|
|
5ecb87ec63 | ||
|
|
8ef135dfd7 | ||
|
|
c26a2cc99e | ||
|
|
e0d8078bf1 | ||
|
|
4528060fd0 | ||
|
|
595467487b | ||
|
|
8cba08a900 | ||
|
|
89c009ab11 | ||
|
|
38f93fa212 | ||
|
|
eac2d64468 | ||
|
|
8a2cef15b2 | ||
|
|
c075f3f66a | ||
|
|
d138778f0a | ||
|
|
cf3aefc201 | ||
|
|
d974be5329 | ||
|
|
8c147283ba | ||
|
|
f72ba6582d | ||
|
|
b65badf097 | ||
|
|
cea71d8ca1 |
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Miner Information (If applicable):**
|
||||||
|
- Manufacturer: [e.g. Bitmain, MicroBT]
|
||||||
|
- Type: [e.g. S9, M20]
|
||||||
|
- Firmware Type: [e.g. Stock, BraiinsOS]
|
||||||
|
- Firmware Version:
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
22
.github/workflows/python-publish.yml
vendored
Normal file
22
.github/workflows/python-publish.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: PyPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- 'docs/**'
|
||||||
|
- 'docsrc/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Publish GH release
|
||||||
|
uses: softprops/action-gh-release@v0.1.14
|
||||||
|
- name: Build using poetry and publish to PyPi
|
||||||
|
uses: JRubics/poetry-publish@v1.11
|
||||||
|
with:
|
||||||
|
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
venv/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
__pycache__/
|
||||||
|
pyvenv.cfg
|
||||||
|
.env/
|
||||||
|
bin/
|
||||||
|
lib/
|
||||||
20
.readthedocs.yaml
Normal file
20
.readthedocs.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the version of Python and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-20.04
|
||||||
|
tools:
|
||||||
|
python: "3.9"
|
||||||
|
|
||||||
|
mkdocs:
|
||||||
|
configuration: mkdocs.yml
|
||||||
|
|
||||||
|
# Optionally declare the Python requirements required to build your docs
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
537
API/bmminer.py
537
API/bmminer.py
@@ -1,537 +0,0 @@
|
|||||||
from API import BaseMinerAPI
|
|
||||||
|
|
||||||
|
|
||||||
class BMMinerAPI(BaseMinerAPI):
|
|
||||||
"""
|
|
||||||
A class that abstracts the BMMiner API in the miners.
|
|
||||||
|
|
||||||
Each method corresponds to an API command in BMMiner.
|
|
||||||
|
|
||||||
BMMiner API documentation:
|
|
||||||
https://github.com/jameshilliard/bmminer/blob/master/API-README
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
ip: the IP address of the miner.
|
|
||||||
port (optional): the port of the API on the miner (standard is 4028)
|
|
||||||
"""
|
|
||||||
def __init__(self, ip: str, port: int = 4028) -> None:
|
|
||||||
super().__init__(ip, port)
|
|
||||||
|
|
||||||
async def version(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'version' command.
|
|
||||||
|
|
||||||
Returns a dict containing version information.
|
|
||||||
"""
|
|
||||||
return await self.send_command("version")
|
|
||||||
|
|
||||||
async def config(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'config' command.
|
|
||||||
|
|
||||||
Returns some miner configuration information:
|
|
||||||
ASC Count <- the number of ASCs
|
|
||||||
PGA Count <- the number of PGAs
|
|
||||||
Pool Count <- the number of Pools
|
|
||||||
Strategy <- the current pool strategy
|
|
||||||
Log Interval <- the interval of logging
|
|
||||||
Device Code <- list of compiled device drivers
|
|
||||||
OS <- the current operating system
|
|
||||||
Failover-Only <- failover-only setting
|
|
||||||
Scan Time <- scan-time setting
|
|
||||||
Queue <- queue setting
|
|
||||||
Expiry <- expiry setting
|
|
||||||
"""
|
|
||||||
return await self.send_command("config")
|
|
||||||
|
|
||||||
async def summary(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'summary' command.
|
|
||||||
|
|
||||||
Returns a dict containing the status summary of the miner.
|
|
||||||
"""
|
|
||||||
return await self.send_command("summary")
|
|
||||||
|
|
||||||
async def pools(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'pools' command.
|
|
||||||
|
|
||||||
Returns a dict containing the status of each pool.
|
|
||||||
"""
|
|
||||||
return await self.send_command("pools")
|
|
||||||
|
|
||||||
async def devs(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'devs' command.
|
|
||||||
|
|
||||||
Returns a dict containing each PGA/ASC with their details.
|
|
||||||
"""
|
|
||||||
return await self.send_command("devs")
|
|
||||||
|
|
||||||
async def edevs(self, old: bool = False) -> dict:
|
|
||||||
"""
|
|
||||||
API 'edevs' command.
|
|
||||||
|
|
||||||
Returns a dict containing each PGA/ASC with their details,
|
|
||||||
ignoring blacklisted devices and zombie devices.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
old (optional): include zombie devices that became zombies less than 'old' seconds ago
|
|
||||||
"""
|
|
||||||
if old:
|
|
||||||
return await self.send_command("edevs", parameters="old")
|
|
||||||
else:
|
|
||||||
return await self.send_command("edevs")
|
|
||||||
|
|
||||||
async def pga(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'pga' command.
|
|
||||||
|
|
||||||
Returns a dict containing the details of a single PGA of number N.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the PGA to get details of.
|
|
||||||
"""
|
|
||||||
return await self.send_command("pga", parameters=n)
|
|
||||||
|
|
||||||
async def pgacount(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'pgacount' command.
|
|
||||||
|
|
||||||
Returns a dict containing the number of PGA devices.
|
|
||||||
"""
|
|
||||||
return await self.send_command("pgacount")
|
|
||||||
|
|
||||||
async def switchpool(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'switchpool' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of switching pools.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the pool to switch to.
|
|
||||||
"""
|
|
||||||
return await self.send_command("switchpool", parameters=n)
|
|
||||||
|
|
||||||
async def enablepool(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'enablepool' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of enabling the pool.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the pool to enable.
|
|
||||||
"""
|
|
||||||
return await self.send_command("enablepool", parameters=n)
|
|
||||||
|
|
||||||
async def addpool(self, url: str, username: str, password: str) -> dict:
|
|
||||||
"""
|
|
||||||
API 'addpool' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of adding the pool.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
url: the URL of the new pool to add.
|
|
||||||
username: the users username on the new pool.
|
|
||||||
password: the worker password on the new pool.
|
|
||||||
"""
|
|
||||||
return await self.send_command("addpool", parameters=f"{url}, {username}, {password}")
|
|
||||||
|
|
||||||
async def poolpriority(self, *n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'poolpriority' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of setting pool priority.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: pool numbers in order of priority.
|
|
||||||
"""
|
|
||||||
return await self.send_command("poolpriority", parameters=f"{','.join([str(item) for item in n])}")
|
|
||||||
|
|
||||||
async def poolquota(self, n: int, q: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'poolquota' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of setting pool quota.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: pool number to set quota on.
|
|
||||||
q: quota to set the pool to.
|
|
||||||
"""
|
|
||||||
return await self.send_command("poolquota", parameters=f"{n}, {q}")
|
|
||||||
|
|
||||||
async def disablepool(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'disablepool' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of disabling the pool.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the pool to disable.
|
|
||||||
"""
|
|
||||||
return await self.send_command("disablepool", parameters=n)
|
|
||||||
|
|
||||||
async def removepool(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'removepool' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of removing the pool.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the pool to remove.
|
|
||||||
"""
|
|
||||||
return await self.send_command("removepool", parameters=n)
|
|
||||||
|
|
||||||
async def save(self, filename: str = None) -> dict:
|
|
||||||
"""
|
|
||||||
API 'save' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of saving the config file..
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
filename (optional): the filename to save the config as.
|
|
||||||
"""
|
|
||||||
if filename:
|
|
||||||
return await self.send_command("save", parameters=filename)
|
|
||||||
else:
|
|
||||||
return await self.send_command("save")
|
|
||||||
|
|
||||||
async def quit(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'quit' command.
|
|
||||||
|
|
||||||
Returns a single "BYE" before BMMiner quits.
|
|
||||||
"""
|
|
||||||
return await self.send_command("quit")
|
|
||||||
|
|
||||||
async def notify(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'notify' command.
|
|
||||||
|
|
||||||
Returns a dict containing the last status and count of each devices problem(s).
|
|
||||||
"""
|
|
||||||
return await self.send_command("notify")
|
|
||||||
|
|
||||||
async def privileged(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'privileged' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with an error if you have no privileged access.
|
|
||||||
"""
|
|
||||||
return await self.send_command("privileged")
|
|
||||||
|
|
||||||
async def pgaenable(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'pgaenable' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of enabling the PGA device N.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the PGA to enable.
|
|
||||||
"""
|
|
||||||
return await self.send_command("pgaenable", parameters=n)
|
|
||||||
|
|
||||||
async def pgadisable(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'pgadisable' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of disabling the PGA device N.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the PGA to disable.
|
|
||||||
"""
|
|
||||||
return await self.send_command("pgadisable", parameters=n)
|
|
||||||
|
|
||||||
async def pgaidentify(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'pgaidentify' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of identifying the PGA device N.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the PGA to identify.
|
|
||||||
"""
|
|
||||||
return await self.send_command("pgaidentify", parameters=n)
|
|
||||||
|
|
||||||
async def devdetails(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'devdetails' command.
|
|
||||||
|
|
||||||
Returns a dict containing all devices with their static details.
|
|
||||||
"""
|
|
||||||
return await self.send_command("devdetails")
|
|
||||||
|
|
||||||
async def restart(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'restart' command.
|
|
||||||
|
|
||||||
Returns a single "RESTART" before BMMiner restarts.
|
|
||||||
"""
|
|
||||||
return await self.send_command("restart")
|
|
||||||
|
|
||||||
async def stats(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'stats' command.
|
|
||||||
|
|
||||||
Returns a dict containing stats for all device/pool with more than 1 getwork.
|
|
||||||
"""
|
|
||||||
return await self.send_command("stats")
|
|
||||||
|
|
||||||
async def estats(self, old: bool = False) -> dict:
|
|
||||||
"""
|
|
||||||
API 'estats' command.
|
|
||||||
|
|
||||||
Returns a dict containing stats for all device/pool with more than 1 getwork,
|
|
||||||
ignoring zombie devices.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
old (optional): include zombie devices that became zombies less than 'old' seconds ago.
|
|
||||||
"""
|
|
||||||
if old:
|
|
||||||
return await self.send_command("estats", parameters="old")
|
|
||||||
else:
|
|
||||||
return await self.send_command("estats")
|
|
||||||
|
|
||||||
async def check(self, command: str) -> dict:
|
|
||||||
"""
|
|
||||||
API 'check' command.
|
|
||||||
|
|
||||||
Returns information about a command:
|
|
||||||
Exists (Y/N) <- the command exists in this version
|
|
||||||
Access (Y/N) <- you have access to use the command
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
command: the command to get information about.
|
|
||||||
"""
|
|
||||||
return await self.send_command("check", parameters=command)
|
|
||||||
|
|
||||||
async def failover_only(self, failover: bool) -> dict:
|
|
||||||
"""
|
|
||||||
API 'failover-only' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with what failover-only was set to.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
failover: what to set failover-only to.
|
|
||||||
"""
|
|
||||||
return await self.send_command("failover-only", parameters=failover)
|
|
||||||
|
|
||||||
async def coin(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'coin' command.
|
|
||||||
|
|
||||||
Returns information about the current coin being mined:
|
|
||||||
Hash Method <- the hashing algorithm
|
|
||||||
Current Block Time <- blocktime as a float, 0 means none
|
|
||||||
Current Block Hash <- the hash of the current block, blank means none
|
|
||||||
LP <- whether LP is in use on at least 1 pool
|
|
||||||
Network Difficulty: the current network difficulty
|
|
||||||
"""
|
|
||||||
return await self.send_command("coin")
|
|
||||||
|
|
||||||
async def debug(self, setting: str) -> dict:
|
|
||||||
"""
|
|
||||||
API 'debug' command.
|
|
||||||
|
|
||||||
Returns which debug setting was enabled or disabled.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
setting: which setting to switch to. Options are:
|
|
||||||
Silent,
|
|
||||||
Quiet,
|
|
||||||
Verbose,
|
|
||||||
Debug,
|
|
||||||
RPCProto,
|
|
||||||
PerDevice,
|
|
||||||
WorkTime,
|
|
||||||
Normal.
|
|
||||||
"""
|
|
||||||
return await self.send_command("debug", parameters=setting)
|
|
||||||
|
|
||||||
async def setconfig(self, name: str, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'setconfig' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of setting 'name' to N.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
name: name of the config setting to set. Options are:
|
|
||||||
queue,
|
|
||||||
scantime,
|
|
||||||
expiry.
|
|
||||||
n: the value to set the 'name' setting to.
|
|
||||||
"""
|
|
||||||
return await self.send_command("setconfig", parameters=f"{name}, {n}")
|
|
||||||
|
|
||||||
async def usbstats(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'usbstats' command.
|
|
||||||
|
|
||||||
Returns a dict containing the stats of all USB devices except ztex.
|
|
||||||
"""
|
|
||||||
return await self.send_command("usbstats")
|
|
||||||
|
|
||||||
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
|
|
||||||
"""
|
|
||||||
API 'pgaset' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of setting PGA N with opt[,val].
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the PGA to set the options on.
|
|
||||||
opt: the option to set. Setting this to 'help' returns a help message.
|
|
||||||
val: the value to set the option to.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
MMQ -
|
|
||||||
opt: clock
|
|
||||||
val: 160 - 230 (multiple of 2)
|
|
||||||
CMR -
|
|
||||||
opt: clock
|
|
||||||
val: 100 - 220
|
|
||||||
"""
|
|
||||||
if val:
|
|
||||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}, {val}")
|
|
||||||
else:
|
|
||||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}")
|
|
||||||
|
|
||||||
async def zero(self, which: str, summary: bool) -> dict:
|
|
||||||
"""
|
|
||||||
API 'zero' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with info on the zero and optional summary.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
which: which device to zero.
|
|
||||||
Setting this to 'all' zeros all devices.
|
|
||||||
Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
|
|
||||||
summary: whether or not to show a full summary.
|
|
||||||
"""
|
|
||||||
return await self.send_command("zero", parameters=f"{which}, {summary}")
|
|
||||||
|
|
||||||
async def hotplug(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'hotplug' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with whether or not hotplug was enabled.
|
|
||||||
"""
|
|
||||||
return await self.send_command("hotplug", parameters=n)
|
|
||||||
|
|
||||||
async def asc(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'asc' command.
|
|
||||||
|
|
||||||
Returns a dict containing the details of a single ASC of number N.
|
|
||||||
|
|
||||||
n: the ASC device to get details of.
|
|
||||||
"""
|
|
||||||
return await self.send_command("asc", parameters=n)
|
|
||||||
|
|
||||||
async def ascenable(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'ascenable' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of enabling the ASC device N.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the ASC to enable.
|
|
||||||
"""
|
|
||||||
return await self.send_command("ascenable", parameters=n)
|
|
||||||
|
|
||||||
async def ascdisable(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'ascdisable' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of disabling the ASC device N.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the ASC to disable.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return await self.send_command("ascdisable", parameters=n)
|
|
||||||
|
|
||||||
async def ascidentify(self, n: int) -> dict:
|
|
||||||
"""
|
|
||||||
API 'ascidentify' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of identifying the ASC device N.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the number of the PGA to identify.
|
|
||||||
"""
|
|
||||||
return await self.send_command("ascidentify", parameters=n)
|
|
||||||
|
|
||||||
async def asccount(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'asccount' command.
|
|
||||||
|
|
||||||
Returns a dict containing the number of ASC devices.
|
|
||||||
"""
|
|
||||||
return await self.send_command("asccount")
|
|
||||||
|
|
||||||
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
|
|
||||||
"""
|
|
||||||
API 'ascset' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the results of setting ASC N with opt[,val].
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
n: the ASC to set the options on.
|
|
||||||
opt: the option to set. Setting this to 'help' returns a help message.
|
|
||||||
val: the value to set the option to.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
AVA+BTB -
|
|
||||||
opt: freq
|
|
||||||
val: 256 - 1024 (chip frequency)
|
|
||||||
BTB -
|
|
||||||
opt: millivolts
|
|
||||||
val: 1000 - 1400 (core voltage)
|
|
||||||
MBA -
|
|
||||||
opt: reset
|
|
||||||
val: 0 - # of chips (reset a chip)
|
|
||||||
|
|
||||||
opt: freq
|
|
||||||
val: 0 - # of chips, 100 - 1400 (chip frequency)
|
|
||||||
|
|
||||||
opt: ledcount
|
|
||||||
val: 0 - 100 (chip count for LED)
|
|
||||||
|
|
||||||
opt: ledlimit
|
|
||||||
val: 0 - 200 (LED off below GH/s)
|
|
||||||
|
|
||||||
opt: spidelay
|
|
||||||
val: 0 - 9999 (SPI per I/O delay)
|
|
||||||
|
|
||||||
opt: spireset
|
|
||||||
val: i or s, 0 - 9999 (SPI regular reset)
|
|
||||||
|
|
||||||
opt: spisleep
|
|
||||||
val: 0 - 9999 (SPI reset sleep in ms)
|
|
||||||
BMA -
|
|
||||||
opt: volt
|
|
||||||
val: 0 - 9
|
|
||||||
|
|
||||||
opt: clock
|
|
||||||
val: 0 - 15
|
|
||||||
"""
|
|
||||||
if val:
|
|
||||||
return await self.send_command("ascset", parameters=f"{n}, {opt}, {val}")
|
|
||||||
else:
|
|
||||||
return await self.send_command("ascset", parameters=f"{n}, {opt}")
|
|
||||||
|
|
||||||
async def lcd(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'lcd' command.
|
|
||||||
|
|
||||||
Returns a dict containing an all in one status summary of the miner.
|
|
||||||
"""
|
|
||||||
return await self.send_command("lcd")
|
|
||||||
|
|
||||||
async def lockstats(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'lockstats' command.
|
|
||||||
|
|
||||||
Returns the STATUS section with the result of writing the lock stats to STDERR.
|
|
||||||
"""
|
|
||||||
return await self.send_command("lockstats")
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
from API import BaseMinerAPI
|
|
||||||
|
|
||||||
|
|
||||||
class BOSMinerAPI(BaseMinerAPI):
|
|
||||||
def __init__(self, ip, port=4028):
|
|
||||||
super().__init__(ip, port)
|
|
||||||
|
|
||||||
async def asccount(self) -> dict:
|
|
||||||
return await self.send_command("asccount")
|
|
||||||
|
|
||||||
async def asc(self, n: int) -> dict:
|
|
||||||
return await self.send_command("asc", parameters=n)
|
|
||||||
|
|
||||||
async def devdetails(self) -> dict:
|
|
||||||
return await self.send_command("devdetails")
|
|
||||||
|
|
||||||
async def devs(self) -> dict:
|
|
||||||
return await self.send_command("devs")
|
|
||||||
|
|
||||||
async def edevs(self, old: bool = False) -> dict:
|
|
||||||
if old:
|
|
||||||
return await self.send_command("edevs", parameters="old")
|
|
||||||
else:
|
|
||||||
return await self.send_command("edevs")
|
|
||||||
|
|
||||||
async def pools(self) -> dict:
|
|
||||||
return await self.send_command("pools")
|
|
||||||
|
|
||||||
async def summary(self) -> dict:
|
|
||||||
return await self.send_command("summary")
|
|
||||||
|
|
||||||
async def stats(self) -> dict:
|
|
||||||
return await self.send_command("stats")
|
|
||||||
|
|
||||||
async def version(self) -> dict:
|
|
||||||
return await self.send_command("version")
|
|
||||||
|
|
||||||
async def estats(self) -> dict:
|
|
||||||
return await self.send_command("estats")
|
|
||||||
|
|
||||||
async def check(self) -> dict:
|
|
||||||
return await self.send_command("check")
|
|
||||||
|
|
||||||
async def coin(self) -> dict:
|
|
||||||
return await self.send_command("coin")
|
|
||||||
|
|
||||||
async def lcd(self) -> dict:
|
|
||||||
return await self.send_command("lcd")
|
|
||||||
|
|
||||||
async def switchpool(self, n: int) -> dict:
|
|
||||||
# BOS has not implemented this yet, they will in the future
|
|
||||||
raise NotImplementedError
|
|
||||||
# return await self.send_command("switchpool", parameters=n)
|
|
||||||
|
|
||||||
async def enablepool(self, n: int) -> dict:
|
|
||||||
# BOS has not implemented this yet, they will in the future
|
|
||||||
raise NotImplementedError
|
|
||||||
# return await self.send_command("enablepool", parameters=n)
|
|
||||||
|
|
||||||
async def disablepool(self, n: int) -> dict:
|
|
||||||
# BOS has not implemented this yet, they will in the future
|
|
||||||
raise NotImplementedError
|
|
||||||
# return await self.send_command("disablepool", parameters=n)
|
|
||||||
|
|
||||||
async def addpool(self, url: str, username: str, password: str) -> dict:
|
|
||||||
# BOS has not implemented this yet, they will in the future
|
|
||||||
raise NotImplementedError
|
|
||||||
# return await self.send_command("addpool", parameters=f"{url}, {username}, {password}")
|
|
||||||
|
|
||||||
async def removepool(self, n: int) -> dict:
|
|
||||||
# BOS has not implemented this yet, they will in the future
|
|
||||||
raise NotImplementedError
|
|
||||||
# return await self.send_command("removepool", parameters=n)
|
|
||||||
|
|
||||||
async def fans(self) -> dict:
|
|
||||||
return await self.send_command("fans")
|
|
||||||
|
|
||||||
async def tempctrl(self) -> dict:
|
|
||||||
return await self.send_command("tempctrl")
|
|
||||||
|
|
||||||
async def temps(self) -> dict:
|
|
||||||
return await self.send_command("temps")
|
|
||||||
|
|
||||||
async def tunerstatus(self) -> dict:
|
|
||||||
return await self.send_command("tunerstatus")
|
|
||||||
|
|
||||||
async def pause(self) -> dict:
|
|
||||||
return await self.send_command("pause")
|
|
||||||
|
|
||||||
async def resume(self) -> dict:
|
|
||||||
return await self.send_command("resume")
|
|
||||||
326
API/btminer.py
326
API/btminer.py
@@ -1,326 +0,0 @@
|
|||||||
from API import BaseMinerAPI, APIError
|
|
||||||
|
|
||||||
from passlib.hash import md5_crypt
|
|
||||||
import asyncio
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import hashlib
|
|
||||||
import binascii
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
import base64
|
|
||||||
|
|
||||||
|
|
||||||
### IMPORTANT ###
|
|
||||||
# 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 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.
|
|
||||||
|
|
||||||
|
|
||||||
def _crypt(word: str, salt: str) -> str:
|
|
||||||
# compile a standard format for the salt
|
|
||||||
standard_salt = re.compile('\s*\$(\d+)\$([\w\./]*)\$')
|
|
||||||
# check if the salt matches
|
|
||||||
match = standard_salt.match(salt)
|
|
||||||
# if the matching fails, the salt is incorrect
|
|
||||||
if not match:
|
|
||||||
raise ValueError("salt format is not correct")
|
|
||||||
# save the matched salt in a new variable
|
|
||||||
new_salt = match.group(2)
|
|
||||||
# encrypt the word with the salt using md5
|
|
||||||
result = md5_crypt.hash(word, salt=new_salt)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _add_to_16(s: str) -> bytes:
|
|
||||||
"""Add null bytes to a string until the length is 16"""
|
|
||||||
while len(s) % 16 != 0:
|
|
||||||
s += '\0'
|
|
||||||
return str.encode(s) # return bytes
|
|
||||||
|
|
||||||
|
|
||||||
def parse_btminer_priviledge_data(token_data, data):
|
|
||||||
enc_data = data['enc']
|
|
||||||
aeskey = hashlib.sha256(token_data['host_passwd_md5'].encode()).hexdigest()
|
|
||||||
aeskey = binascii.unhexlify(aeskey.encode())
|
|
||||||
aes = AES.new(aeskey, AES.MODE_ECB)
|
|
||||||
ret_msg = json.loads(str(
|
|
||||||
aes.decrypt(base64.decodebytes(bytes(
|
|
||||||
enc_data, encoding='utf8'))).rstrip(b'\0').decode("utf8")))
|
|
||||||
return ret_msg
|
|
||||||
|
|
||||||
|
|
||||||
def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
|
|
||||||
# add token to command
|
|
||||||
command['token'] = token_data['host_sign']
|
|
||||||
# encode host_passwd data and get hexdigest
|
|
||||||
aeskey = hashlib.sha256(token_data['host_passwd_md5'].encode()).hexdigest()
|
|
||||||
# unhexlify the encoded host_passwd
|
|
||||||
aeskey = binascii.unhexlify(aeskey.encode())
|
|
||||||
# create a new AES key
|
|
||||||
aes = AES.new(aeskey, AES.MODE_ECB)
|
|
||||||
# dump the command to json
|
|
||||||
api_json_str = json.dumps(command)
|
|
||||||
# encode the json command with the aes key
|
|
||||||
api_json_str_enc = str(base64.encodebytes(
|
|
||||||
aes.encrypt(_add_to_16(api_json_str))),
|
|
||||||
encoding='utf8').replace('\n', '')
|
|
||||||
# label the data as being encoded
|
|
||||||
data_enc = {'enc': 1, 'data': api_json_str_enc}
|
|
||||||
# dump the labeled data to json
|
|
||||||
api_packet_str = json.dumps(data_enc)
|
|
||||||
return api_packet_str.encode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerAPI(BaseMinerAPI):
|
|
||||||
def __init__(self, ip, port=4028, pwd: str = "admin"):
|
|
||||||
super().__init__(ip, port)
|
|
||||||
self.admin_pwd = pwd
|
|
||||||
self.current_token = None
|
|
||||||
|
|
||||||
async def send_command(self, command: str | bytes, **kwargs) -> dict:
|
|
||||||
"""Send an API command to the miner and return the result."""
|
|
||||||
if isinstance(command, str):
|
|
||||||
command = json.dumps({"command": command}).encode("utf-8")
|
|
||||||
try:
|
|
||||||
# get reader and writer streams
|
|
||||||
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
|
||||||
# handle OSError 121
|
|
||||||
except OSError as e:
|
|
||||||
if e.winerror == "121":
|
|
||||||
print("Semaphore Timeout has Expired.")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# send the command
|
|
||||||
writer.write(command)
|
|
||||||
await writer.drain()
|
|
||||||
|
|
||||||
# instantiate data
|
|
||||||
data = b""
|
|
||||||
|
|
||||||
# loop to receive all the data
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
d = await reader.read(4096)
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
data += d
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
data = self.load_api_data(data)
|
|
||||||
|
|
||||||
# close the connection
|
|
||||||
writer.close()
|
|
||||||
await writer.wait_closed()
|
|
||||||
|
|
||||||
if 'enc' in data.keys():
|
|
||||||
try:
|
|
||||||
data = parse_btminer_priviledge_data(self.current_token, data)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
if not self.validate_command_output(data):
|
|
||||||
raise APIError(data["Msg"])
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
async def get_token(self):
|
|
||||||
data = await self.send_command("get_token")
|
|
||||||
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + '$')
|
|
||||||
pwd = pwd.split('$')
|
|
||||||
host_passwd_md5 = pwd[3]
|
|
||||||
tmp = _crypt(pwd[3] + data["Msg"]["time"], "$1$" + data["Msg"]["newsalt"] + '$')
|
|
||||||
tmp = tmp.split('$')
|
|
||||||
host_sign = tmp[3]
|
|
||||||
self.current_token = {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
|
|
||||||
return {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
|
|
||||||
|
|
||||||
#### privileged COMMANDS ####
|
|
||||||
# Please read the top of this file to learn
|
|
||||||
# how to configure the whatsminer API to
|
|
||||||
# use these commands.
|
|
||||||
|
|
||||||
async def update_pools(self,
|
|
||||||
pool_1: str, worker_1: str, passwd_1: str,
|
|
||||||
pool_2: str = None, worker_2: str = None, passwd_2: str = None,
|
|
||||||
pool_3: str = None, worker_3: str = None, passwd_3: str = None):
|
|
||||||
token_data = await self.get_token()
|
|
||||||
if pool_2 and pool_3:
|
|
||||||
command = {
|
|
||||||
"cmd": "update_pools",
|
|
||||||
"pool1": pool_1, "worker1": worker_1, "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,
|
|
||||||
}
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def restart_btminer(self):
|
|
||||||
command = {"cmd": "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):
|
|
||||||
if respbefore:
|
|
||||||
command = {"cmd": "power_off", "respbefore": "true"}
|
|
||||||
else:
|
|
||||||
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):
|
|
||||||
command = {"cmd": "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):
|
|
||||||
command = {"cmd": "set_led", "param": "auto"}
|
|
||||||
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(self, color: str = "red", period: int = 2000, duration: int = 1000, start: int = 0):
|
|
||||||
command = {"cmd": "set_led", "color": color, "period": period, "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):
|
|
||||||
command = {"cmd": "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):
|
|
||||||
# to be determined if this will be added later
|
|
||||||
# requires a file stream in bytes
|
|
||||||
return NotImplementedError
|
|
||||||
|
|
||||||
async def reboot(self):
|
|
||||||
command = {"cmd": "reboot"}
|
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def factory_reset(self):
|
|
||||||
command = {"cmd": "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):
|
|
||||||
# check if password length is greater than 8 bytes
|
|
||||||
if len(new_pwd.encode('utf-8')) > 8:
|
|
||||||
return APIError(
|
|
||||||
f"New password too long, the max length is 8. Password size: {len(new_pwd.encode('utf-8'))}")
|
|
||||||
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
|
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
async def set_target_freq(self, percent: int):
|
|
||||||
if not -10 < percent < 100:
|
|
||||||
return APIError(f"Frequency % is outside of the allowed range. Please set a % between -10 and 100")
|
|
||||||
command = {"cmd": "set_target_freq", "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 enable_fast_boot(self):
|
|
||||||
command = {"cmd": "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):
|
|
||||||
command = {"cmd": "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):
|
|
||||||
command = {"cmd": "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):
|
|
||||||
command = {"cmd": "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):
|
|
||||||
command = {"cmd": "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):
|
|
||||||
if not 0 < percent < 100:
|
|
||||||
return APIError(f"Power PCT % is outside of the allowed range. Please set a % between 0 and 100")
|
|
||||||
command = {"cmd": "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):
|
|
||||||
if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue":
|
|
||||||
return APIError(
|
|
||||||
'Message is incorrect, please choose one of '
|
|
||||||
'["wait for adjust temp", "adjust complete", "adjust continue"]'
|
|
||||||
)
|
|
||||||
if complete:
|
|
||||||
complete = "true"
|
|
||||||
else:
|
|
||||||
complete = "false"
|
|
||||||
command = {"cmd": "pre_power_on", "complete": complete, "msg": msg}
|
|
||||||
token_data = await self.get_token()
|
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
|
||||||
return await self.send_command(enc_command)
|
|
||||||
|
|
||||||
#### END privileged COMMANDS ####
|
|
||||||
|
|
||||||
async def summary(self):
|
|
||||||
return await self.send_command("summary")
|
|
||||||
|
|
||||||
async def pools(self):
|
|
||||||
return await self.send_command("pools")
|
|
||||||
|
|
||||||
async def devs(self):
|
|
||||||
return await self.send_command("devs")
|
|
||||||
|
|
||||||
async def edevs(self):
|
|
||||||
return await self.send_command("edevs")
|
|
||||||
|
|
||||||
async def devdetails(self):
|
|
||||||
return await self.send_command("devdetails")
|
|
||||||
|
|
||||||
async def get_psu(self):
|
|
||||||
return await self.send_command("get_psu")
|
|
||||||
|
|
||||||
async def version(self):
|
|
||||||
return await self.send_command("get_version")
|
|
||||||
|
|
||||||
async def status(self):
|
|
||||||
return await self.send_command("status")
|
|
||||||
|
|
||||||
async def get_miner_info(self):
|
|
||||||
return await self.send_command("get_miner_info")
|
|
||||||
167
API/cgminer.py
167
API/cgminer.py
@@ -1,167 +0,0 @@
|
|||||||
from API import BaseMinerAPI
|
|
||||||
|
|
||||||
|
|
||||||
class CGMinerAPI(BaseMinerAPI):
|
|
||||||
def __init__(self, ip, port=4028):
|
|
||||||
super().__init__(ip, port)
|
|
||||||
|
|
||||||
async def version(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'version' command.
|
|
||||||
|
|
||||||
Returns a dict containing version information.
|
|
||||||
"""
|
|
||||||
return await self.send_command("version")
|
|
||||||
|
|
||||||
async def config(self) -> dict:
|
|
||||||
"""
|
|
||||||
API 'config' command.
|
|
||||||
|
|
||||||
Returns a dict containing some miner configuration information:
|
|
||||||
ASC Count <- the number of ASCs
|
|
||||||
PGA Count <- the number of PGAs
|
|
||||||
Pool Count <- the number of Pools
|
|
||||||
Strategy <- the current pool strategy
|
|
||||||
Log Interval <- the interval of logging
|
|
||||||
Device Code <- list of compiled device drivers
|
|
||||||
OS <- the current operating system
|
|
||||||
"""
|
|
||||||
return await self.send_command("config")
|
|
||||||
|
|
||||||
async def summary(self) -> dict:
|
|
||||||
return await self.send_command("summary")
|
|
||||||
|
|
||||||
async def pools(self) -> dict:
|
|
||||||
return await self.send_command("pools")
|
|
||||||
|
|
||||||
async def devs(self) -> dict:
|
|
||||||
return await self.send_command("devs")
|
|
||||||
|
|
||||||
async def edevs(self, old: bool = False) -> dict:
|
|
||||||
if old:
|
|
||||||
return await self.send_command("edevs", parameters="old")
|
|
||||||
else:
|
|
||||||
return await self.send_command("edevs")
|
|
||||||
|
|
||||||
async def pga(self, n: int) -> dict:
|
|
||||||
return await self.send_command("pga", parameters=n)
|
|
||||||
|
|
||||||
async def pgacount(self) -> dict:
|
|
||||||
return await self.send_command("pgacount")
|
|
||||||
|
|
||||||
async def switchpool(self, n: int) -> dict:
|
|
||||||
return await self.send_command("switchpool", parameters=n)
|
|
||||||
|
|
||||||
async def enablepool(self, n: int) -> dict:
|
|
||||||
return await self.send_command("enablepool", parameters=n)
|
|
||||||
|
|
||||||
async def addpool(self, url: str, username: str, password: str) -> dict:
|
|
||||||
return await self.send_command("addpool", parameters=f"{url}, {username}, {password}")
|
|
||||||
|
|
||||||
async def poolpriority(self, *n: int) -> dict:
|
|
||||||
return await self.send_command("poolpriority", parameters=f"{','.join([str(item) for item in n])}")
|
|
||||||
|
|
||||||
async def poolquota(self, n: int, q: int) -> dict:
|
|
||||||
return await self.send_command("poolquota", parameters=f"{n}, {q}")
|
|
||||||
|
|
||||||
async def disablepool(self, n: int) -> dict:
|
|
||||||
return await self.send_command("disablepool", parameters=n)
|
|
||||||
|
|
||||||
async def removepool(self, n: int) -> dict:
|
|
||||||
return await self.send_command("removepool", parameters=n)
|
|
||||||
|
|
||||||
async def save(self, filename: str = None) -> dict:
|
|
||||||
if filename:
|
|
||||||
return await self.send_command("save", parameters=filename)
|
|
||||||
else:
|
|
||||||
return await self.send_command("save")
|
|
||||||
|
|
||||||
async def quit(self) -> dict:
|
|
||||||
return await self.send_command("quit")
|
|
||||||
|
|
||||||
async def notify(self) -> dict:
|
|
||||||
return await self.send_command("notify")
|
|
||||||
|
|
||||||
async def privileged(self) -> dict:
|
|
||||||
return await self.send_command("privileged")
|
|
||||||
|
|
||||||
async def pgaenable(self, n: int) -> dict:
|
|
||||||
return await self.send_command("pgaenable", parameters=n)
|
|
||||||
|
|
||||||
async def pgadisable(self, n: int) -> dict:
|
|
||||||
return await self.send_command("pgadisable", parameters=n)
|
|
||||||
|
|
||||||
async def pgaidentify(self, n: int) -> dict:
|
|
||||||
return await self.send_command("pgaidentify", parameters=n)
|
|
||||||
|
|
||||||
async def devdetails(self) -> dict:
|
|
||||||
return await self.send_command("devdetails")
|
|
||||||
|
|
||||||
async def restart(self) -> dict:
|
|
||||||
return await self.send_command("restart")
|
|
||||||
|
|
||||||
async def stats(self) -> dict:
|
|
||||||
return await self.send_command("stats")
|
|
||||||
|
|
||||||
async def estats(self, old: bool = False) -> dict:
|
|
||||||
if old:
|
|
||||||
return await self.send_command("estats", parameters="old")
|
|
||||||
else:
|
|
||||||
return await self.send_command("estats")
|
|
||||||
|
|
||||||
async def check(self, command) -> dict:
|
|
||||||
return await self.send_command("check", parameters=command)
|
|
||||||
|
|
||||||
async def failover_only(self, failover: bool) -> dict:
|
|
||||||
return await self.send_command("failover-only", parameters=failover)
|
|
||||||
|
|
||||||
async def coin(self) -> dict:
|
|
||||||
return await self.send_command("coin")
|
|
||||||
|
|
||||||
async def debug(self, setting: str) -> dict:
|
|
||||||
return await self.send_command("debug", parameters=setting)
|
|
||||||
|
|
||||||
async def setconfig(self, name: str, n: int) -> dict:
|
|
||||||
return await self.send_command("setconfig", parameters=f"{name}, {n}")
|
|
||||||
|
|
||||||
async def usbstats(self) -> dict:
|
|
||||||
return await self.send_command("usbstats")
|
|
||||||
|
|
||||||
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
|
|
||||||
if val:
|
|
||||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}, {val}")
|
|
||||||
else:
|
|
||||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}")
|
|
||||||
|
|
||||||
async def zero(self, which: str, value: bool) -> dict:
|
|
||||||
return await self.send_command("zero", parameters=f"{which}, {value}")
|
|
||||||
|
|
||||||
async def hotplug(self, n: int) -> dict:
|
|
||||||
return await self.send_command("hotplug", parameters=n)
|
|
||||||
|
|
||||||
async def asc(self, n: int) -> dict:
|
|
||||||
return await self.send_command("asc", parameters=n)
|
|
||||||
|
|
||||||
async def ascenable(self, n: int) -> dict:
|
|
||||||
return await self.send_command("ascenable", parameters=n)
|
|
||||||
|
|
||||||
async def ascdisable(self, n: int) -> dict:
|
|
||||||
return await self.send_command("ascdisable", parameters=n)
|
|
||||||
|
|
||||||
async def ascidentify(self, n: int) -> dict:
|
|
||||||
return await self.send_command("ascidentify", parameters=n)
|
|
||||||
|
|
||||||
async def asccount(self) -> dict:
|
|
||||||
return await self.send_command("asccount")
|
|
||||||
|
|
||||||
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
|
|
||||||
if val:
|
|
||||||
return await self.send_command("ascset", parameters=f"{n}, {opt}, {val}")
|
|
||||||
else:
|
|
||||||
return await self.send_command("ascset", parameters=f"{n}, {opt}")
|
|
||||||
|
|
||||||
async def lcd(self) -> dict:
|
|
||||||
return await self.send_command("lcd")
|
|
||||||
|
|
||||||
async def lockstats(self) -> dict:
|
|
||||||
return await self.send_command("lockstats")
|
|
||||||
246
README.md
246
README.md
@@ -1,131 +1,169 @@
|
|||||||
# minerInterface
|
# pyasic
|
||||||
*A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH.*
|
*A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH.*
|
||||||
|
|
||||||
|
[](https://github.com/psf/black)
|
||||||
|
[](https://pypi.org/project/pyasic/)
|
||||||
|
[](https://pypi.org/project/pyasic/)
|
||||||
|
[](https://pyasic.readthedocs.io/en/latest/)
|
||||||
|
## Documentation
|
||||||
|
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Standard Usage
|
||||||
|
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)
|
||||||
|
|
||||||
|
### Developers
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### CFG Util
|
You can also use poetry by initializing and running ```poetry install```
|
||||||
*CFG Util is a GUI for interfacing with the miners easily, it is mostly self-explanatory.*
|
|
||||||
|
|
||||||
To use CFG Util you have 2 options -
|
|
||||||
1. Run it directly with the file ```config_tool.py``` or import it with ```from cfg_util import main```, then run the ```main()``` function in an asyncio event loop like -
|
|
||||||
|
|
||||||
```python
|
|
||||||
from cfg_util import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
```
|
|
||||||
2. Make a build of the CFG Util for your system using cx_freeze and ```make_cfg_tool_exe.py```
|
|
||||||
(Alternatively, you can get a build made by me here -> https://drive.google.com/drive/folders/1nzojuGRu0IszIGpwx7SvG5RlJ2_KXIOv)
|
|
||||||
1. Open either Command Prompt on Windows or Terminal on Mac or UNIX.
|
|
||||||
2. Navigate to this directory, and run ```make_cfg_tool_exe.py build``` on Windows or ```python3 make_cfg_tool_exe.py``` on Mac or UNIX.
|
|
||||||
|
|
||||||
### Interfacing with miners programmatically
|
### 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>
|
||||||
|
|
||||||
To write your own custom programs with this repo, you have many options.
|
To write your own custom programs with this repo, you have many options.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
A basic script to find all miners on the network and get the hashrate from them looks like this -
|
There are 2 main ways to get a miner and it's functions via scanning or via the MinerFactory.
|
||||||
|
|
||||||
|
#### Scanning for miners
|
||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
from network import MinerNetwork
|
import sys
|
||||||
from cfg_util.func import safe_parse_api_data
|
|
||||||
|
|
||||||
async def get_hashrate():
|
from pyasic.network import MinerNetwork
|
||||||
# Miner Network class allows for easy scanning of a network
|
|
||||||
# Give it any IP on a network and it will find the whole subnet
|
# Fix whatsminer bug
|
||||||
# It can also be passed a subnet mask:
|
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
||||||
# miner_network = MinerNetwork('192.168.1.55', mask=23)
|
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
||||||
miner_network = MinerNetwork('192.168.1.1')
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
# Miner Network scan function returns Miner classes for all miners found
|
|
||||||
miners = await miner_network.scan_network_for_miners()
|
|
||||||
# Each miner will return with its own set of functions, and an API class instance
|
# define asynchronous function to scan for miners
|
||||||
tasks = [miner.api.summary() for miner in miners]
|
async def scan_and_get_data():
|
||||||
|
# Define network range to be used for scanning
|
||||||
|
# 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
|
||||||
|
net = MinerNetwork("192.168.1.69", mask=24)
|
||||||
|
# Scan the network for miners
|
||||||
|
# This function returns a list of miners of the correct type as a class
|
||||||
|
miners: list = await net.scan_network_for_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
|
||||||
|
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)
|
||||||
parse_tasks = []
|
|
||||||
|
# Data is now a list of MinerData, and we can reference any part of that
|
||||||
|
# Print out all data for now
|
||||||
for item in data:
|
for item in data:
|
||||||
# safe_parse_api_data parses the data from a miner API
|
print(item)
|
||||||
# It will raise an APIError (from API import APIError) if there is a problem
|
|
||||||
parse_tasks.append(safe_parse_api_data(item, 'SUMMARY', 0, 'MHS 5s'))
|
if __name__ == "__main__":
|
||||||
# Gather all tasks asynchronously and run them
|
asyncio.run(scan_and_get_data())
|
||||||
data = await asyncio.gather(*parse_tasks)
|
```
|
||||||
# Print a list of all the hashrates
|
|
||||||
|
</br>
|
||||||
|
|
||||||
|
#### Getting a miner if you know the IP
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pyasic.miners.miner_factory import MinerFactory
|
||||||
|
|
||||||
|
# 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
|
||||||
|
async def get_miner_data(miner_ip: str):
|
||||||
|
# Use MinerFactory to get miner
|
||||||
|
# MinerFactory is a singleton, so we can just get the instance in place
|
||||||
|
miner = await MinerFactory().get_miner(miner_ip)
|
||||||
|
|
||||||
|
# Get data from the miner
|
||||||
|
data = await miner.get_data()
|
||||||
print(data)
|
print(data)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
asyncio.new_event_loop().run_until_complete(get_hashrate())
|
asyncio.run(get_miner_data("192.168.1.69"))
|
||||||
```
|
```
|
||||||
<br>
|
|
||||||
You can also create your own miner without scanning if you know the IP:
|
### Advanced data gathering
|
||||||
|
|
||||||
|
If needed, this library exposes a wrapper for the miner API that can be used for advanced data gathering.
|
||||||
|
|
||||||
|
#### List available API commands
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pyasic.miners.miner_factory import MinerFactory
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
# Get the miner
|
||||||
|
miner = await MinerFactory().get_miner(miner_ip)
|
||||||
|
|
||||||
|
# List all available commands
|
||||||
|
print(miner.api.get_commands())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(get_api_commands("192.168.1.69"))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use miner API commands to gather data
|
||||||
|
|
||||||
|
The miner API commands will raise an `APIError` if they fail with a bad status code, to bypass this you must send them manually by using `miner.api.send_command(command, ignore_errors=True)`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
import ipaddress
|
import sys
|
||||||
from miners.miner_factory import MinerFactory
|
|
||||||
from cfg_util.func import safe_parse_api_data
|
|
||||||
|
|
||||||
async def get_miner_hashrate(ip: str):
|
from pyasic.miners.miner_factory import MinerFactory
|
||||||
# Instantiate a Miner Factory to generate miners from their IP
|
|
||||||
miner_factory = MinerFactory()
|
|
||||||
# Make the string IP into an IP address
|
|
||||||
miner_ip = ipaddress.ip_address(ip)
|
|
||||||
# Wait for the factory to return the miner
|
|
||||||
miner = await miner_factory.get_miner(miner_ip)
|
|
||||||
# Get the API data
|
|
||||||
summary = await miner.api.summary()
|
|
||||||
# safe_parse_api_data parses the data from a miner API
|
|
||||||
# It will raise an APIError (from API import APIError) if there is a problem
|
|
||||||
data = await safe_parse_api_data(summary, 'SUMMARY', 0, 'MHS 5s')
|
|
||||||
print(data)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
# Fix whatsminer bug
|
||||||
asyncio.new_event_loop().run_until_complete(get_miner_hashrate(str("192.168.1.69")))
|
# 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())
|
||||||
<br>
|
|
||||||
Or generate a miner directly without the factory:
|
|
||||||
|
async def get_api_commands(miner_ip: str):
|
||||||
```python
|
# Get the miner
|
||||||
import asyncio
|
miner = await MinerFactory().get_miner(miner_ip)
|
||||||
from miners.bosminer import BOSminer
|
|
||||||
from cfg_util.func import safe_parse_api_data
|
# Run the devdetails command
|
||||||
|
# This is equivalent to await miner.api.send_command("devdetails")
|
||||||
async def get_miner_hashrate(ip: str):
|
devdetails: dict = await miner.api.devdetails()
|
||||||
# Create a BOSminer miner object
|
print(devdetails)
|
||||||
miner = BOSminer(ip)
|
|
||||||
# Get the API data
|
|
||||||
summary = await miner.api.summary()
|
if __name__ == "__main__":
|
||||||
# safe_parse_api_data parses the data from a miner API
|
asyncio.run(get_api_commands("192.168.1.69"))
|
||||||
# It will raise an APIError (from API import APIError) if there is a problem
|
|
||||||
data = await safe_parse_api_data(summary, 'SUMMARY', 0, 'MHS 5s')
|
|
||||||
print(data)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.new_event_loop().run_until_complete(get_miner_hashrate(str("192.168.1.69")))
|
|
||||||
```
|
|
||||||
|
|
||||||
<br>
|
|
||||||
Or finally, just get the API directly:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import asyncio
|
|
||||||
from API.bosminer import BOSMinerAPI
|
|
||||||
from cfg_util.func import safe_parse_api_data
|
|
||||||
|
|
||||||
async def get_miner_hashrate(ip: str):
|
|
||||||
# Create a BOSminerAPI object
|
|
||||||
# Port can be declared manually, if not it defaults to 4028
|
|
||||||
api = BOSMinerAPI(ip, port=4028)
|
|
||||||
# Get the API data
|
|
||||||
summary = await api.summary()
|
|
||||||
# safe_parse_api_data parses the data from a miner API
|
|
||||||
# It will raise an APIError (from API import APIError) if there is a problem
|
|
||||||
data = await safe_parse_api_data(summary, 'SUMMARY', 0, 'MHS 5s')
|
|
||||||
print(data)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.new_event_loop().run_until_complete(get_miner_hashrate(str("192.168.1.69")))
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
from cfg_util.miner_factory import miner_factory
|
|
||||||
from cfg_util.layout import window
|
|
||||||
from cfg_util.ui import ui
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Fix bug with some whatsminers and asyncio because of a socket not being shut down:
|
|
||||||
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
loop.run_until_complete(ui())
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
import ipaddress
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from operator import itemgetter
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import aiofiles
|
|
||||||
import toml
|
|
||||||
|
|
||||||
from cfg_util.miner_factory import miner_factory
|
|
||||||
from cfg_util.layout import window
|
|
||||||
from cfg_util.func.data import safe_parse_api_data
|
|
||||||
|
|
||||||
from config.bos import bos_config_convert, general_config_convert_bos
|
|
||||||
|
|
||||||
from API import APIError
|
|
||||||
|
|
||||||
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
|
|
||||||
|
|
||||||
|
|
||||||
async def update_ui_with_data(key, message, append=False):
|
|
||||||
if append:
|
|
||||||
message = window[key].get_text() + message
|
|
||||||
window[key].update(message)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_prog_bar(amount):
|
|
||||||
window["progress"].Update(amount)
|
|
||||||
percent_done = 100 * (amount / window['progress'].maxlen)
|
|
||||||
window["progress_percent"].Update(f"{round(percent_done, 2)} %")
|
|
||||||
if percent_done == 100:
|
|
||||||
window["progress_percent"].Update("")
|
|
||||||
|
|
||||||
|
|
||||||
async def set_progress_bar_len(amount):
|
|
||||||
window["progress"].Update(0, max=amount)
|
|
||||||
window["progress"].maxlen = amount
|
|
||||||
|
|
||||||
|
|
||||||
async def scan_network(network):
|
|
||||||
await update_ui_with_data("status", "Scanning")
|
|
||||||
network_size = len(network)
|
|
||||||
miner_generator = network.scan_network_generator()
|
|
||||||
await set_progress_bar_len(2 * network_size)
|
|
||||||
progress_bar_len = 0
|
|
||||||
miners = []
|
|
||||||
async for miner in miner_generator:
|
|
||||||
if miner:
|
|
||||||
miners.append(miner)
|
|
||||||
progress_bar_len += 1
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
progress_bar_len += network_size - len(miners)
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
get_miner_genenerator = miner_factory.get_miner_generator(miners)
|
|
||||||
all_miners = []
|
|
||||||
async for found_miner in get_miner_genenerator:
|
|
||||||
all_miners.append(found_miner)
|
|
||||||
progress_bar_len += 1
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
all_miners.sort(key=lambda x: x.ip)
|
|
||||||
window["ip_list"].update([str(miner.ip) for miner in all_miners])
|
|
||||||
await update_ui_with_data("ip_count", str(len(all_miners)))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def miner_light(ips: list):
|
|
||||||
await asyncio.gather(*[flip_light(ip) for ip in ips])
|
|
||||||
|
|
||||||
|
|
||||||
async def flip_light(ip):
|
|
||||||
listbox = window['ip_list'].Widget
|
|
||||||
miner = await miner_factory.get_miner(ip)
|
|
||||||
if ip in window["ip_list"].Values:
|
|
||||||
index = window["ip_list"].Values.index(ip)
|
|
||||||
if listbox.itemcget(index, "background") == 'red':
|
|
||||||
listbox.itemconfigure(index, bg='#f0f3f7', fg='#000000')
|
|
||||||
await miner.fault_light_off()
|
|
||||||
else:
|
|
||||||
listbox.itemconfigure(index, bg='red', fg='white')
|
|
||||||
await miner.fault_light_on()
|
|
||||||
|
|
||||||
|
|
||||||
async def import_config(ip):
|
|
||||||
await update_ui_with_data("status", "Importing")
|
|
||||||
miner = await miner_factory.get_miner(ipaddress.ip_address(*ip))
|
|
||||||
await miner.get_config()
|
|
||||||
config = miner.config
|
|
||||||
await update_ui_with_data("config", str(config))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def import_iplist(file_location):
|
|
||||||
await update_ui_with_data("status", "Importing")
|
|
||||||
if not os.path.exists(file_location):
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
ip_list = []
|
|
||||||
async with aiofiles.open(file_location, mode='r') as file:
|
|
||||||
async for line in file:
|
|
||||||
ips = [x.group() for x in re.finditer(
|
|
||||||
"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", line)]
|
|
||||||
for ip in ips:
|
|
||||||
if ip not in ip_list:
|
|
||||||
ip_list.append(ipaddress.ip_address(ip))
|
|
||||||
ip_list.sort()
|
|
||||||
window["ip_list"].update([str(ip) for ip in ip_list])
|
|
||||||
await update_ui_with_data("ip_count", str(len(ip_list)))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def export_iplist(file_location, ip_list_selected):
|
|
||||||
await update_ui_with_data("status", "Exporting")
|
|
||||||
if not os.path.exists(file_location):
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
if ip_list_selected is not None and not ip_list_selected == []:
|
|
||||||
async with aiofiles.open(file_location, mode='w') as file:
|
|
||||||
for item in ip_list_selected:
|
|
||||||
await file.write(str(item) + "\n")
|
|
||||||
else:
|
|
||||||
async with aiofiles.open(file_location, mode='w') as file:
|
|
||||||
for item in window['ip_list'].Values:
|
|
||||||
await file.write(str(item) + "\n")
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def send_config_generator(miners: list, config):
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
config_tasks = []
|
|
||||||
for miner in miners:
|
|
||||||
if len(config_tasks) >= CONFIG_THREADS:
|
|
||||||
configured = asyncio.as_completed(config_tasks)
|
|
||||||
config_tasks = []
|
|
||||||
for sent_config in configured:
|
|
||||||
yield await sent_config
|
|
||||||
config_tasks.append(loop.create_task(miner.send_config(config)))
|
|
||||||
configured = asyncio.as_completed(config_tasks)
|
|
||||||
for sent_config in configured:
|
|
||||||
yield await sent_config
|
|
||||||
|
|
||||||
|
|
||||||
async def send_config(ips: list, config):
|
|
||||||
await update_ui_with_data("status", "Configuring")
|
|
||||||
await set_progress_bar_len(2 * len(ips))
|
|
||||||
progress_bar_len = 0
|
|
||||||
get_miner_genenerator = miner_factory.get_miner_generator(ips)
|
|
||||||
all_miners = []
|
|
||||||
async for miner in get_miner_genenerator:
|
|
||||||
all_miners.append(miner)
|
|
||||||
progress_bar_len += 1
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
|
|
||||||
config_sender_generator = send_config_generator(all_miners, config)
|
|
||||||
async for _config_sender in config_sender_generator:
|
|
||||||
progress_bar_len += 1
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def import_config_file(file_location):
|
|
||||||
await update_ui_with_data("status", "Importing")
|
|
||||||
if not os.path.exists(file_location):
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
async with aiofiles.open(file_location, mode='r') as file:
|
|
||||||
config = await file.read()
|
|
||||||
await update_ui_with_data("config", await bos_config_convert(toml.loads(config)))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def export_config_file(file_location, config):
|
|
||||||
await update_ui_with_data("status", "Exporting")
|
|
||||||
config = toml.loads(config)
|
|
||||||
config['format']['generator'] = 'upstream_config_util'
|
|
||||||
config['format']['timestamp'] = int(time.time())
|
|
||||||
config = toml.dumps(config)
|
|
||||||
async with aiofiles.open(file_location, mode='w+') as file:
|
|
||||||
await file.write(await general_config_convert_bos(config))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def get_data(ip_list: list):
|
|
||||||
await update_ui_with_data("status", "Getting Data")
|
|
||||||
ips = [ipaddress.ip_address(ip) for ip in ip_list]
|
|
||||||
await set_progress_bar_len(len(ips))
|
|
||||||
progress_bar_len = 0
|
|
||||||
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in ips])
|
|
||||||
miner_data = []
|
|
||||||
for all_data in data_gen:
|
|
||||||
miner_data.append(await all_data)
|
|
||||||
progress_bar_len += 1
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
|
|
||||||
miner_data.sort(key=lambda x: ipaddress.ip_address(x['IP']))
|
|
||||||
|
|
||||||
total_hr = round(sum(d.get('TH/s', 0) for d in miner_data), 2)
|
|
||||||
window["hr_total"].update(f"{total_hr} TH/s")
|
|
||||||
window["hr_list"].update(disabled=False)
|
|
||||||
window["hr_list"].update([item['IP'] + " | "
|
|
||||||
+ item['host'] + " | "
|
|
||||||
+ str(item['TH/s']) + " TH/s | "
|
|
||||||
+ item['user'] + " | "
|
|
||||||
+ str(item['wattage']) + " W"
|
|
||||||
for item in miner_data])
|
|
||||||
window["hr_list"].update(disabled=True)
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def get_formatted_data(ip: ipaddress.ip_address):
|
|
||||||
miner = await miner_factory.get_miner(ip)
|
|
||||||
try:
|
|
||||||
miner_data = await miner.api.multicommand("summary", "pools", "tunerstatus")
|
|
||||||
except APIError:
|
|
||||||
return {'TH/s': "Unknown", 'IP': str(miner.ip), 'host': "Unknown", 'user': "Unknown", 'wattage': 0}
|
|
||||||
host = await miner.get_hostname()
|
|
||||||
if "tunerstatus" in miner_data.keys():
|
|
||||||
wattage = await safe_parse_api_data(miner_data, "tunerstatus", 0, 'TUNERSTATUS', 0, "PowerLimit")
|
|
||||||
# data['tunerstatus'][0]['TUNERSTATUS'][0]['PowerLimit']
|
|
||||||
else:
|
|
||||||
wattage = 0
|
|
||||||
if "summary" in miner_data.keys():
|
|
||||||
if 'MHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
|
||||||
th5s = round(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'MHS 5s') / 1000000, 2)
|
|
||||||
elif 'GHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
|
||||||
if not miner_data['summary'][0]['SUMMARY'][0]['GHS 5s'] == "":
|
|
||||||
th5s = round(float(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'GHS 5s')) / 1000, 2)
|
|
||||||
else:
|
|
||||||
th5s = 0
|
|
||||||
else:
|
|
||||||
th5s = 0
|
|
||||||
else:
|
|
||||||
th5s = 0
|
|
||||||
if "pools" not in miner_data.keys():
|
|
||||||
user = "?"
|
|
||||||
elif not miner_data['pools'][0]['POOLS'] == []:
|
|
||||||
user = await safe_parse_api_data(miner_data, 'pools', 0, 'POOLS', 0, 'User')
|
|
||||||
else:
|
|
||||||
user = "Blank"
|
|
||||||
return {'TH/s': th5s, 'IP': str(miner.ip), 'host': host, 'user': user, 'wattage': wattage}
|
|
||||||
|
|
||||||
|
|
||||||
async def generate_config():
|
|
||||||
config = {'group': [{
|
|
||||||
'name': 'group',
|
|
||||||
'quota': 1,
|
|
||||||
'pool': [{
|
|
||||||
'url': 'stratum2+tcp://us-east.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt',
|
|
||||||
'user': 'UpstreamDataInc.test',
|
|
||||||
'password': '123'
|
|
||||||
}, {
|
|
||||||
'url': 'stratum2+tcp://stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt',
|
|
||||||
'user': 'UpstreamDataInc.test',
|
|
||||||
'password': '123'
|
|
||||||
}, {
|
|
||||||
'url': 'stratum+tcp://stratum.slushpool.com:3333',
|
|
||||||
'user': 'UpstreamDataInc.test',
|
|
||||||
'password': '123'
|
|
||||||
}]
|
|
||||||
}],
|
|
||||||
'format': {
|
|
||||||
'version': '1.2+',
|
|
||||||
'model': 'Antminer S9',
|
|
||||||
'generator': 'upstream_config_util',
|
|
||||||
'timestamp': int(time.time())
|
|
||||||
},
|
|
||||||
'temp_control': {
|
|
||||||
'target_temp': 80.0,
|
|
||||||
'hot_temp': 90.0,
|
|
||||||
'dangerous_temp': 120.0
|
|
||||||
},
|
|
||||||
'autotuning': {
|
|
||||||
'enabled': True,
|
|
||||||
'psu_power_limit': 900
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window['config'].update(await bos_config_convert(config))
|
|
||||||
|
|
||||||
|
|
||||||
async def sort_data(index: int or str):
|
|
||||||
await update_ui_with_data("status", "Sorting Data")
|
|
||||||
data_list = window['hr_list'].Values
|
|
||||||
new_list = []
|
|
||||||
indexes = {}
|
|
||||||
for item in data_list:
|
|
||||||
item_data = [part.strip() for part in item.split("|")]
|
|
||||||
for idx, part in enumerate(item_data):
|
|
||||||
if re.match("[0-9]* W", part):
|
|
||||||
item_data[idx] = item_data[idx].replace(" W", "")
|
|
||||||
indexes['wattage'] = idx
|
|
||||||
elif re.match("[0-9]*\.?[0-9]* TH\/s", part):
|
|
||||||
item_data[idx] = item_data[idx].replace(" TH/s", "")
|
|
||||||
indexes['hr'] = idx
|
|
||||||
elif re.match("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", part):
|
|
||||||
item_data[idx] = ipaddress.ip_address(item_data[idx])
|
|
||||||
indexes['ip'] = idx
|
|
||||||
new_list.append(item_data)
|
|
||||||
if not isinstance(index, str):
|
|
||||||
if index == indexes['hr']:
|
|
||||||
new_data_list = sorted(new_list, key=lambda x: float(x[index]))
|
|
||||||
else:
|
|
||||||
new_data_list = sorted(new_list, key=itemgetter(index))
|
|
||||||
else:
|
|
||||||
if index.lower() not in indexes.keys():
|
|
||||||
return
|
|
||||||
elif index.lower() == 'hr':
|
|
||||||
new_data_list = sorted(new_list, key=lambda x: float(x[indexes[index]]))
|
|
||||||
else:
|
|
||||||
new_data_list = sorted(new_list, key=itemgetter(indexes[index]))
|
|
||||||
new_ip_list = []
|
|
||||||
for item in new_data_list:
|
|
||||||
new_ip_list.append(item[indexes['ip']])
|
|
||||||
new_data_list = [str(item[indexes['ip']]) + " | "
|
|
||||||
+ item[1] + " | "
|
|
||||||
+ item[indexes['hr']] + " TH/s | "
|
|
||||||
+ item[3] + " | "
|
|
||||||
+ str(item[indexes['wattage']]) + " W"
|
|
||||||
for item in new_data_list]
|
|
||||||
window["hr_list"].update(disabled=False)
|
|
||||||
window["hr_list"].update(new_data_list)
|
|
||||||
window['ip_list'].update(new_ip_list)
|
|
||||||
window["hr_list"].update(disabled=True)
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
from API import APIError
|
|
||||||
|
|
||||||
|
|
||||||
async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int = 0):
|
|
||||||
path = [*path]
|
|
||||||
if len(path) == idx+1:
|
|
||||||
if isinstance(path[idx], str):
|
|
||||||
if isinstance(data, dict):
|
|
||||||
if path[idx] in data.keys():
|
|
||||||
return data[path[idx]]
|
|
||||||
elif isinstance(path[idx], int):
|
|
||||||
if isinstance(data, list):
|
|
||||||
if len(data) > path[idx]:
|
|
||||||
return data[path[idx]]
|
|
||||||
else:
|
|
||||||
if isinstance(path[idx], str):
|
|
||||||
if isinstance(data, dict):
|
|
||||||
if path[idx] in data.keys():
|
|
||||||
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
|
|
||||||
# has to be == None, or else it fails on 0.0 hashrates
|
|
||||||
if parsed_data == None:
|
|
||||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
|
||||||
return parsed_data
|
|
||||||
else:
|
|
||||||
if idx == 0:
|
|
||||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
if idx == 0:
|
|
||||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
|
||||||
return False
|
|
||||||
elif isinstance(path[idx], int):
|
|
||||||
if isinstance(data, list):
|
|
||||||
if len(data) > path[idx]:
|
|
||||||
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
|
|
||||||
# has to be == None, or else it fails on 0.0 hashrates
|
|
||||||
if parsed_data == None:
|
|
||||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
|
||||||
return parsed_data
|
|
||||||
else:
|
|
||||||
if idx == 0:
|
|
||||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
if idx == 0:
|
|
||||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
|
||||||
return False
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
|||||||
from miners.miner_factory import MinerFactory
|
|
||||||
|
|
||||||
miner_factory = MinerFactory()
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from cfg_util.layout import window
|
|
||||||
from cfg_util.func import scan_network, sort_data, send_config, miner_light, get_data, export_config_file, \
|
|
||||||
generate_config, import_config, import_iplist, import_config_file, export_iplist
|
|
||||||
|
|
||||||
from network import MinerNetwork
|
|
||||||
|
|
||||||
|
|
||||||
async def ui():
|
|
||||||
while True:
|
|
||||||
event, value = window.read(timeout=10)
|
|
||||||
if event in (None, 'Close'):
|
|
||||||
sys.exit()
|
|
||||||
if event == 'scan':
|
|
||||||
if len(value['miner_network'].split("/")) > 1:
|
|
||||||
network = value['miner_network'].split("/")
|
|
||||||
miner_network = MinerNetwork(ip_addr=network[0], mask=network[1])
|
|
||||||
else:
|
|
||||||
miner_network = MinerNetwork(value['miner_network'])
|
|
||||||
asyncio.create_task(scan_network(miner_network))
|
|
||||||
if event == 'select_all_ips':
|
|
||||||
if value['ip_list'] == window['ip_list'].Values:
|
|
||||||
window['ip_list'].set_value([])
|
|
||||||
else:
|
|
||||||
window['ip_list'].set_value(window['ip_list'].Values)
|
|
||||||
if event == 'import_config':
|
|
||||||
if 2 > len(value['ip_list']) > 0:
|
|
||||||
asyncio.create_task(import_config(value['ip_list']))
|
|
||||||
if event == 'light':
|
|
||||||
asyncio.create_task(miner_light(value['ip_list']))
|
|
||||||
if event == "import_iplist":
|
|
||||||
asyncio.create_task(import_iplist(value["file_iplist"]))
|
|
||||||
if event == "export_iplist":
|
|
||||||
asyncio.create_task(export_iplist(value["file_iplist"], value['ip_list']))
|
|
||||||
if event == "send_config":
|
|
||||||
asyncio.create_task(send_config(value['ip_list'], value['config']))
|
|
||||||
if event == "import_file_config":
|
|
||||||
asyncio.create_task(import_config_file(value['file_config']))
|
|
||||||
if event == "export_file_config":
|
|
||||||
asyncio.create_task(export_config_file(value['file_config'], value["config"]))
|
|
||||||
if event == "get_data":
|
|
||||||
asyncio.create_task(get_data(value['ip_list']))
|
|
||||||
if event == "generate_config":
|
|
||||||
asyncio.create_task(generate_config())
|
|
||||||
if event == "sort_data_ip":
|
|
||||||
asyncio.create_task(sort_data('ip'))
|
|
||||||
if event == "sort_data_hr":
|
|
||||||
asyncio.create_task(sort_data('hr'))
|
|
||||||
if event == "sort_data_user":
|
|
||||||
asyncio.create_task(sort_data(3))
|
|
||||||
if event == "sort_data_w":
|
|
||||||
asyncio.create_task(sort_data('wattage'))
|
|
||||||
if event == "__TIMEOUT__":
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
"""
|
|
||||||
SAMPLE CONFIG
|
|
||||||
-------------------
|
|
||||||
{
|
|
||||||
"format": {
|
|
||||||
"version": "1.2+", # -> (default = "1.2+", str, (bos: format.version))
|
|
||||||
"model": "Antminer S9", # -> (default = "Antminer S9", str, (bos: format.model))
|
|
||||||
"generator": "upstream_config_util", # -> (hidden, always = "upstream_config_util", str, (bos: format.generator))
|
|
||||||
"timestamp": 1606842000, # -> (hidden, always = int(time.time()) (current unix time), int, (bos: format.timestamp))
|
|
||||||
},
|
|
||||||
"temperature": {
|
|
||||||
"mode": "auto", # -> (default = "auto", str["auto", "manual", "disabled"], (bos: temp_control.mode))
|
|
||||||
"target": 70.0, # -> (default = 70.0, float, (bos: temp_control.target_temp))
|
|
||||||
"hot": 80.0, # -> (default = 80.0, float, (bos: temp_control.hot_temp))
|
|
||||||
"danger": 90.0, # -> (default = 90.0, float, (bos: temp_control.dangerous_temp))
|
|
||||||
},
|
|
||||||
"fans": { # -> (optional, required if temperature["mode"] == "disabled", (bos: fan_control))
|
|
||||||
"min_fans": 1, # -> (default = 1, int, (bos: fan_control.min_fans))
|
|
||||||
"speed": 100, # -> (default = 100, 0 < int < 100, (bos: fan_control.speed))
|
|
||||||
},
|
|
||||||
"asicboost": True, # -> (default = True, bool, (bos : hash_chain_global.asic_boost))
|
|
||||||
"pool_groups": [
|
|
||||||
{
|
|
||||||
"group_name": "Upstream", # -> (default = "group_{index}" (group_0), str, (bos: group.[index].name))
|
|
||||||
"quota": 1, # -> (default = 1, int, (bos: group.[index].quota))
|
|
||||||
"pools": [
|
|
||||||
{
|
|
||||||
"url": "stratum+tcp://stratum.slushpool.com:3333", # -> (str, (bos: group.[index].pool.[index].url))
|
|
||||||
"username": "UpstreamDataInc.test", # -> (str, (bos: group.[index].pool.[index].user))
|
|
||||||
"password": "123", # -> (str, (bos: group.[index].pool.[index].password))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "stratum+tcp://us-east.stratum.slushpool.com:3333", # -> (str, (bos: group.[index].pool.[index].url))
|
|
||||||
"username": "UpstreamDataInc.test", # -> (str, (bos: group.[index].pool.[index].user))
|
|
||||||
"password": "123", # -> (str, (bos: group.[index].pool.[index].password))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "stratum+tcp://ca.stratum.slushpool.com:3333", # -> (str, (bos: group.[index].pool.[index].url))
|
|
||||||
"username": "UpstreamDataInc.test", # -> (str, (bos: group.[index].pool.[index].user))
|
|
||||||
"password": "123", # -> (str, (bos: group.[index].pool.[index].password))
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group_name": "Upstream2", # -> (default = "group_{index}" (group_1), str, (bos: group.[index].name))
|
|
||||||
"quota": 4, # -> (default = 1, int, (bos: group.[index].quota))
|
|
||||||
"pools": [
|
|
||||||
{
|
|
||||||
"url": "stratum+tcp://stratum.slushpool.com:3333", # -> (str, (bos: group.[index].pool.[index].url))
|
|
||||||
"username": "UpstreamDataTesting.test", # -> (str, (bos: group.[index].pool.[index].user))
|
|
||||||
"password": "123", # -> (str, (bos: group.[index].pool.[index].password))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "stratum+tcp://us-east.stratum.slushpool.com:3333", # -> (str, (bos: group.[index].pool.[index].url))
|
|
||||||
"username": "UpstreamDataTesting.test", # -> (str, (bos: group.[index].pool.[index].user))
|
|
||||||
"password": "123", # -> (str, (bos: group.[index].pool.[index].password))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "stratum+tcp://ca.stratum.slushpool.com:3333", # -> (str, (bos: group.[index].pool.[index].url))
|
|
||||||
"username": "UpstreamDataTesting.test", # -> (str, (bos: group.[index].pool.[index].user))
|
|
||||||
"password": "123", # -> (str, (bos: group.[index].pool.[index].password))
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"autotuning": {
|
|
||||||
"enabled": True, # -> (default = True, bool), (bos: autotuning.enabled)
|
|
||||||
"wattage": 900, # -> (default = 900, int, (bos: autotuning.psu_power_limit))
|
|
||||||
},
|
|
||||||
"power_scaling": {
|
|
||||||
"enabled": False, # -> (default = False, bool, (bos: power_scaling.enabled))
|
|
||||||
"power_step": 100, # -> (default = 100, int, (bos: power_scaling.power_step))
|
|
||||||
"min_psu_power_limit": 800, # -> (default = 800, int, (bos: power_scaling.min_psu_power_limit))
|
|
||||||
"shutdown_enabled": True, # -> (default = False, bool, (bos: power_scaling.shutdown_enabled))
|
|
||||||
"shutdown_duration": 3.0, # -> (default = 3.0, float, (bos: power_scaling.shutdown_duration))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
188
config/bos.py
188
config/bos.py
@@ -1,188 +0,0 @@
|
|||||||
import time
|
|
||||||
import yaml
|
|
||||||
import toml
|
|
||||||
|
|
||||||
|
|
||||||
async def bos_config_convert(config: dict):
|
|
||||||
out_config = {}
|
|
||||||
for opt in config:
|
|
||||||
if opt == "format":
|
|
||||||
out_config["format"] = config[opt]
|
|
||||||
out_config["format"]["generator"] = 'upstream_config_util'
|
|
||||||
out_config["format"]["timestamp"] = int(time.time())
|
|
||||||
elif opt == "temp_control":
|
|
||||||
out_config["temperature"] = {}
|
|
||||||
if "mode" in config[opt].keys():
|
|
||||||
out_config["temperature"]["mode"] = config[opt]["mode"]
|
|
||||||
else:
|
|
||||||
out_config["temperature"]["mode"] = "auto"
|
|
||||||
|
|
||||||
if "target_temp" in config[opt].keys():
|
|
||||||
out_config["temperature"]["target"] = config[opt]["target_temp"]
|
|
||||||
else:
|
|
||||||
out_config["temperature"]["target"] = 70.0
|
|
||||||
|
|
||||||
if "hot_temp" in config[opt].keys():
|
|
||||||
out_config["temperature"]["hot"] = config[opt]["hot_temp"]
|
|
||||||
else:
|
|
||||||
out_config["temperature"]["hot"] = 80.0
|
|
||||||
|
|
||||||
if "dangerous_temp" in config[opt].keys():
|
|
||||||
out_config["temperature"]["danger"] = config[opt]["dangerous_temp"]
|
|
||||||
else:
|
|
||||||
out_config["temperature"]["danger"] = 90.0
|
|
||||||
elif opt == "fan_control":
|
|
||||||
out_config["fans"] = {}
|
|
||||||
if "min_fans" in config[opt].keys():
|
|
||||||
out_config["fans"]["min_fans"] = config[opt]["min_fans"]
|
|
||||||
else:
|
|
||||||
out_config["fans"]["min_fans"] = 1
|
|
||||||
if "speed" in config[opt].keys():
|
|
||||||
out_config["fans"]["speed"] = config[opt]["speed"]
|
|
||||||
else:
|
|
||||||
out_config["fans"]["speed"] = 100
|
|
||||||
elif opt == "group":
|
|
||||||
out_config["pool_groups"] = [{} for _item in range(len(config[opt]))]
|
|
||||||
for idx in range(len(config[opt])):
|
|
||||||
out_config["pool_groups"][idx]["pools"] = []
|
|
||||||
out_config["pool_groups"][idx] = {}
|
|
||||||
if "name" in config[opt][idx].keys():
|
|
||||||
out_config["pool_groups"][idx]["group_name"] = config[opt][idx]["name"]
|
|
||||||
else:
|
|
||||||
out_config["pool_groups"][idx]["group_name"] = f"group_{idx}"
|
|
||||||
if "quota" in config[opt][idx].keys():
|
|
||||||
out_config["pool_groups"][idx]["quota"] = config[opt][idx]["quota"]
|
|
||||||
else:
|
|
||||||
out_config["pool_groups"][idx]["quota"] = 1
|
|
||||||
out_config["pool_groups"][idx]["pools"] = [{} for _item in range(len(config[opt][idx]["pool"]))]
|
|
||||||
for pool_idx in range(len(config[opt][idx]["pool"])):
|
|
||||||
out_config["pool_groups"][idx]["pools"][pool_idx]["url"] = config[opt][idx]["pool"][pool_idx]["url"]
|
|
||||||
out_config["pool_groups"][idx]["pools"][pool_idx]["username"] = config[opt][idx]["pool"][pool_idx][
|
|
||||||
"user"]
|
|
||||||
out_config["pool_groups"][idx]["pools"][pool_idx]["password"] = config[opt][idx]["pool"][pool_idx][
|
|
||||||
"password"]
|
|
||||||
elif opt == "autotuning":
|
|
||||||
out_config["autotuning"] = {}
|
|
||||||
if "enabled" in config[opt].keys():
|
|
||||||
out_config["autotuning"]["enabled"] = config[opt]["enabled"]
|
|
||||||
else:
|
|
||||||
out_config["autotuning"]["enabled"] = True
|
|
||||||
if "psu_power_limit" in config[opt].keys():
|
|
||||||
out_config["autotuning"]["wattage"] = config[opt]["psu_power_limit"]
|
|
||||||
else:
|
|
||||||
out_config["autotuning"]["wattage"] = 900
|
|
||||||
elif opt == "power_scaling":
|
|
||||||
out_config["power_scaling"] = {}
|
|
||||||
if "enabled" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["enabled"] = config[opt]["enabled"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["enabled"] = False
|
|
||||||
if "power_step" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["power_step"] = config[opt]["power_step"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["power_step"] = 100
|
|
||||||
if "min_psu_power_limit" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["min_psu_power_limit"] = config[opt]["min_psu_power_limit"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["min_psu_power_limit"] = 800
|
|
||||||
if "shutdown_enabled" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["shutdown_enabled"] = config[opt]["shutdown_enabled"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["shutdown_enabled"] = False
|
|
||||||
if "shutdown_duration" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["shutdown_duration"] = config[opt]["shutdown_duration"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["shutdown_duration"] = 3.0
|
|
||||||
return yaml.dump(out_config, sort_keys=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def general_config_convert_bos(yaml_config):
|
|
||||||
config = yaml.load(yaml_config, Loader=yaml.SafeLoader)
|
|
||||||
out_config = {}
|
|
||||||
for opt in config:
|
|
||||||
if opt == "format":
|
|
||||||
out_config["format"] = config[opt]
|
|
||||||
out_config["format"]["generator"] = 'upstream_config_util'
|
|
||||||
out_config["format"]["timestamp"] = int(time.time())
|
|
||||||
elif opt == "temperature":
|
|
||||||
out_config["temp_control"] = {}
|
|
||||||
if "mode" in config[opt].keys():
|
|
||||||
out_config["temp_control"]["mode"] = config[opt]["mode"]
|
|
||||||
else:
|
|
||||||
out_config["temp_control"]["mode"] = "auto"
|
|
||||||
|
|
||||||
if "target" in config[opt].keys():
|
|
||||||
out_config["temp_control"]["target_temp"] = config[opt]["target"]
|
|
||||||
else:
|
|
||||||
out_config["temp_control"]["target_temp"] = 70.0
|
|
||||||
|
|
||||||
if "hot" in config[opt].keys():
|
|
||||||
out_config["temp_control"]["hot_temp"] = config[opt]["hot"]
|
|
||||||
else:
|
|
||||||
out_config["temp_control"]["hot_temp"] = 80.0
|
|
||||||
|
|
||||||
if "danger" in config[opt].keys():
|
|
||||||
out_config["temp_control"]["dangerous_temp"] = config[opt]["danger"]
|
|
||||||
else:
|
|
||||||
out_config["temp_control"]["dangerous_temp"] = 90.0
|
|
||||||
elif opt == "fans":
|
|
||||||
out_config["fan_control"] = {}
|
|
||||||
if "min_fans" in config[opt].keys():
|
|
||||||
out_config["fan_control"]["min_fans"] = config[opt]["min_fans"]
|
|
||||||
else:
|
|
||||||
out_config["fan_control"]["min_fans"] = 1
|
|
||||||
if "speed" in config[opt].keys():
|
|
||||||
out_config["fan_control"]["speed"] = config[opt]["speed"]
|
|
||||||
else:
|
|
||||||
out_config["fan_control"]["speed"] = 100
|
|
||||||
elif opt == "pool_groups":
|
|
||||||
out_config["group"] = [{} for _item in range(len(config[opt]))]
|
|
||||||
for idx in range(len(config[opt])):
|
|
||||||
out_config["group"][idx]["pools"] = []
|
|
||||||
out_config["group"][idx] = {}
|
|
||||||
if "group_name" in config[opt][idx].keys():
|
|
||||||
out_config["group"][idx]["name"] = config[opt][idx]["group_name"]
|
|
||||||
else:
|
|
||||||
out_config["group"][idx]["name"] = f"group_{idx}"
|
|
||||||
if "quota" in config[opt][idx].keys():
|
|
||||||
out_config["group"][idx]["quota"] = config[opt][idx]["quota"]
|
|
||||||
else:
|
|
||||||
out_config["group"][idx]["quota"] = 1
|
|
||||||
out_config["group"][idx]["pool"] = [{} for _item in range(len(config[opt][idx]["pools"]))]
|
|
||||||
for pool_idx in range(len(config[opt][idx]["pools"])):
|
|
||||||
out_config["group"][idx]["pool"][pool_idx]["url"] = config[opt][idx]["pools"][pool_idx]["url"]
|
|
||||||
out_config["group"][idx]["pool"][pool_idx]["user"] = config[opt][idx]["pools"][pool_idx]["username"]
|
|
||||||
out_config["group"][idx]["pool"][pool_idx]["password"] = config[opt][idx]["pools"][pool_idx]["password"]
|
|
||||||
elif opt == "autotuning":
|
|
||||||
out_config["autotuning"] = {}
|
|
||||||
if "enabled" in config[opt].keys():
|
|
||||||
out_config["autotuning"]["enabled"] = config[opt]["enabled"]
|
|
||||||
else:
|
|
||||||
out_config["autotuning"]["enabled"] = True
|
|
||||||
if "wattage" in config[opt].keys():
|
|
||||||
out_config["autotuning"]["psu_power_limit"] = config[opt]["wattage"]
|
|
||||||
else:
|
|
||||||
out_config["autotuning"]["psu_power_limit"] = 900
|
|
||||||
elif opt == "power_scaling":
|
|
||||||
out_config["power_scaling"] = {}
|
|
||||||
if "enabled" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["enabled"] = config[opt]["enabled"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["enabled"] = False
|
|
||||||
if "power_step" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["power_step"] = config[opt]["power_step"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["power_step"] = 100
|
|
||||||
if "min_psu_power_limit" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["min_psu_power_limit"] = config[opt]["min_psu_power_limit"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["min_psu_power_limit"] = 800
|
|
||||||
if "shutdown_enabled" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["shutdown_enabled"] = config[opt]["shutdown_enabled"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["shutdown_enabled"] = False
|
|
||||||
if "shutdown_duration" in config[opt].keys():
|
|
||||||
out_config["power_scaling"]["shutdown_duration"] = config[opt]["shutdown_duration"]
|
|
||||||
else:
|
|
||||||
out_config["power_scaling"]["shutdown_duration"] = 3.0
|
|
||||||
return toml.dumps(out_config)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
|
|
||||||
config cgminer 'default'
|
|
||||||
option pool1pw 'x'
|
|
||||||
option pool2pw 'x'
|
|
||||||
option pool3pw 'x'
|
|
||||||
option voltage_level_offset '0'
|
|
||||||
option fan '10'
|
|
||||||
option api_allow 'W:0/0'
|
|
||||||
option power_mode 'balance'
|
|
||||||
option pool1url 'stratum+tcp://ca.stratum.slushpool.com:3333'
|
|
||||||
option pool1user 'poolacct.worker1'
|
|
||||||
option pool2url 'stratum+tcp://ca.stratum.slushpool.com:3333'
|
|
||||||
option pool2user 'poolacct.worker2'
|
|
||||||
option pool3url 'stratum+tcp://ca.stratum.slushpool.com:3333'
|
|
||||||
option pool3user 'poolacct.worker3'
|
|
||||||
option ntp_enable 'openwrt'
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from cfg_util import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
42
docs/api.md
Normal file
42
docs/api.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# pyasic
|
||||||
|
## Miner APIs
|
||||||
|
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 miner that is a subclass of `BaseMiner` should have an API linked to it as `Miner.api`.
|
||||||
|
|
||||||
|
All API implementations inherit from `BaseMinerAPI`, which implements the basic communications protocols.
|
||||||
|
|
||||||
|
## BMMinerAPI
|
||||||
|
::: pyasic.API.bmminer.BMMinerAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## BOSMinerAPI
|
||||||
|
::: pyasic.API.bosminer.BOSMinerAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## BTMinerAPI
|
||||||
|
::: pyasic.API.btminer.BTMinerAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## CGMinerAPI
|
||||||
|
::: pyasic.API.cgminer.CGMinerAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## UnknownAPI
|
||||||
|
::: pyasic.API.unknown.UnknownAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
99
docs/index.md
Normal file
99
docs/index.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# pyasic
|
||||||
|
*A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH.*
|
||||||
|
|
||||||
|
[](https://github.com/psf/black)
|
||||||
|
[](https://pypi.org/project/pyasic/)
|
||||||
|
[](https://pypi.org/project/pyasic/)
|
||||||
|
[](https://pyasic.readthedocs.io/en/latest/)
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
Welcome to pyasic! Pyasic uses an asynchronous method of communicating with asic miners on your network, which makes it super fast.
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
## Scanning for miners
|
||||||
|
To scan for miners in pyasic, we use the class [`MinerNetwork`][pyasic.network.MinerNetwork], which abstracts the search, communication, identification, setup, and return of a miner to 1 command.
|
||||||
|
The command [`MinerNetwork().scan_network_for_miners()`][pyasic.network.MinerNetwork.scan_network_for_miners] returns a list that contains any miners found.
|
||||||
|
```python
|
||||||
|
import asyncio # asyncio for handling the async part
|
||||||
|
from pyasic.network import MinerNetwork # miner network handles the scanning
|
||||||
|
|
||||||
|
|
||||||
|
async def scan_miners(): # define async scan function to allow awaiting
|
||||||
|
# create a miner network
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# scan for miners asynchronously
|
||||||
|
# this will return the correct type of miners if they are supported with all functionality.
|
||||||
|
miners = await network.scan_network_for_miners()
|
||||||
|
print(miners)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(scan_miners()) # run the scan asynchronously with asyncio.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Creating miners based on IP
|
||||||
|
If you already know the IP address of your miner or miners, you can use the [`MinerFactory`][pyasic.miners.miner_factory.MinerFactory] to communicate and identify the miners.
|
||||||
|
The function [`MinerFactory().get_miner()`][pyasic.miners.miner_factory.MinerFactory.get_miner] will return any miner it found at the IP address specified, or an `UnknownMiner` if it cannot identify the miner.
|
||||||
|
```python
|
||||||
|
import asyncio # asyncio for handling the async part
|
||||||
|
from pyasic.miners.miner_factory import MinerFactory # miner factory handles miners creation
|
||||||
|
|
||||||
|
|
||||||
|
async def get_miners(): # define async scan function to allow awaiting
|
||||||
|
# get the miner with miner factory
|
||||||
|
# miner factory is a singleton, and will always use the same object and cache
|
||||||
|
# this means you can always call it as MinerFactory().get_miner()
|
||||||
|
miner_1 = await MinerFactory().get_miner("192.168.1.75")
|
||||||
|
miner_2 = await MinerFactory().get_miner("192.168.1.76")
|
||||||
|
print(miner_1, miner_2)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Getting data from miners
|
||||||
|
|
||||||
|
Once you have your miner(s) identified, you will likely want to get data from the miner(s). You can do this using a built in function in each miner called `get_data()`.
|
||||||
|
This function will return a instance of the dataclass [`MinerData`][pyasic.data.MinerData] with all data it can gather from the miner.
|
||||||
|
Each piece of data in a [`MinerData`][pyasic.data.MinerData] instance can be referenced by getting it as an attribute, such as [`MinerData().hashrate`][pyasic.data.MinerData].
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
from pyasic.miners.miner_factory import MinerFactory
|
||||||
|
|
||||||
|
async def gather_miner_data():
|
||||||
|
miner = await MinerFactory().get_miner("192.168.1.75")
|
||||||
|
miner_data = await miner.get_data()
|
||||||
|
print(miner_data) # all data from the dataclass
|
||||||
|
print(miner_data.hashrate) # hashrate of the miner in TH/s
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(gather_miner_data())
|
||||||
|
```
|
||||||
|
|
||||||
|
You can do something similar with multiple miners, with only needing to make a small change to get all the data at once.
|
||||||
|
```python
|
||||||
|
import asyncio # asyncio for handling the async part
|
||||||
|
from pyasic.network import MinerNetwork # miner network handles the scanning
|
||||||
|
|
||||||
|
|
||||||
|
async def gather_miner_data(): # define async scan function to allow awaiting
|
||||||
|
network = MinerNetwork("192.168.1.50")
|
||||||
|
miners = await network.scan_network_for_miners()
|
||||||
|
|
||||||
|
# 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])
|
||||||
|
|
||||||
|
for miner_data in all_miner_data:
|
||||||
|
print(miner_data) # print out all the data one by one
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(gather_miner_data())
|
||||||
|
```
|
||||||
8
docs/miner_data.md
Normal file
8
docs/miner_data.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# pyasic
|
||||||
|
## Miner Data
|
||||||
|
|
||||||
|
::: pyasic.data.MinerData
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
8
docs/miner_factory.md
Normal file
8
docs/miner_factory.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# pyasic
|
||||||
|
## Miner Factory
|
||||||
|
|
||||||
|
::: pyasic.miners.miner_factory.MinerFactory
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
8
docs/miner_network.md
Normal file
8
docs/miner_network.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# pyasic
|
||||||
|
## Miner Network
|
||||||
|
|
||||||
|
::: pyasic.network.MinerNetwork
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
3
docs/requirements.txt
Normal file
3
docs/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
jinja2<3.1.0
|
||||||
|
mkdocs
|
||||||
|
mkdocstrings[python]
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""
|
|
||||||
Make a build of the config tool.
|
|
||||||
|
|
||||||
Usage: make_config_tool.py build
|
|
||||||
|
|
||||||
The build will show up in the build directory.
|
|
||||||
"""
|
|
||||||
import datetime
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
from cx_Freeze import setup, Executable
|
|
||||||
|
|
||||||
base = None
|
|
||||||
if sys.platform == "win32":
|
|
||||||
base = "Win32GUI"
|
|
||||||
|
|
||||||
version = datetime.datetime.now()
|
|
||||||
version = version.strftime("%y.%m.%d")
|
|
||||||
print(version)
|
|
||||||
setup(name="UpstreamCFGUtil.exe",
|
|
||||||
version=version,
|
|
||||||
description="Upstream Data Config Utility Build",
|
|
||||||
options={"build_exe": {"build_exe": f"{os.getcwd()}\\build\\UpstreamCFGUtil-{version}-{sys.platform}\\"}},
|
|
||||||
executables=[Executable("config_tool.py", base=base, icon="icon.ico", target_name="UpstreamCFGUtil.exe")]
|
|
||||||
)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
from API.bmminer import BMMinerAPI
|
|
||||||
from API.bosminer import BOSMinerAPI
|
|
||||||
from API.cgminer import CGMinerAPI
|
|
||||||
from API.btminer import BTMinerAPI
|
|
||||||
from API.unknown import UnknownAPI
|
|
||||||
import ipaddress
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMiner:
|
|
||||||
def __init__(self, ip: str, api: BMMinerAPI | BOSMinerAPI | CGMinerAPI | BTMinerAPI | UnknownAPI) -> None:
|
|
||||||
self.ip = ipaddress.ip_address(ip)
|
|
||||||
self.api = api
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
from miners import BaseMiner
|
|
||||||
from API.bosminer import BOSMinerAPI
|
|
||||||
import asyncssh
|
|
||||||
import toml
|
|
||||||
from config.bos import bos_config_convert, general_config_convert_bos
|
|
||||||
|
|
||||||
|
|
||||||
class BOSMinerS9(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BOSMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
self.config = None
|
|
||||||
self.uname = 'root'
|
|
||||||
self.pwd = 'admin'
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"S9 - BOSminer: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
|
||||||
"""Create a new asyncssh connection"""
|
|
||||||
conn = await asyncssh.connect(str(self.ip), known_hosts=None, username=self.uname, password=self.pwd,
|
|
||||||
server_host_key_algs=['ssh-rsa'])
|
|
||||||
# return created connection
|
|
||||||
return conn
|
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd: str) -> None:
|
|
||||||
"""Sends SSH command to miner."""
|
|
||||||
# creates result variable
|
|
||||||
result = None
|
|
||||||
|
|
||||||
# runs the command on the miner
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
# attempt to run command up to 3 times
|
|
||||||
for i in range(3):
|
|
||||||
try:
|
|
||||||
# save result of the command
|
|
||||||
result = await conn.run(cmd)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{cmd} error: {e}")
|
|
||||||
if i == 3:
|
|
||||||
return
|
|
||||||
continue
|
|
||||||
|
|
||||||
# let the user know the result of the command
|
|
||||||
if result is not None:
|
|
||||||
if result.stdout != "":
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
elif result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
else:
|
|
||||||
print(cmd)
|
|
||||||
|
|
||||||
async def fault_light_on(self) -> None:
|
|
||||||
"""Sends command to turn on fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light on')
|
|
||||||
|
|
||||||
async def fault_light_off(self) -> None:
|
|
||||||
"""Sends command to turn off fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light off')
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
"""Restart bosminer hashing process."""
|
|
||||||
await self.send_ssh_command('/etc/init.d/bosminer restart')
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
"""Reboots power to the physical miner."""
|
|
||||||
await self.send_ssh_command('/sbin/reboot')
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml') as file:
|
|
||||||
toml_data = toml.loads(await file.read())
|
|
||||||
cfg = await bos_config_convert(toml_data)
|
|
||||||
self.config = cfg
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
"""Attempts to get hostname from miner."""
|
|
||||||
try:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
|
||||||
return data.stdout.strip()
|
|
||||||
except Exception as e:
|
|
||||||
print(self.ip, e)
|
|
||||||
return "BOSMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self, yaml_config) -> None:
|
|
||||||
"""Configures miner with yaml config."""
|
|
||||||
toml_conf = await general_config_convert_bos(yaml_config)
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
|
|
||||||
await file.write(toml_conf)
|
|
||||||
await conn.run("/etc/init.d/bosminer restart")
|
|
||||||
|
|
||||||
async def get_bad_boards(self) -> list:
|
|
||||||
"""Checks for and provides list of non working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if bad > 0:
|
|
||||||
return [str(self.ip), bad]
|
|
||||||
|
|
||||||
async def check_good_boards(self) -> str:
|
|
||||||
"""Checks for and provides list for working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if not bad > 0:
|
|
||||||
return str(self.ip)
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
from miners import BaseMiner
|
|
||||||
from API.bosminer import BOSMinerAPI
|
|
||||||
import asyncssh
|
|
||||||
import toml
|
|
||||||
from config.bos import bos_config_convert, general_config_convert_bos
|
|
||||||
|
|
||||||
|
|
||||||
class BOSminerX17(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BOSMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
self.config = None
|
|
||||||
self.uname = 'root'
|
|
||||||
self.pwd = 'admin'
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"X17 - BOSminer: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
|
||||||
"""Create a new asyncssh connection"""
|
|
||||||
conn = await asyncssh.connect(str(self.ip), known_hosts=None, username=self.uname, password=self.pwd,
|
|
||||||
server_host_key_algs=['ssh-rsa'])
|
|
||||||
# return created connection
|
|
||||||
return conn
|
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd: str) -> None:
|
|
||||||
"""Sends SSH command to miner."""
|
|
||||||
# creates result variable
|
|
||||||
result = None
|
|
||||||
|
|
||||||
# runs the command on the miner
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
# attempt to run command up to 3 times
|
|
||||||
for i in range(3):
|
|
||||||
try:
|
|
||||||
# save result of the command
|
|
||||||
result = await conn.run(cmd)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{cmd} error: {e}")
|
|
||||||
if i == 3:
|
|
||||||
return
|
|
||||||
continue
|
|
||||||
|
|
||||||
# let the user know the result of the command
|
|
||||||
if result is not None:
|
|
||||||
if result.stdout != "":
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
elif result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
else:
|
|
||||||
print(cmd)
|
|
||||||
|
|
||||||
async def fault_light_on(self) -> None:
|
|
||||||
"""Sends command to turn on fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light on')
|
|
||||||
|
|
||||||
async def fault_light_off(self) -> None:
|
|
||||||
"""Sends command to turn off fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light off')
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
"""Restart bosminer hashing process."""
|
|
||||||
await self.send_ssh_command('/etc/init.d/bosminer restart')
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
"""Reboots power to the physical miner."""
|
|
||||||
await self.send_ssh_command('/sbin/reboot')
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml') as file:
|
|
||||||
toml_data = toml.loads(await file.read())
|
|
||||||
cfg = await bos_config_convert(toml_data)
|
|
||||||
self.config = cfg
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
"""Attempts to get hostname from miner."""
|
|
||||||
try:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
|
||||||
return data.stdout.strip()
|
|
||||||
except Exception as e:
|
|
||||||
print(self.ip, e)
|
|
||||||
return "BOSMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self, yaml_config) -> None:
|
|
||||||
"""Configures miner with yaml config."""
|
|
||||||
toml_conf = await general_config_convert_bos(yaml_config)
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
|
|
||||||
await file.write(toml_conf)
|
|
||||||
await conn.run("/etc/init.d/bosminer restart")
|
|
||||||
|
|
||||||
async def get_bad_boards(self) -> list:
|
|
||||||
"""Checks for and provides list of non working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if bad > 0:
|
|
||||||
return [str(self.ip), bad]
|
|
||||||
|
|
||||||
async def check_good_boards(self) -> str:
|
|
||||||
"""Checks for and provides list for working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if not bad > 0:
|
|
||||||
return str(self.ip)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from API.bmminer import BMMinerAPI
|
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BMMiner(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BMMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"BMMiner: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BMMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self, _):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None # Murray
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None # Murray
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None # Murray
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
from miners import BaseMiner
|
|
||||||
from API.bosminer import BOSMinerAPI
|
|
||||||
import asyncssh
|
|
||||||
import toml
|
|
||||||
from config.bos import bos_config_convert, general_config_convert_bos
|
|
||||||
|
|
||||||
|
|
||||||
class BOSminer(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BOSMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
self.config = None
|
|
||||||
self.uname = 'root'
|
|
||||||
self.pwd = 'admin'
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"BOSminer: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
|
||||||
"""Create a new asyncssh connection"""
|
|
||||||
conn = await asyncssh.connect(str(self.ip), known_hosts=None, username=self.uname, password=self.pwd,
|
|
||||||
server_host_key_algs=['ssh-rsa'])
|
|
||||||
# return created connection
|
|
||||||
return conn
|
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd: str) -> None:
|
|
||||||
"""Sends SSH command to miner."""
|
|
||||||
# creates result variable
|
|
||||||
result = None
|
|
||||||
|
|
||||||
# runs the command on the miner
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
# attempt to run command up to 3 times
|
|
||||||
for i in range(3):
|
|
||||||
try:
|
|
||||||
# save result of the command
|
|
||||||
result = await conn.run(cmd)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{cmd} error: {e}")
|
|
||||||
if i == 3:
|
|
||||||
return
|
|
||||||
continue
|
|
||||||
|
|
||||||
# let the user know the result of the command
|
|
||||||
if result is not None:
|
|
||||||
if result.stdout != "":
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
elif result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
else:
|
|
||||||
print(cmd)
|
|
||||||
|
|
||||||
async def fault_light_on(self) -> None:
|
|
||||||
"""Sends command to turn on fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light on')
|
|
||||||
|
|
||||||
async def fault_light_off(self) -> None:
|
|
||||||
"""Sends command to turn off fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light off')
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
"""Restart bosminer hashing process."""
|
|
||||||
await self.send_ssh_command('/etc/init.d/bosminer restart')
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
"""Reboots power to the physical miner."""
|
|
||||||
await self.send_ssh_command('/sbin/reboot')
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml') as file:
|
|
||||||
toml_data = toml.loads(await file.read())
|
|
||||||
cfg = await bos_config_convert(toml_data)
|
|
||||||
self.config = cfg
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
"""Attempts to get hostname from miner."""
|
|
||||||
try:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
|
||||||
return data.stdout.strip()
|
|
||||||
except Exception as e:
|
|
||||||
print(self.ip, e)
|
|
||||||
return "BOSMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self, yaml_config) -> None:
|
|
||||||
"""Configures miner with yaml config."""
|
|
||||||
toml_conf = await general_config_convert_bos(yaml_config)
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
|
|
||||||
await file.write(toml_conf)
|
|
||||||
await conn.run("/etc/init.d/bosminer restart")
|
|
||||||
|
|
||||||
async def get_bad_boards(self) -> list:
|
|
||||||
"""Checks for and provides list of non working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if bad > 0:
|
|
||||||
return [str(self.ip), bad]
|
|
||||||
|
|
||||||
async def check_good_boards(self) -> str:
|
|
||||||
"""Checks for and provides list for working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if not bad > 0:
|
|
||||||
return str(self.ip)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from API.btminer import BTMinerAPI
|
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMiner(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BTMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"BTMiner: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self, _):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
from miners import BaseMiner
|
|
||||||
from API.cgminer import CGMinerAPI
|
|
||||||
import asyncssh
|
|
||||||
|
|
||||||
|
|
||||||
class CGMiner(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = CGMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
self.config = None
|
|
||||||
self.uname = 'root'
|
|
||||||
self.pwd = 'admin'
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"CGMiner: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
try:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
if conn is not None:
|
|
||||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
|
||||||
return data.stdout.strip()
|
|
||||||
else:
|
|
||||||
return "CGMiner Unknown"
|
|
||||||
except Exception:
|
|
||||||
return "CGMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self, _):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
|
||||||
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="admin",
|
|
||||||
password="admin",
|
|
||||||
server_host_key_algs=['ssh-rsa'])
|
|
||||||
return conn
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
except OSError:
|
|
||||||
print(str(self.ip) + " Connection refused.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd):
|
|
||||||
result = None
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
for i in range(3):
|
|
||||||
try:
|
|
||||||
result = await conn.run(cmd)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{cmd} error: {e}")
|
|
||||||
if i == 3:
|
|
||||||
return
|
|
||||||
continue
|
|
||||||
# handle result
|
|
||||||
self._result_handler(result)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _result_handler(result: asyncssh.process.SSHCompletedProcess) -> None:
|
|
||||||
if result is not None:
|
|
||||||
if len(result.stdout) > 0:
|
|
||||||
print("ssh stdout: \n" + result.stdout)
|
|
||||||
if len(result.stderr) > 0:
|
|
||||||
print("ssh stderr: \n" + result.stderrr)
|
|
||||||
if len(result.stdout) <= 0 and len(result.stderr) <= 0:
|
|
||||||
print("ssh stdout stderr empty")
|
|
||||||
# if result.stdout != "":
|
|
||||||
# print(result.stdout)
|
|
||||||
# if result.stderr != "":
|
|
||||||
# print("ERROR: " + result.stderr)
|
|
||||||
# elif result.stderr != "":
|
|
||||||
# print("ERROR: " + result.stderr)
|
|
||||||
# else:
|
|
||||||
# print(cmd)
|
|
||||||
|
|
||||||
async def restart_cgminer(self) -> None:
|
|
||||||
commands = ['cgminer-api restart',
|
|
||||||
'/usr/bin/cgminer-monitor >/dev/null 2>&1']
|
|
||||||
commands = ';'.join(commands)
|
|
||||||
await self.send_ssh_command(commands)
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
commands = ['reboot']
|
|
||||||
commands = ';'.join(commands)
|
|
||||||
await self.send_ssh_command(commands)
|
|
||||||
|
|
||||||
async def start_cgminer(self) -> None:
|
|
||||||
commands = ['mkdir -p /etc/tmp/',
|
|
||||||
'echo \"*/3 * * * * /usr/bin/cgminer-monitor\" > /etc/tmp/root',
|
|
||||||
'crontab -u root /etc/tmp/root',
|
|
||||||
'/usr/bin/cgminer-monitor >/dev/null 2>&1']
|
|
||||||
commands = ';'.join(commands)
|
|
||||||
await self.send_ssh_command(commands)
|
|
||||||
|
|
||||||
async def stop_cgminer(self) -> None:
|
|
||||||
commands = ['mkdir -p /etc/tmp/',
|
|
||||||
'echo \"\" > /etc/tmp/root',
|
|
||||||
'crontab -u root /etc/tmp/root',
|
|
||||||
'killall cgminer']
|
|
||||||
commands = ';'.join(commands)
|
|
||||||
await self.send_ssh_command(commands)
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
command = 'cat /etc/config/cgminer'
|
|
||||||
result = await conn.run(command, check=True)
|
|
||||||
self._result_handler(result)
|
|
||||||
self.config = result.stdout
|
|
||||||
print(str(self.config))
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
from miners.bosminer import BOSminer
|
|
||||||
from miners.bmminer import BMMiner
|
|
||||||
from miners.cgminer import CGMiner
|
|
||||||
from miners.btminer import BTMiner
|
|
||||||
from miners.unknown import UnknownMiner
|
|
||||||
from API import APIError
|
|
||||||
import asyncio
|
|
||||||
import ipaddress
|
|
||||||
import json
|
|
||||||
|
|
||||||
from settings import MINER_FACTORY_GET_VERSION_RETRIES as GET_VERSION_RETRIES
|
|
||||||
|
|
||||||
|
|
||||||
class MinerFactory:
|
|
||||||
def __init__(self):
|
|
||||||
self.miners = {}
|
|
||||||
|
|
||||||
async def get_miner_generator(self, ips: list):
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
scan_tasks = []
|
|
||||||
for miner in ips:
|
|
||||||
scan_tasks.append(loop.create_task(self.get_miner(miner)))
|
|
||||||
scanned = asyncio.as_completed(scan_tasks)
|
|
||||||
for miner in scanned:
|
|
||||||
yield await miner
|
|
||||||
|
|
||||||
async def get_miner(self, ip: ipaddress.ip_address) -> BOSminer or CGMiner or BMMiner or UnknownMiner:
|
|
||||||
"""Decide a miner type using the IP address of the miner."""
|
|
||||||
# check if the miner already exists in cache
|
|
||||||
if ip in self.miners:
|
|
||||||
return self.miners[ip]
|
|
||||||
# get the version data
|
|
||||||
version = None
|
|
||||||
for i in range(GET_VERSION_RETRIES):
|
|
||||||
version_data = await self._get_version_data(ip)
|
|
||||||
if version_data:
|
|
||||||
# if we got version data, get a list of the keys so we can check type of miner
|
|
||||||
version = list(version_data['VERSION'][0].keys())
|
|
||||||
break
|
|
||||||
if version:
|
|
||||||
# check version against different return miner types
|
|
||||||
if "BOSminer" in version or "BOSminer+" in version:
|
|
||||||
miner = BOSminer(str(ip))
|
|
||||||
elif "CGMiner" in version:
|
|
||||||
miner = CGMiner(str(ip))
|
|
||||||
elif "BMMiner" in version:
|
|
||||||
miner = BMMiner(str(ip))
|
|
||||||
elif "BTMiner" in version:
|
|
||||||
miner = BTMiner(str(ip))
|
|
||||||
else:
|
|
||||||
print(f"Bad API response: {version}")
|
|
||||||
miner = UnknownMiner(str(ip))
|
|
||||||
else:
|
|
||||||
# if we don't get version, miner type is unknown
|
|
||||||
print(f"No API response: {str(ip)}")
|
|
||||||
miner = UnknownMiner(str(ip))
|
|
||||||
# save the miner in cache
|
|
||||||
self.miners[ip] = miner
|
|
||||||
return miner
|
|
||||||
|
|
||||||
def clear_cached_miners(self):
|
|
||||||
"""Clear the miner factory cache."""
|
|
||||||
self.miners = {}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _get_version_data(ip: ipaddress.ip_address) -> dict or None:
|
|
||||||
"""Get data on the version of the miner to return the right miner."""
|
|
||||||
for i in range(3):
|
|
||||||
try:
|
|
||||||
# open a connection to the miner
|
|
||||||
fut = asyncio.open_connection(str(ip), 4028)
|
|
||||||
# get reader and writer streams
|
|
||||||
try:
|
|
||||||
reader, writer = await asyncio.wait_for(fut, timeout=7)
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# create the command
|
|
||||||
cmd = {"command": "version"}
|
|
||||||
|
|
||||||
# send the command
|
|
||||||
writer.write(json.dumps(cmd).encode('utf-8'))
|
|
||||||
await writer.drain()
|
|
||||||
|
|
||||||
# instantiate data
|
|
||||||
data = b""
|
|
||||||
|
|
||||||
# loop to receive all the data
|
|
||||||
while True:
|
|
||||||
d = await reader.read(4096)
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
data += d
|
|
||||||
|
|
||||||
if data.endswith(b"\x00"):
|
|
||||||
data = json.loads(data.decode('utf-8')[:-1])
|
|
||||||
else:
|
|
||||||
# some stupid whatsminers need a different command
|
|
||||||
fut = asyncio.open_connection(str(ip), 4028)
|
|
||||||
# get reader and writer streams
|
|
||||||
try:
|
|
||||||
reader, writer = await asyncio.wait_for(fut, timeout=7)
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# create the command
|
|
||||||
cmd = {"command": "get_version"}
|
|
||||||
|
|
||||||
# send the command
|
|
||||||
writer.write(json.dumps(cmd).encode('utf-8'))
|
|
||||||
await writer.drain()
|
|
||||||
|
|
||||||
# instantiate data
|
|
||||||
data = b""
|
|
||||||
|
|
||||||
# loop to receive all the data
|
|
||||||
while True:
|
|
||||||
d = await reader.read(4096)
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
data += d
|
|
||||||
|
|
||||||
data = data.decode('utf-8').replace("\n", "")
|
|
||||||
data = json.loads(data)
|
|
||||||
|
|
||||||
# close the connection
|
|
||||||
writer.close()
|
|
||||||
await writer.wait_closed()
|
|
||||||
# check if the data returned is correct or an error
|
|
||||||
# if status isn't a key, it is a multicommand
|
|
||||||
if "STATUS" not in data.keys():
|
|
||||||
for key in data.keys():
|
|
||||||
# make sure not to try to turn id into a dict
|
|
||||||
if not key == "id":
|
|
||||||
# make sure they succeeded
|
|
||||||
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
|
|
||||||
# this is an error
|
|
||||||
raise APIError(data["STATUS"][0]["Msg"])
|
|
||||||
else:
|
|
||||||
# check for stupid whatsminer formatting
|
|
||||||
if not isinstance(data["STATUS"], list):
|
|
||||||
if data["STATUS"] not in ("S", "I"):
|
|
||||||
raise APIError(data["Msg"])
|
|
||||||
else:
|
|
||||||
if "whatsminer" in data["Description"]:
|
|
||||||
return {"VERSION": [{"BTMiner": data["Description"]}]}
|
|
||||||
# make sure the command succeeded
|
|
||||||
elif data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
|
||||||
# this is an error
|
|
||||||
raise APIError(data["STATUS"][0]["Msg"])
|
|
||||||
# return the data
|
|
||||||
return data
|
|
||||||
except OSError as e:
|
|
||||||
if e.winerror == 121:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
print(ip, e)
|
|
||||||
# except json.decoder.JSONDecodeError:
|
|
||||||
# print("Decode Error @ " + str(ip) + str(data))
|
|
||||||
# except Exception as e:
|
|
||||||
# print(ip, e)
|
|
||||||
return None
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
from API.unknown import UnknownAPI
|
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownMiner(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = UnknownAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"Unknown: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def send_config(self, _):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_hostname(self):
|
|
||||||
return "Unknown"
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from API.btminer import BTMinerAPI
|
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM20(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BTMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"M20 - BTMiner: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from API.btminer import BTMinerAPI
|
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM21(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BTMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"M21 - BTMiner: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from API.btminer import BTMinerAPI
|
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM30(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BTMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"M30 - BTMiner: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from API.btminer import BTMinerAPI
|
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM31(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BTMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"M31 - BTMiner: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from API.btminer import BTMinerAPI
|
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM32(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BTMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"M32 - BTMiner: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
14
mkdocs.yml
Normal file
14
mkdocs.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
site_name: pyasic
|
||||||
|
repo_url: https://github.com/UpstreamData/pyasic
|
||||||
|
nav:
|
||||||
|
- Introduction: "index.md"
|
||||||
|
- Usage:
|
||||||
|
- Miner Factory: "miner_factory.md"
|
||||||
|
- Miner Network: "miner_network.md"
|
||||||
|
- Miner Data: "miner_data.md"
|
||||||
|
- Advanced:
|
||||||
|
- API: "api.md"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- mkdocstrings
|
||||||
|
- search
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import ipaddress
|
|
||||||
import asyncio
|
|
||||||
from miners.miner_factory import MinerFactory
|
|
||||||
from settings import NETWORK_PING_RETRIES as PING_RETRIES, NETWORK_PING_TIMEOUT as PING_TIMEOUT, \
|
|
||||||
NETWORK_SCAN_THREADS as SCAN_THREADS
|
|
||||||
|
|
||||||
|
|
||||||
class MinerNetwork:
|
|
||||||
def __init__(self, ip_addr: str or None = None, mask: str or int or None = None) -> None:
|
|
||||||
self.network = None
|
|
||||||
self.miner_factory = MinerFactory()
|
|
||||||
self.ip_addr = ip_addr
|
|
||||||
self.connected_miners = {}
|
|
||||||
self.mask = mask
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len([item for item in self.get_network().hosts()])
|
|
||||||
|
|
||||||
def get_network(self) -> ipaddress.ip_network:
|
|
||||||
"""Get the network using the information passed to the MinerNetwork or from cache."""
|
|
||||||
if self.network:
|
|
||||||
return self.network
|
|
||||||
if not self.ip_addr:
|
|
||||||
default_gateway = "192.168.1.0"
|
|
||||||
else:
|
|
||||||
default_gateway = self.ip_addr
|
|
||||||
if self.mask:
|
|
||||||
subnet_mask = str(self.mask)
|
|
||||||
else:
|
|
||||||
subnet_mask = "24"
|
|
||||||
return ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False)
|
|
||||||
|
|
||||||
async def scan_network_for_miners(self) -> None or list:
|
|
||||||
"""Scan the network for miners, and """
|
|
||||||
local_network = self.get_network()
|
|
||||||
print(f"Scanning {local_network} for miners...")
|
|
||||||
scan_tasks = []
|
|
||||||
miner_ips = []
|
|
||||||
for host in local_network.hosts():
|
|
||||||
if len(scan_tasks) < SCAN_THREADS:
|
|
||||||
scan_tasks.append(self.ping_miner(host))
|
|
||||||
else:
|
|
||||||
miner_ips_scan = await asyncio.gather(*scan_tasks)
|
|
||||||
miner_ips.extend(miner_ips_scan)
|
|
||||||
scan_tasks = []
|
|
||||||
miner_ips_scan = await asyncio.gather(*scan_tasks)
|
|
||||||
miner_ips.extend(miner_ips_scan)
|
|
||||||
miner_ips = list(filter(None, miner_ips))
|
|
||||||
print(f"Found {len(miner_ips)} connected miners...")
|
|
||||||
create_miners_tasks = []
|
|
||||||
self.miner_factory.clear_cached_miners()
|
|
||||||
for miner_ip in miner_ips:
|
|
||||||
create_miners_tasks.append(self.miner_factory.get_miner(miner_ip))
|
|
||||||
miners = await asyncio.gather(*create_miners_tasks)
|
|
||||||
return miners
|
|
||||||
|
|
||||||
async def scan_network_generator(self):
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
local_network = self.get_network()
|
|
||||||
scan_tasks = []
|
|
||||||
for host in local_network.hosts():
|
|
||||||
if len(scan_tasks) >= SCAN_THREADS:
|
|
||||||
scanned = asyncio.as_completed(scan_tasks)
|
|
||||||
scan_tasks = []
|
|
||||||
for miner in scanned:
|
|
||||||
yield await miner
|
|
||||||
scan_tasks.append(loop.create_task(self.ping_miner(host)))
|
|
||||||
scanned = asyncio.as_completed(scan_tasks)
|
|
||||||
for miner in scanned:
|
|
||||||
yield await miner
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def ping_miner(ip: ipaddress.ip_address) -> None or ipaddress.ip_address:
|
|
||||||
for i in range(PING_RETRIES):
|
|
||||||
connection_fut = asyncio.open_connection(str(ip), 4028)
|
|
||||||
try:
|
|
||||||
# get the read and write streams from the connection
|
|
||||||
reader, writer = await asyncio.wait_for(connection_fut, timeout=PING_TIMEOUT)
|
|
||||||
# immediately close connection, we know connection happened
|
|
||||||
writer.close()
|
|
||||||
# make sure the writer is closed
|
|
||||||
await writer.wait_closed()
|
|
||||||
# ping was successful
|
|
||||||
return ip
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
# ping failed if we time out
|
|
||||||
continue
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
# handle for other connection errors
|
|
||||||
print(f"{str(ip)}: Connection Refused.")
|
|
||||||
# ping failed, likely with an exception
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
continue
|
|
||||||
return
|
|
||||||
396
poetry.lock
generated
Normal file
396
poetry.lock
generated
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "3.6.1"
|
||||||
|
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.2"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
idna = ">=2.8"
|
||||||
|
sniffio = ">=1.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
|
||||||
|
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)"]
|
||||||
|
trio = ["trio (>=0.16)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asyncssh"
|
||||||
|
version = "2.11.0"
|
||||||
|
description = "AsyncSSH: Asynchronous SSHv2 client and server library"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">= 3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cryptography = ">=3.1"
|
||||||
|
typing-extensions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
bcrypt = ["bcrypt (>=3.1.3)"]
|
||||||
|
fido2 = ["fido2 (>=0.9.2)"]
|
||||||
|
gssapi = ["gssapi (>=1.2.0)"]
|
||||||
|
libnacl = ["libnacl (>=1.4.2)"]
|
||||||
|
pkcs11 = ["python-pkcs11 (>=0.7.0)"]
|
||||||
|
pyopenssl = ["pyOpenSSL (>=17.0.0)"]
|
||||||
|
pywin32 = ["pywin32 (>=227)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2022.6.15"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "1.15.1"
|
||||||
|
description = "Foreign Function Interface for Python calling C code."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pycparser = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "37.0.4"
|
||||||
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = ">=1.12"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
|
||||||
|
docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||||
|
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
|
||||||
|
sdist = ["setuptools_rust (>=0.11.4)"]
|
||||||
|
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)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.12.0"
|
||||||
|
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "0.15.0"
|
||||||
|
description = "A minimal low-level HTTP client."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
anyio = ">=3.0.0,<4.0.0"
|
||||||
|
certifi = "*"
|
||||||
|
h11 = ">=0.11,<0.13"
|
||||||
|
sniffio = ">=1.0.0,<2.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.23.0"
|
||||||
|
description = "The next generation HTTP client."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = "*"
|
||||||
|
httpcore = ">=0.15.0,<0.16.0"
|
||||||
|
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
|
||||||
|
sniffio = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotlicffi", "brotli"]
|
||||||
|
cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.3"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "passlib"
|
||||||
|
version = "1.7.4"
|
||||||
|
description = "comprehensive password hashing framework supporting over 30 schemes"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
argon2 = ["argon2-cffi (>=18.2.0)"]
|
||||||
|
bcrypt = ["bcrypt (>=3.1.0)"]
|
||||||
|
build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"]
|
||||||
|
totp = ["cryptography"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyaml"
|
||||||
|
version = "21.10.1"
|
||||||
|
description = "PyYAML-based module to produce pretty and readable YAML-serialized data"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
PyYAML = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "2.21"
|
||||||
|
description = "C parser in Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0"
|
||||||
|
description = "YAML parser and emitter for Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfc3986"
|
||||||
|
version = "1.5.0"
|
||||||
|
description = "Validating URI References per RFC 3986"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
idna2008 = ["idna"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.2.0"
|
||||||
|
description = "Sniff out which async library your code is running under"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.10.2"
|
||||||
|
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.3.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "1.1"
|
||||||
|
python-versions = "^3.9"
|
||||||
|
content-hash = "8d93eafd928d7fed4b0a00d13e46982c2d4310c37acb2faec7e7a477b3f35e9c"
|
||||||
|
|
||||||
|
[metadata.files]
|
||||||
|
anyio = [
|
||||||
|
{file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"},
|
||||||
|
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
|
||||||
|
]
|
||||||
|
asyncssh = [
|
||||||
|
{file = "asyncssh-2.11.0-py3-none-any.whl", hash = "sha256:7302348cbd54c58d3259da17f13e77912de1b005e366b15c8b183d948c8a91a8"},
|
||||||
|
{file = "asyncssh-2.11.0.tar.gz", hash = "sha256:59c36ce77ba9dda8dd57ad875776e7105ddb1fa851bc039bb3aeadeac4f67b56"},
|
||||||
|
]
|
||||||
|
certifi = [
|
||||||
|
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
|
||||||
|
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
|
||||||
|
]
|
||||||
|
cffi = [
|
||||||
|
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
|
||||||
|
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
|
||||||
|
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
|
||||||
|
{file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
|
||||||
|
{file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
|
||||||
|
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
|
||||||
|
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
|
||||||
|
{file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
|
||||||
|
{file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
|
||||||
|
{file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
|
||||||
|
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
|
||||||
|
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
|
||||||
|
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
|
||||||
|
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
|
||||||
|
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
|
||||||
|
{file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
|
||||||
|
{file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
|
||||||
|
{file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
|
||||||
|
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
|
||||||
|
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
|
||||||
|
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
|
||||||
|
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
|
||||||
|
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
|
||||||
|
{file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
|
||||||
|
{file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
|
||||||
|
{file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
|
||||||
|
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
|
||||||
|
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
|
||||||
|
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
|
||||||
|
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
|
||||||
|
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
|
||||||
|
{file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
|
||||||
|
{file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
|
||||||
|
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
|
||||||
|
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
|
||||||
|
]
|
||||||
|
cryptography = [
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"},
|
||||||
|
{file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"},
|
||||||
|
{file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"},
|
||||||
|
{file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"},
|
||||||
|
{file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"},
|
||||||
|
{file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"},
|
||||||
|
{file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"},
|
||||||
|
{file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"},
|
||||||
|
{file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"},
|
||||||
|
{file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"},
|
||||||
|
{file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"},
|
||||||
|
{file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"},
|
||||||
|
{file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"},
|
||||||
|
]
|
||||||
|
h11 = [
|
||||||
|
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
|
||||||
|
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
|
||||||
|
]
|
||||||
|
httpcore = [
|
||||||
|
{file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"},
|
||||||
|
{file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"},
|
||||||
|
]
|
||||||
|
httpx = [
|
||||||
|
{file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"},
|
||||||
|
{file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"},
|
||||||
|
]
|
||||||
|
idna = [
|
||||||
|
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||||
|
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||||
|
]
|
||||||
|
passlib = [
|
||||||
|
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
|
||||||
|
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
|
||||||
|
]
|
||||||
|
pyaml = [
|
||||||
|
{file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"},
|
||||||
|
{file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"},
|
||||||
|
]
|
||||||
|
pycparser = [
|
||||||
|
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||||
|
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||||
|
]
|
||||||
|
pyyaml = [
|
||||||
|
{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-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
|
||||||
|
{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-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
||||||
|
{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_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
|
||||||
|
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||||
|
]
|
||||||
|
rfc3986 = [
|
||||||
|
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
|
||||||
|
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
|
||||||
|
]
|
||||||
|
sniffio = [
|
||||||
|
{file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
|
||||||
|
{file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
|
||||||
|
]
|
||||||
|
toml = [
|
||||||
|
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||||
|
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||||
|
]
|
||||||
|
typing-extensions = [
|
||||||
|
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
||||||
|
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
||||||
|
]
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import warnings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class APIError(Exception):
|
class APIError(Exception):
|
||||||
@@ -17,6 +19,20 @@ class APIError(Exception):
|
|||||||
return "Incorrect API parameters."
|
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:
|
||||||
def __init__(self, ip: str, port: int = 4028) -> None:
|
def __init__(self, ip: str, port: int = 4028) -> None:
|
||||||
# api port, should be 4028
|
# api port, should be 4028
|
||||||
@@ -26,36 +42,67 @@ class BaseMinerAPI:
|
|||||||
|
|
||||||
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."""
|
||||||
return [func for func in
|
return [
|
||||||
# each function in self
|
func
|
||||||
dir(self) if callable(getattr(self, func)) and
|
for func in
|
||||||
# no __ methods
|
# each function in self
|
||||||
not func.startswith("__") and
|
dir(self)
|
||||||
# remove all functions that are in this base class
|
if callable(getattr(self, func)) and
|
||||||
func not in
|
# no __ methods
|
||||||
[func for func in
|
not func.startswith("__") and
|
||||||
dir(BaseMinerAPI) if callable(getattr(BaseMinerAPI, func))
|
# remove all functions that are in this base class
|
||||||
]
|
func
|
||||||
]
|
not in [
|
||||||
|
func
|
||||||
|
for func in dir(BaseMinerAPI)
|
||||||
|
if callable(getattr(BaseMinerAPI, func))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
async def multicommand(self, *commands: str) -> dict:
|
async def multicommand(
|
||||||
|
self, *commands: str, ignore_x19_error: bool = False
|
||||||
|
) -> dict:
|
||||||
"""Creates and sends multiple commands as one command to the miner."""
|
"""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
|
# split the commands into a proper list
|
||||||
commands = [*commands]
|
user_commands = [*commands]
|
||||||
|
allowed_commands = self.get_commands()
|
||||||
for item in commands:
|
# make sure we can actually run the command, otherwise it will fail
|
||||||
# make sure we can actually run the command, otherwise it will fail
|
commands = [command for command in user_commands if command in allowed_commands]
|
||||||
if item not in self.get_commands():
|
for item in list(set(user_commands) - set(commands)):
|
||||||
# if the command isnt allowed, remove it
|
warnings.warn(
|
||||||
print(f"Removing incorrect command: {item}")
|
f"""Removing incorrect command: {item}
|
||||||
commands.remove(item)
|
If you are sure you want to use this command please use API.send_command("{item}", ignore_errors=True) instead.""",
|
||||||
|
APIWarning,
|
||||||
|
)
|
||||||
# standard multicommand format is "command1+command2"
|
# standard multicommand format is "command1+command2"
|
||||||
# doesnt work for S19 which is dealt with in the send command function
|
# doesnt work for S19 which is dealt with in the send command function
|
||||||
command = "+".join(commands)
|
command = "+".join(commands)
|
||||||
return await self.send_command(command)
|
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(self, command: str, parameters: str or int or bool = None) -> dict:
|
async def send_command(
|
||||||
|
self,
|
||||||
|
command: str or bytes,
|
||||||
|
parameters: str or int or bool = None,
|
||||||
|
ignore_errors: bool = False,
|
||||||
|
x19_command: bool = False,
|
||||||
|
) -> dict:
|
||||||
"""Send an API command to the miner and return the result."""
|
"""Send an API command to the miner and return the result."""
|
||||||
try:
|
try:
|
||||||
# get reader and writer streams
|
# get reader and writer streams
|
||||||
@@ -63,7 +110,7 @@ class BaseMinerAPI:
|
|||||||
# handle OSError 121
|
# handle OSError 121
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.winerror == "121":
|
if e.winerror == "121":
|
||||||
print("Semaphore Timeout has Expired.")
|
logging.warning("Semaphore Timeout has Expired.")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# create the command
|
# create the command
|
||||||
@@ -72,7 +119,7 @@ class BaseMinerAPI:
|
|||||||
cmd["parameter"] = parameters
|
cmd["parameter"] = parameters
|
||||||
|
|
||||||
# send the command
|
# send the command
|
||||||
writer.write(json.dumps(cmd).encode('utf-8'))
|
writer.write(json.dumps(cmd).encode("utf-8"))
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
# instantiate data
|
# instantiate data
|
||||||
@@ -86,7 +133,7 @@ class BaseMinerAPI:
|
|||||||
break
|
break
|
||||||
data += d
|
data += d
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
logging.warning(f"{self.ip}: API Command Error: {e}")
|
||||||
|
|
||||||
data = self.load_api_data(data)
|
data = self.load_api_data(data)
|
||||||
|
|
||||||
@@ -94,26 +141,19 @@ class BaseMinerAPI:
|
|||||||
writer.close()
|
writer.close()
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
|
|
||||||
# validate the command suceeded
|
# check for if the user wants to allow errors to return
|
||||||
# also handle for S19 not liking "command1+command2" format
|
if not ignore_errors:
|
||||||
if not self.validate_command_output(data):
|
# validate the command succeeded
|
||||||
try:
|
validation = self.validate_command_output(data)
|
||||||
data = {}
|
if not validation[0]:
|
||||||
# S19 handler, try again
|
if not x19_command:
|
||||||
for cmd in command.split("+"):
|
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
|
||||||
data[cmd] = []
|
raise APIError(validation[1])
|
||||||
data[cmd].append(await self.send_command(cmd))
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
# check again after second try
|
|
||||||
if not self.validate_command_output(data):
|
|
||||||
raise APIError(data["STATUS"][0]["Msg"])
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_command_output(data: dict) -> bool:
|
def validate_command_output(data: dict) -> tuple:
|
||||||
"""Check if the returned command output is correctly formatted."""
|
"""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
|
||||||
@@ -122,42 +162,53 @@ class BaseMinerAPI:
|
|||||||
# make sure not to try to turn id into a dict
|
# make sure not to try to turn id into a dict
|
||||||
if not key == "id":
|
if not key == "id":
|
||||||
# make sure they succeeded
|
# make sure they succeeded
|
||||||
if "STATUS" in data.keys():
|
if "STATUS" in data[key][0].keys():
|
||||||
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
|
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
|
||||||
# this is an error
|
# this is an error
|
||||||
return False
|
return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"]
|
||||||
elif "id" not in data.keys():
|
elif "id" not in data.keys():
|
||||||
if data["STATUS"] not in ["S", "I"]:
|
if data["STATUS"] not in ["S", "I"]:
|
||||||
return False
|
return False, data["Msg"]
|
||||||
else:
|
else:
|
||||||
# make sure the command succeeded
|
# make sure the command succeeded
|
||||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
if type(data["STATUS"]) == str:
|
||||||
|
if data["STATUS"] in ["RESTART"]:
|
||||||
|
return True, None
|
||||||
|
elif data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||||
# this is an error
|
# this is an error
|
||||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||||
return False
|
return False, data["STATUS"][0]["Msg"]
|
||||||
return True
|
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"""
|
"""Convert API data from JSON to dict"""
|
||||||
|
str_data = None
|
||||||
try:
|
try:
|
||||||
# some json from the API returns with a null byte (\x00) on the end
|
# some json from the API returns with a null byte (\x00) on the end
|
||||||
if data.endswith(b"\x00"):
|
if data.endswith(b"\x00"):
|
||||||
# handle the null byte
|
# handle the null byte
|
||||||
str_data = data.decode('utf-8')[:-1]
|
str_data = data.decode("utf-8")[:-1]
|
||||||
else:
|
else:
|
||||||
# no null byte
|
# no null byte
|
||||||
str_data = data.decode('utf-8')
|
str_data = data.decode("utf-8")
|
||||||
# fix an error with a btminer return having an extra comma that breaks json.loads()
|
# fix an error with a btminer return having an extra comma that breaks json.loads()
|
||||||
str_data = str_data.replace(",}", "}")
|
str_data = str_data.replace(",}", "}")
|
||||||
# fix an error with a btminer return having a newline that breaks json.loads()
|
# fix an error with a btminer return having a newline that breaks json.loads()
|
||||||
str_data = str_data.replace("\n", "")
|
str_data = str_data.replace("\n", "")
|
||||||
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
|
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
|
||||||
str_data = str_data.replace("}{", "},{")
|
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
|
# parse the json
|
||||||
parsed_data = json.loads(str_data)
|
parsed_data = json.loads(str_data)
|
||||||
# handle bad json
|
# handle bad json
|
||||||
except json.decoder.JSONDecodeError as e:
|
except json.decoder.JSONDecodeError as e:
|
||||||
print(e)
|
raise APIError(f"Decode Error {e}: {str_data}")
|
||||||
raise APIError(f"Decode Error: {data}")
|
|
||||||
return parsed_data
|
return parsed_data
|
||||||
680
pyasic/API/bmminer.py
Normal file
680
pyasic/API/bmminer.py
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
from pyasic.API import BaseMinerAPI
|
||||||
|
|
||||||
|
|
||||||
|
class BMMinerAPI(BaseMinerAPI):
|
||||||
|
"""An abstraction of the BMMiner API.
|
||||||
|
|
||||||
|
Each method corresponds to an API command in BMMiner.
|
||||||
|
|
||||||
|
[BMMiner API documentation](https://github.com/jameshilliard/bmminer/blob/master/API-README)
|
||||||
|
|
||||||
|
This class abstracts use of the BMMiner API, as well as the
|
||||||
|
methods for sending commands to it. The `self.send_command()`
|
||||||
|
function handles sending a command to the miner asynchronously, and
|
||||||
|
as such is the base for many of the functions in this class, which
|
||||||
|
rely on it to send the command for them.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ip: The IP of the miner to reference the API on.
|
||||||
|
port: The port to reference the API on. Default is 4028.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ip: str, port: int = 4028) -> None:
|
||||||
|
super().__init__(ip, port)
|
||||||
|
|
||||||
|
async def version(self) -> dict:
|
||||||
|
"""Get miner version info.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Miner version information.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("version")
|
||||||
|
|
||||||
|
async def config(self) -> dict:
|
||||||
|
"""Get some basic configuration info.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
## Some miner configuration information:
|
||||||
|
* ASC Count <- the number of ASCs
|
||||||
|
* PGA Count <- the number of PGAs
|
||||||
|
* Pool Count <- the number of Pools
|
||||||
|
* Strategy <- the current pool strategy
|
||||||
|
* Log Interval <- the interval of logging
|
||||||
|
* Device Code <- list of compiled device drivers
|
||||||
|
* OS <- the current operating system
|
||||||
|
* Failover-Only <- failover-only setting
|
||||||
|
* Scan Time <- scan-time setting
|
||||||
|
* Queue <- queue setting
|
||||||
|
* Expiry <- expiry setting
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("config")
|
||||||
|
|
||||||
|
async def summary(self) -> dict:
|
||||||
|
"""Get the status summary of the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The status summary of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("summary")
|
||||||
|
|
||||||
|
async def pools(self) -> dict:
|
||||||
|
"""Get pool information.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Miner pool information.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pools")
|
||||||
|
|
||||||
|
async def devs(self) -> dict:
|
||||||
|
"""Get data on each PGA/ASC with their details.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on each PGA/ASC with their details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("devs")
|
||||||
|
|
||||||
|
async def edevs(self, old: bool = False) -> dict:
|
||||||
|
"""Get data on each PGA/ASC with their details, ignoring blacklisted and zombie devices.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old: Include zombie devices that became zombies less than 'old' seconds ago
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on each PGA/ASC with their details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if old:
|
||||||
|
return await self.send_command("edevs", parameters=old)
|
||||||
|
else:
|
||||||
|
return await self.send_command("edevs")
|
||||||
|
|
||||||
|
async def pga(self, n: int) -> dict:
|
||||||
|
"""Get data from PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA number to get data from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on the PGA n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pga", parameters=n)
|
||||||
|
|
||||||
|
async def pgacount(self) -> dict:
|
||||||
|
"""Get data fon all PGAs.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on the PGAs connected.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pgacount")
|
||||||
|
|
||||||
|
async def switchpool(self, n: int) -> dict:
|
||||||
|
"""Switch pools to pool n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The pool to switch to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of switching to pool n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("switchpool", parameters=n)
|
||||||
|
|
||||||
|
async def enablepool(self, n: int) -> dict:
|
||||||
|
"""Enable pool n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The pool to enable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of enabling pool n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("enablepool", parameters=n)
|
||||||
|
|
||||||
|
async def addpool(self, url: str, username: str, password: str) -> dict:
|
||||||
|
"""Add a pool to the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
url: The URL of the new pool to add.
|
||||||
|
username: The users username on the new pool.
|
||||||
|
password: The worker password on the new pool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of adding the pool.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command(
|
||||||
|
"addpool", parameters=f"{url},{username},{password}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def poolpriority(self, *n: int) -> dict:
|
||||||
|
"""Set pool priority.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
*n: Pools in order of priority.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of setting pool priority.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
pools = f"{','.join([str(item) for item in n])}"
|
||||||
|
return await self.send_command("poolpriority", parameters=pools)
|
||||||
|
|
||||||
|
async def poolquota(self, n: int, q: int) -> dict:
|
||||||
|
"""Set pool quota.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: Pool number to set quota on.
|
||||||
|
q: Quota to set the pool to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of setting pool quota.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("poolquota", parameters=f"{n},{q}")
|
||||||
|
|
||||||
|
async def disablepool(self, n: int) -> dict:
|
||||||
|
"""Disable a pool.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: Pool to disable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of diabling the pool.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("disablepool", parameters=n)
|
||||||
|
|
||||||
|
async def removepool(self, n: int) -> dict:
|
||||||
|
"""Remove a pool.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: Pool to remove.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of removing the pool.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("removepool", parameters=n)
|
||||||
|
|
||||||
|
async def save(self, filename: str = None) -> dict:
|
||||||
|
"""Save the config.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
filename: Filename to save the config as.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of saving the config.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if filename:
|
||||||
|
return await self.send_command("save", parameters=filename)
|
||||||
|
else:
|
||||||
|
return await self.send_command("save")
|
||||||
|
|
||||||
|
async def quit(self) -> dict:
|
||||||
|
"""Quit BMMiner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A single "BYE" before BMMiner quits.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("quit")
|
||||||
|
|
||||||
|
async def notify(self) -> dict:
|
||||||
|
"""Notify the user of past errors.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The last status and count of each devices problem(s).
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("notify")
|
||||||
|
|
||||||
|
async def privileged(self) -> dict:
|
||||||
|
"""Check if you have privileged access.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The STATUS section with an error if you have no privileged access, or success if you have privileged access.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("privileged")
|
||||||
|
|
||||||
|
async def pgaenable(self, n: int) -> dict:
|
||||||
|
"""Enable PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA to enable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of enabling PGA n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pgaenable", parameters=n)
|
||||||
|
|
||||||
|
async def pgadisable(self, n: int) -> dict:
|
||||||
|
"""Disable PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA to disable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of disabling PGA n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pgadisable", parameters=n)
|
||||||
|
|
||||||
|
async def pgaidentify(self, n: int) -> dict:
|
||||||
|
"""Identify PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA to identify.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of identifying PGA n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pgaidentify", parameters=n)
|
||||||
|
|
||||||
|
async def devdetails(self) -> dict:
|
||||||
|
"""Get data on all devices with their static details.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on all devices with their static details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("devdetails")
|
||||||
|
|
||||||
|
async def restart(self) -> dict:
|
||||||
|
"""Restart BMMiner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A reply informing of the restart.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("restart")
|
||||||
|
|
||||||
|
async def stats(self) -> dict:
|
||||||
|
"""Get stats of each device/pool with more than 1 getwork.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Stats of each device/pool with more than 1 getwork.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("stats")
|
||||||
|
|
||||||
|
async def estats(self, old: bool = False) -> dict:
|
||||||
|
"""Get stats of each device/pool with more than 1 getwork, ignoring zombie devices.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old: Include zombie devices that became zombies less than 'old' seconds ago.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Stats of each device/pool with more than 1 getwork, ignoring zombie devices.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if old:
|
||||||
|
return await self.send_command("estats", parameters=old)
|
||||||
|
else:
|
||||||
|
return await self.send_command("estats")
|
||||||
|
|
||||||
|
async def check(self, command: str) -> dict:
|
||||||
|
"""Check if the command command exists in BMMiner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
command: The command to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
## Information about a command:
|
||||||
|
* Exists (Y/N) <- the command exists in this version
|
||||||
|
* Access (Y/N) <- you have access to use the command
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("check", parameters=command)
|
||||||
|
|
||||||
|
async def failover_only(self, failover: bool) -> dict:
|
||||||
|
"""Set failover-only.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
failover: What to set failover-only to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of setting failover-only.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("failover-only", parameters=failover)
|
||||||
|
|
||||||
|
async def coin(self) -> dict:
|
||||||
|
"""Get information on the current coin.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
## Information about the current coin being mined:
|
||||||
|
* Hash Method <- the hashing algorithm
|
||||||
|
* Current Block Time <- blocktime as a float, 0 means none
|
||||||
|
* Current Block Hash <- the hash of the current block, blank means none
|
||||||
|
* LP <- whether LP is in use on at least 1 pool
|
||||||
|
* Network Difficulty: the current network difficulty
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("coin")
|
||||||
|
|
||||||
|
async def debug(self, setting: str) -> dict:
|
||||||
|
"""Set a debug setting.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
setting: Which setting to switch to.
|
||||||
|
## Options are:
|
||||||
|
* Silent
|
||||||
|
* Quiet
|
||||||
|
* Verbose
|
||||||
|
* Debug
|
||||||
|
* RPCProto
|
||||||
|
* PerDevice
|
||||||
|
* WorkTime
|
||||||
|
* Normal
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on which debug setting was enabled or disabled.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("debug", parameters=setting)
|
||||||
|
|
||||||
|
async def setconfig(self, name: str, n: int) -> dict:
|
||||||
|
"""Set config of name to value n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name: The name of the config setting to set.
|
||||||
|
## Options are:
|
||||||
|
* queue
|
||||||
|
* scantime
|
||||||
|
* expiry
|
||||||
|
n: The value to set the 'name' setting to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The results of setting config of name to n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("setconfig", parameters=f"{name},{n}")
|
||||||
|
|
||||||
|
async def usbstats(self) -> dict:
|
||||||
|
"""Get stats of all USB devices except ztex.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The stats of all USB devices except ztex.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("usbstats")
|
||||||
|
|
||||||
|
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
|
||||||
|
"""Set PGA option opt to val on PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
```
|
||||||
|
MMQ -
|
||||||
|
opt: clock
|
||||||
|
val: 160 - 230 (multiple of 2)
|
||||||
|
CMR -
|
||||||
|
opt: clock
|
||||||
|
val: 100 - 220
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA to set the options on.
|
||||||
|
opt: The option to set. Setting this to 'help' returns a help message.
|
||||||
|
val: The value to set the option to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of setting PGA n with opt[,val].
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if val:
|
||||||
|
return await self.send_command("pgaset", parameters=f"{n},{opt},{val}")
|
||||||
|
else:
|
||||||
|
return await self.send_command("pgaset", parameters=f"{n},{opt}")
|
||||||
|
|
||||||
|
async def zero(self, which: str, summary: bool) -> dict:
|
||||||
|
"""Zero a device.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
which: Which device to zero. Setting this to 'all' zeros all devices. Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
|
||||||
|
summary: Whether or not to show a full summary.
|
||||||
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the STATUS section with info on the zero and optional summary.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("zero", parameters=f"{which},{summary}")
|
||||||
|
|
||||||
|
async def hotplug(self, n: int) -> dict:
|
||||||
|
"""Enable hotplug.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device number to set hotplug on.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Information on hotplug status.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("hotplug", parameters=n)
|
||||||
|
|
||||||
|
async def asc(self, n: int) -> dict:
|
||||||
|
"""Get data for ASC device n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device to get data for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The data for ASC device n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("asc", parameters=n)
|
||||||
|
|
||||||
|
async def ascenable(self, n: int) -> dict:
|
||||||
|
"""Enable ASC device n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device to enable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of enabling ASC device n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("ascenable", parameters=n)
|
||||||
|
|
||||||
|
async def ascdisable(self, n: int) -> dict:
|
||||||
|
"""Disable ASC device n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device to disable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of disabling ASC device n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("ascdisable", parameters=n)
|
||||||
|
|
||||||
|
async def ascidentify(self, n: int) -> dict:
|
||||||
|
"""Identify ASC device n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device to identify.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of identifying ASC device n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("ascidentify", parameters=n)
|
||||||
|
|
||||||
|
async def asccount(self) -> dict:
|
||||||
|
"""Get data on the number of ASC devices and their info.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on all ASC devices.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("asccount")
|
||||||
|
|
||||||
|
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
|
||||||
|
"""Set ASC n option opt to value val.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Sets an option on the ASC n to a value. Allowed options are:
|
||||||
|
```
|
||||||
|
AVA+BTB -
|
||||||
|
opt: freq
|
||||||
|
val: 256 - 1024 (chip frequency)
|
||||||
|
BTB -
|
||||||
|
opt: millivolts
|
||||||
|
val: 1000 - 1400 (core voltage)
|
||||||
|
MBA -
|
||||||
|
opt: reset
|
||||||
|
val: 0 - # of chips (reset a chip)
|
||||||
|
|
||||||
|
opt: freq
|
||||||
|
val: 0 - # of chips, 100 - 1400 (chip frequency)
|
||||||
|
|
||||||
|
opt: ledcount
|
||||||
|
val: 0 - 100 (chip count for LED)
|
||||||
|
|
||||||
|
opt: ledlimit
|
||||||
|
val: 0 - 200 (LED off below GH/s)
|
||||||
|
|
||||||
|
opt: spidelay
|
||||||
|
val: 0 - 9999 (SPI per I/O delay)
|
||||||
|
|
||||||
|
opt: spireset
|
||||||
|
val: i or s, 0 - 9999 (SPI regular reset)
|
||||||
|
|
||||||
|
opt: spisleep
|
||||||
|
val: 0 - 9999 (SPI reset sleep in ms)
|
||||||
|
BMA -
|
||||||
|
opt: volt
|
||||||
|
val: 0 - 9
|
||||||
|
|
||||||
|
opt: clock
|
||||||
|
val: 0 - 15
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The ASC to set the options on.
|
||||||
|
opt: The option to set. Setting this to 'help' returns a help message.
|
||||||
|
val: The value to set the option to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of setting option opt to value val.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if val:
|
||||||
|
return await self.send_command("ascset", parameters=f"{n},{opt},{val}")
|
||||||
|
else:
|
||||||
|
return await self.send_command("ascset", parameters=f"{n},{opt}")
|
||||||
|
|
||||||
|
async def lcd(self) -> dict:
|
||||||
|
"""Get a general all-in-one status summary of the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An all-in-one status summary of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("lcd")
|
||||||
|
|
||||||
|
async def lockstats(self) -> dict:
|
||||||
|
"""Write lockstats to STDERR.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The result of writing the lock stats to STDERR.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("lockstats")
|
||||||
261
pyasic/API/bosminer.py
Normal file
261
pyasic/API/bosminer.py
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
from pyasic.API import BaseMinerAPI
|
||||||
|
|
||||||
|
|
||||||
|
class BOSMinerAPI(BaseMinerAPI):
|
||||||
|
"""An abstraction of the BOSMiner API.
|
||||||
|
|
||||||
|
Each method corresponds to an API command in BOSMiner.
|
||||||
|
|
||||||
|
[BOSMiner API documentation](https://docs.braiins.com/os/plus-en/Development/1_api.html)
|
||||||
|
|
||||||
|
This class abstracts use of the BOSMiner API, as well as the
|
||||||
|
methods for sending commands to it. The `self.send_command()`
|
||||||
|
function handles sending a command to the miner asynchronously, and
|
||||||
|
as such is the base for many of the functions in this class, which
|
||||||
|
rely on it to send the command for them.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ip: The IP of the miner to reference the API on.
|
||||||
|
port: The port to reference the API on. Default is 4028.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ip: str, port: int = 4028):
|
||||||
|
super().__init__(ip, port)
|
||||||
|
|
||||||
|
async def asccount(self) -> dict:
|
||||||
|
"""Get data on the number of ASC devices and their info.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on all ASC devices.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("asccount")
|
||||||
|
|
||||||
|
async def asc(self, n: int) -> dict:
|
||||||
|
"""Get data for ASC device n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device to get data for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The data for ASC device n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("asc", parameters=n)
|
||||||
|
|
||||||
|
async def devdetails(self) -> dict:
|
||||||
|
"""Get data on all devices with their static details.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on all devices with their static details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("devdetails")
|
||||||
|
|
||||||
|
async def devs(self) -> dict:
|
||||||
|
"""Get data on each PGA/ASC with their details.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on each PGA/ASC with their details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("devs")
|
||||||
|
|
||||||
|
async def edevs(self, old: bool = False) -> dict:
|
||||||
|
"""Get data on each PGA/ASC with their details, ignoring blacklisted and zombie devices.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old: Include zombie devices that became zombies less than 'old' seconds ago
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on each PGA/ASC with their details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if old:
|
||||||
|
return await self.send_command("edevs", parameters="old")
|
||||||
|
else:
|
||||||
|
return await self.send_command("edevs")
|
||||||
|
|
||||||
|
async def pools(self) -> dict:
|
||||||
|
"""Get pool information.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Miner pool information.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pools")
|
||||||
|
|
||||||
|
async def summary(self) -> dict:
|
||||||
|
"""Get the status summary of the miner.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The status summary of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("summary")
|
||||||
|
|
||||||
|
async def stats(self) -> dict:
|
||||||
|
"""Get stats of each device/pool with more than 1 getwork.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Stats of each device/pool with more than 1 getwork.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("stats")
|
||||||
|
|
||||||
|
async def version(self) -> dict:
|
||||||
|
"""Get miner version info.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Miner version information.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("version")
|
||||||
|
|
||||||
|
async def estats(self, old: bool = False) -> dict:
|
||||||
|
"""Get stats of each device/pool with more than 1 getwork, ignoring zombie devices.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old: Include zombie devices that became zombies less than 'old' seconds ago.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Stats of each device/pool with more than 1 getwork, ignoring zombie devices.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if old:
|
||||||
|
return await self.send_command("estats", parameters=old)
|
||||||
|
else:
|
||||||
|
return await self.send_command("estats")
|
||||||
|
|
||||||
|
async def check(self, command: str) -> dict:
|
||||||
|
"""Check if the command command exists in BOSMiner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
command: The command to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
## Information about a command:
|
||||||
|
* Exists (Y/N) <- the command exists in this version
|
||||||
|
* Access (Y/N) <- you have access to use the command
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("check", parameters=command)
|
||||||
|
|
||||||
|
async def coin(self) -> dict:
|
||||||
|
"""Get information on the current coin.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
## Information about the current coin being mined:
|
||||||
|
* Hash Method <- the hashing algorithm
|
||||||
|
* Current Block Time <- blocktime as a float, 0 means none
|
||||||
|
* Current Block Hash <- the hash of the current block, blank means none
|
||||||
|
* LP <- whether LP is in use on at least 1 pool
|
||||||
|
* Network Difficulty: the current network difficulty
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("coin")
|
||||||
|
|
||||||
|
async def lcd(self) -> dict:
|
||||||
|
"""Get a general all-in-one status summary of the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An all-in-one status summary of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("lcd")
|
||||||
|
|
||||||
|
async def fans(self) -> dict:
|
||||||
|
"""Get fan data.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on the fans of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("fans")
|
||||||
|
|
||||||
|
async def tempctrl(self) -> dict:
|
||||||
|
"""Get temperature control data.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data about the temp control settings of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("tempctrl")
|
||||||
|
|
||||||
|
async def temps(self) -> dict:
|
||||||
|
"""Get temperature data.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on the temps of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("temps")
|
||||||
|
|
||||||
|
async def tunerstatus(self) -> dict:
|
||||||
|
"""Get tuner status data
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on the status of autotuning.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("tunerstatus")
|
||||||
|
|
||||||
|
async def pause(self) -> dict:
|
||||||
|
"""Pause mining.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of pausing mining.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pause")
|
||||||
|
|
||||||
|
async def resume(self) -> dict:
|
||||||
|
"""Resume mining.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of resuming mining.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("resume")
|
||||||
850
pyasic/API/btminer.py
Normal file
850
pyasic/API/btminer.py
Normal file
@@ -0,0 +1,850 @@
|
|||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import binascii
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from passlib.handlers.md5_crypt import md5_crypt
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
||||||
|
from pyasic.API import BaseMinerAPI, APIError
|
||||||
|
from pyasic.settings import WHATSMINER_PWD
|
||||||
|
|
||||||
|
|
||||||
|
### IMPORTANT ###
|
||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# you change the password, you can pass that to the this class as pwd,
|
||||||
|
# or add it as the Whatsminer_pwd in the settings.toml file.
|
||||||
|
|
||||||
|
|
||||||
|
def _crypt(word: str, salt: str) -> str:
|
||||||
|
"""Encrypts a word with a salt, using a standard salt format.
|
||||||
|
|
||||||
|
Encrypts a word using a salt with the format
|
||||||
|
'\s*\$(\d+)\$([\w\./]*)\$'. If this format is not used, a
|
||||||
|
ValueError is raised.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
word: The word to be encrypted.
|
||||||
|
salt: The salt to encrypt the word.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An MD5 hash of the word with the salt.
|
||||||
|
"""
|
||||||
|
# compile a standard format for the salt
|
||||||
|
standard_salt = re.compile("\s*\$(\d+)\$([\w\./]*)\$")
|
||||||
|
# check if the salt matches
|
||||||
|
match = standard_salt.match(salt)
|
||||||
|
# if the matching fails, the salt is incorrect
|
||||||
|
if not match:
|
||||||
|
raise ValueError("Salt format is not correct.")
|
||||||
|
# save the matched salt in a new variable
|
||||||
|
new_salt = match.group(2)
|
||||||
|
# encrypt the word with the salt using md5
|
||||||
|
result = md5_crypt.hash(word, salt=new_salt)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _add_to_16(string: str) -> bytes:
|
||||||
|
"""Add null bytes to a string until the length is a multiple 16
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
string: The string to lengthen to a multiple of 16 and encode.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The input string as bytes with a multiple of 16 as the length.
|
||||||
|
"""
|
||||||
|
while len(string) % 16 != 0:
|
||||||
|
string += "\0"
|
||||||
|
return str.encode(string) # return bytes
|
||||||
|
|
||||||
|
|
||||||
|
def parse_btminer_priviledge_data(token_data: dict, data: dict):
|
||||||
|
"""Parses data returned from the BTMiner privileged API.
|
||||||
|
|
||||||
|
Parses data from the BTMiner privileged API using the the token
|
||||||
|
from the API in an AES format.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
token_data: The token information from self.get_token().
|
||||||
|
data: The data to parse, returned from the API.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A decoded dict version of the privileged command output.
|
||||||
|
"""
|
||||||
|
# get the encoded data from the dict
|
||||||
|
enc_data = data["enc"]
|
||||||
|
# get the aes key from the token data
|
||||||
|
aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest()
|
||||||
|
# unhexlify the aes key
|
||||||
|
aeskey = binascii.unhexlify(aeskey.encode())
|
||||||
|
# create the required decryptor
|
||||||
|
aes = Cipher(algorithms.AES(aeskey), modes.ECB())
|
||||||
|
decryptor = aes.decryptor()
|
||||||
|
# decode the message with the decryptor
|
||||||
|
ret_msg = json.loads(
|
||||||
|
decryptor.update(base64.decodebytes(bytes(enc_data, encoding="utf8")))
|
||||||
|
.rstrip(b"\0")
|
||||||
|
.decode("utf8")
|
||||||
|
)
|
||||||
|
return ret_msg
|
||||||
|
|
||||||
|
|
||||||
|
def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
|
||||||
|
"""Create a privileged command to send to the BTMiner API.
|
||||||
|
|
||||||
|
Creates a privileged command using the token from the API and the
|
||||||
|
command as a dict of {'command': cmd}, with cmd being any command
|
||||||
|
that the miner API accepts.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
token_data: The token information from self.get_token().
|
||||||
|
command: The command to turn into a privileged command.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The encrypted privileged command to be sent to the miner.
|
||||||
|
"""
|
||||||
|
# add token to command
|
||||||
|
command["token"] = token_data["host_sign"]
|
||||||
|
# encode host_passwd data and get hexdigest
|
||||||
|
aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest()
|
||||||
|
# unhexlify the encoded host_passwd
|
||||||
|
aeskey = binascii.unhexlify(aeskey.encode())
|
||||||
|
# create a new AES key
|
||||||
|
aes = Cipher(algorithms.AES(aeskey), modes.ECB())
|
||||||
|
encryptor = aes.encryptor()
|
||||||
|
# dump the command to json
|
||||||
|
api_json_str = json.dumps(command)
|
||||||
|
# encode the json command with the aes key
|
||||||
|
api_json_str_enc = (
|
||||||
|
base64.encodebytes(encryptor.update(_add_to_16(api_json_str)))
|
||||||
|
.decode("utf-8")
|
||||||
|
.replace("\n", "")
|
||||||
|
)
|
||||||
|
# label the data as being encoded
|
||||||
|
data_enc = {"enc": 1, "data": api_json_str_enc}
|
||||||
|
# dump the labeled data to json
|
||||||
|
api_packet_str = json.dumps(data_enc)
|
||||||
|
return api_packet_str.encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class BTMinerAPI(BaseMinerAPI):
|
||||||
|
"""An abstraction of the API for MicroBT Whatsminers, BTMiner.
|
||||||
|
|
||||||
|
Each method corresponds to an API command in BMMiner.
|
||||||
|
|
||||||
|
This class abstracts use of the BTMiner API, as well as the
|
||||||
|
methods for sending commands to it. The `self.send_command()`
|
||||||
|
function handles sending a command to the miner asynchronously, and
|
||||||
|
as such is the base for many of the functions in this class, which
|
||||||
|
rely on it to send the command for them.
|
||||||
|
|
||||||
|
All privileged commands for BTMiner's API require that you change
|
||||||
|
the password of the miners using the Whatsminer tool, and it can be
|
||||||
|
changed back to admin with this tool after. Set the new password
|
||||||
|
either by passing it to the __init__ method, or changing it in
|
||||||
|
settings.toml.
|
||||||
|
|
||||||
|
Additionally, the API commands for the privileged API must be
|
||||||
|
encoded using a token from the miner, all privileged commands do
|
||||||
|
this automatically for you and will decode the output to look like
|
||||||
|
a normal output from a miner API.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ip: The IP of the miner to reference the API on.
|
||||||
|
port: The port to reference the API on. Default is 4028.
|
||||||
|
pwd: The admin password of the miner. Default is admin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ip: str, port: int = 4028, pwd: str = WHATSMINER_PWD):
|
||||||
|
super().__init__(ip, port)
|
||||||
|
self.admin_pwd = pwd
|
||||||
|
self.current_token = None
|
||||||
|
|
||||||
|
async def send_command(
|
||||||
|
self,
|
||||||
|
command: str or bytes,
|
||||||
|
parameters: str or int or bool = None,
|
||||||
|
ignore_errors: bool = False,
|
||||||
|
**kwargs,
|
||||||
|
) -> dict:
|
||||||
|
# check if command is a string
|
||||||
|
# if its bytes its encoded and needs to be sent raw
|
||||||
|
if isinstance(command, str):
|
||||||
|
# if it is a string, put it into the standard command format
|
||||||
|
command = json.dumps({"command": command}).encode("utf-8")
|
||||||
|
try:
|
||||||
|
# get reader and writer streams
|
||||||
|
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
||||||
|
# handle OSError 121
|
||||||
|
except OSError as e:
|
||||||
|
if e.winerror == "121":
|
||||||
|
print("Semaphore Timeout has Expired.")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# send the command
|
||||||
|
writer.write(command)
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
# instantiate data
|
||||||
|
data = b""
|
||||||
|
|
||||||
|
# loop to receive all the data
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
d = await reader.read(4096)
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
data += d
|
||||||
|
except Exception as 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 it fails to validate, it is likely an error
|
||||||
|
validation = self.validate_command_output(data)
|
||||||
|
if not validation[0]:
|
||||||
|
raise APIError(validation[1])
|
||||||
|
|
||||||
|
# return the parsed json as a dict
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def get_token(self) -> dict:
|
||||||
|
"""Gets token information from the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An encoded token and md5 password, which are used for the privileged API.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
# get the token
|
||||||
|
data = await self.send_command("get_token")
|
||||||
|
|
||||||
|
# encrypt the admin password with the salt
|
||||||
|
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + "$")
|
||||||
|
pwd = pwd.split("$")
|
||||||
|
|
||||||
|
# take the 4th item from the pwd split
|
||||||
|
host_passwd_md5 = pwd[3]
|
||||||
|
|
||||||
|
# encrypt the pwd with the time and new salt
|
||||||
|
tmp = _crypt(pwd[3] + data["Msg"]["time"], "$1$" + data["Msg"]["newsalt"] + "$")
|
||||||
|
tmp = tmp.split("$")
|
||||||
|
|
||||||
|
# take the 4th item from the encrypted pwd split
|
||||||
|
host_sign = tmp[3]
|
||||||
|
|
||||||
|
# set the current token
|
||||||
|
self.current_token = {
|
||||||
|
"host_sign": host_sign,
|
||||||
|
"host_passwd_md5": host_passwd_md5,
|
||||||
|
}
|
||||||
|
return self.current_token
|
||||||
|
|
||||||
|
#### PRIVILEGED COMMANDS ####
|
||||||
|
# Please read the top of this file to learn
|
||||||
|
# how to configure the Whatsminer API to
|
||||||
|
# use these commands.
|
||||||
|
|
||||||
|
async def update_pools(
|
||||||
|
self,
|
||||||
|
pool_1: str,
|
||||||
|
worker_1: str,
|
||||||
|
passwd_1: str,
|
||||||
|
pool_2: str = None,
|
||||||
|
worker_2: str = None,
|
||||||
|
passwd_2: str = None,
|
||||||
|
pool_3: str = None,
|
||||||
|
worker_3: str = None,
|
||||||
|
passwd_3: str = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Update the pools of the miner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Update the pools of the miner using the API, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
pool_1: The URL to update pool 1 to.
|
||||||
|
worker_1: The worker name for pool 1 to update to.
|
||||||
|
passwd_1: The password for pool 1 to update to.
|
||||||
|
pool_2: The URL to update pool 2 to.
|
||||||
|
worker_2: The worker name for pool 2 to update to.
|
||||||
|
passwd_2: The password for pool 2 to update to.
|
||||||
|
pool_3: The URL to update pool 3 to.
|
||||||
|
worker_3: The worker name for pool 3 to update to.
|
||||||
|
passwd_3: The password for pool 3 to update to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict from the API to confirm the pools were updated.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
# get the token and password from the miner
|
||||||
|
token_data = await self.get_token()
|
||||||
|
|
||||||
|
# parse pool data
|
||||||
|
if not pool_1:
|
||||||
|
raise APIError("No pools set.")
|
||||||
|
elif pool_2 and pool_3:
|
||||||
|
command = {
|
||||||
|
"cmd": "update_pools",
|
||||||
|
"pool1": pool_1,
|
||||||
|
"worker1": worker_1,
|
||||||
|
"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:
|
||||||
|
"""Restart BTMiner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Restart BTMiner using the API, only works after changing
|
||||||
|
the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A reply informing of the restart.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "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:
|
||||||
|
"""Power off the miner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Power off the miner using the API, only works after changing
|
||||||
|
the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
respbefore: Whether to respond before powering off.
|
||||||
|
Returns:
|
||||||
|
A reply informing of the status of powering off.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if respbefore:
|
||||||
|
command = {"cmd": "power_off", "respbefore": "true"}
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
"""Power on the miner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Power on the miner using the API, only works after changing
|
||||||
|
the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of powering on.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "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:
|
||||||
|
"""Reset the LED on the miner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Reset the LED on the miner using the API, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of resetting the LED.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "set_led", "param": "auto"}
|
||||||
|
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(
|
||||||
|
self,
|
||||||
|
color: str = "red",
|
||||||
|
period: int = 2000,
|
||||||
|
duration: int = 1000,
|
||||||
|
start: int = 0,
|
||||||
|
) -> dict:
|
||||||
|
"""Set the LED on the miner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Set the LED on the miner using the API, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
color: The LED color to set, either 'red' or 'green'.
|
||||||
|
period: The flash cycle in ms.
|
||||||
|
duration: LED on time in the cycle in ms.
|
||||||
|
start: LED on time offset in the cycle in ms.
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of setting the LED.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {
|
||||||
|
"cmd": "set_led",
|
||||||
|
"color": color,
|
||||||
|
"period": period,
|
||||||
|
"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:
|
||||||
|
"""Set low power mode on the miner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Set low power mode on the miner using the API, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of setting low power mode.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "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
|
||||||
|
"""Not implemented."""
|
||||||
|
# to be determined if this will be added later
|
||||||
|
# requires a file stream in bytes
|
||||||
|
return NotImplementedError
|
||||||
|
|
||||||
|
async def reboot(self) -> dict:
|
||||||
|
"""Reboot the miner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of the reboot.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "reboot"}
|
||||||
|
token_data = await self.get_token()
|
||||||
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
|
async def factory_reset(self) -> dict:
|
||||||
|
"""Reset the miner to factory defaults.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of the reset.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "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:
|
||||||
|
"""Update the admin user's password.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Update the admin user's password, only works after changing the
|
||||||
|
password of the miner using the Whatsminer tool. New password
|
||||||
|
has a max length of 8 bytes, using letters, numbers, and
|
||||||
|
underscores.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old_pwd: The old admin password.
|
||||||
|
new_pwd: The new password to set.
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of setting the password.
|
||||||
|
"""
|
||||||
|
# check if password length is greater than 8 bytes
|
||||||
|
if len(new_pwd.encode("utf-8")) > 8:
|
||||||
|
raise APIError(
|
||||||
|
f"New password too long, the max length is 8. "
|
||||||
|
f"Password size: {len(new_pwd.encode('utf-8'))}"
|
||||||
|
)
|
||||||
|
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
|
||||||
|
token_data = await self.get_token()
|
||||||
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
|
async def set_target_freq(self, percent: int) -> dict:
|
||||||
|
"""Update the target frequency.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Update the target frequency, only works after changing the
|
||||||
|
password of the miner using the Whatsminer tool. The new
|
||||||
|
frequency must be between -10% and 100%.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
percent: The frequency % to set.
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of setting the frequency.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if not -10 < percent < 100:
|
||||||
|
raise APIError(
|
||||||
|
f"Frequency % is outside of the allowed "
|
||||||
|
f"range. Please set a % between -10 and "
|
||||||
|
f"100"
|
||||||
|
)
|
||||||
|
command = {"cmd": "set_target_freq", "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 enable_fast_boot(self) -> dict:
|
||||||
|
"""Turn on fast boot.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Turn on fast boot, only works after changing the password of
|
||||||
|
the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of enabling fast boot.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "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:
|
||||||
|
"""Turn off fast boot.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Turn off fast boot, only works after changing the password of
|
||||||
|
the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of disabling fast boot.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "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:
|
||||||
|
"""Turn on web pool updates.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Turn on web pool updates, only works after changing the
|
||||||
|
password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of enabling web pools.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "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:
|
||||||
|
"""Turn off web pool updates.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Turn off web pool updates, only works after changing the
|
||||||
|
password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of disabling web pools.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "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:
|
||||||
|
"""Set the hostname of the miner.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Set the hostname of the miner, only works after changing the
|
||||||
|
password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
hostname: The new hostname to use.
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of setting the hostname.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
command = {"cmd": "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:
|
||||||
|
"""Set the power percentage of the miner.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Set the power percentage of the miner, only works after changing
|
||||||
|
the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
percent: The power percentage to set.
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of setting the power percentage.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not 0 < percent < 100:
|
||||||
|
raise APIError(
|
||||||
|
f"Power PCT % is outside of the allowed "
|
||||||
|
f"range. Please set a % between 0 and "
|
||||||
|
f"100"
|
||||||
|
)
|
||||||
|
command = {"cmd": "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:
|
||||||
|
"""Configure or check status of pre power on.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Configure or check status of pre power on, only works after
|
||||||
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
complete: check whether pre power on is complete.
|
||||||
|
msg: ## the message to check.
|
||||||
|
* `wait for adjust temp`
|
||||||
|
* `adjust complete`
|
||||||
|
* `adjust continue`
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
A reply informing of the status of pre power on.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue":
|
||||||
|
raise APIError(
|
||||||
|
"Message is incorrect, please choose one of "
|
||||||
|
'["wait for adjust temp", '
|
||||||
|
'"adjust complete", '
|
||||||
|
'"adjust continue"]'
|
||||||
|
)
|
||||||
|
if complete:
|
||||||
|
complete = "true"
|
||||||
|
else:
|
||||||
|
complete = "false"
|
||||||
|
command = {"cmd": "pre_power_on", "complete": complete, "msg": msg}
|
||||||
|
token_data = await self.get_token()
|
||||||
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
|
#### END privileged COMMANDS ####
|
||||||
|
|
||||||
|
async def summary(self) -> dict:
|
||||||
|
"""Get the summary status from the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
Summary status of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("summary")
|
||||||
|
|
||||||
|
async def pools(self) -> dict:
|
||||||
|
"""Get the pool status from the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
Pool status of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pools")
|
||||||
|
|
||||||
|
async def devs(self) -> dict:
|
||||||
|
"""Get data on each PGA/ASC with their details.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
Data on each PGA/ASC with their details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("devs")
|
||||||
|
|
||||||
|
async def edevs(self) -> dict:
|
||||||
|
"""Get data on each PGA/ASC with their details, ignoring blacklisted and zombie devices.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
Data on each PGA/ASC with their details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("edevs")
|
||||||
|
|
||||||
|
async def devdetails(self) -> dict:
|
||||||
|
"""Get data on all devices with their static details.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
Data on all devices with their static details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("devdetails")
|
||||||
|
|
||||||
|
async def get_psu(self) -> dict:
|
||||||
|
"""Get data on the PSU and power information.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
Data on the PSU and power information.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("get_psu")
|
||||||
|
|
||||||
|
async def version(self) -> dict:
|
||||||
|
"""Get version data for the miner. Wraps `self.get_version()`.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Get version data for the miner. This calls another function,
|
||||||
|
self.get_version(), but is named version to stay consistent
|
||||||
|
with the other miner APIs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
Version data for the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.get_version()
|
||||||
|
|
||||||
|
async def get_version(self) -> dict:
|
||||||
|
"""Get version data for the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
Version data for the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("get_version")
|
||||||
|
|
||||||
|
async def status(self) -> dict:
|
||||||
|
"""Get BTMiner status and firmware version.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
BTMiner status and firmware version.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("status")
|
||||||
|
|
||||||
|
async def get_miner_info(self) -> dict:
|
||||||
|
"""Get general miner info.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
General miner info.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("get_miner_info")
|
||||||
680
pyasic/API/cgminer.py
Normal file
680
pyasic/API/cgminer.py
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
from pyasic.API import BaseMinerAPI
|
||||||
|
|
||||||
|
|
||||||
|
class CGMinerAPI(BaseMinerAPI):
|
||||||
|
"""An abstraction of the CGMiner API.
|
||||||
|
|
||||||
|
Each method corresponds to an API command in GGMiner.
|
||||||
|
|
||||||
|
[CGMiner API documentation](https://github.com/ckolivas/cgminer/blob/master/API-README)
|
||||||
|
|
||||||
|
This class abstracts use of the CGMiner API, as well as the
|
||||||
|
methods for sending commands to it. The self.send_command()
|
||||||
|
function handles sending a command to the miner asynchronously, and
|
||||||
|
as such is the base for many of the functions in this class, which
|
||||||
|
rely on it to send the command for them.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ip: The IP of the miner to reference the API on.
|
||||||
|
port: The port to reference the API on. Default is 4028.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ip: str, port: int = 4028):
|
||||||
|
super().__init__(ip, port)
|
||||||
|
|
||||||
|
async def version(self) -> dict:
|
||||||
|
"""Get miner version info.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Miner version information.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("version")
|
||||||
|
|
||||||
|
async def config(self) -> dict:
|
||||||
|
"""Get some basic configuration info.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
## Some miner configuration information:
|
||||||
|
* ASC Count <- the number of ASCs
|
||||||
|
* PGA Count <- the number of PGAs
|
||||||
|
* Pool Count <- the number of Pools
|
||||||
|
* Strategy <- the current pool strategy
|
||||||
|
* Log Interval <- the interval of logging
|
||||||
|
* Device Code <- list of compiled device drivers
|
||||||
|
* OS <- the current operating system
|
||||||
|
* Failover-Only <- failover-only setting
|
||||||
|
* Scan Time <- scan-time setting
|
||||||
|
* Queue <- queue setting
|
||||||
|
* Expiry <- expiry setting
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("config")
|
||||||
|
|
||||||
|
async def summary(self) -> dict:
|
||||||
|
"""Get the status summary of the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The status summary of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("summary")
|
||||||
|
|
||||||
|
async def pools(self) -> dict:
|
||||||
|
"""Get pool information.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Miner pool information.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pools")
|
||||||
|
|
||||||
|
async def devs(self) -> dict:
|
||||||
|
"""Get data on each PGA/ASC with their details.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on each PGA/ASC with their details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("devs")
|
||||||
|
|
||||||
|
async def edevs(self, old: bool = False) -> dict:
|
||||||
|
"""Get data on each PGA/ASC with their details, ignoring blacklisted and zombie devices.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old: Include zombie devices that became zombies less than 'old' seconds ago
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on each PGA/ASC with their details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if old:
|
||||||
|
return await self.send_command("edevs", parameters=old)
|
||||||
|
else:
|
||||||
|
return await self.send_command("edevs")
|
||||||
|
|
||||||
|
async def pga(self, n: int) -> dict:
|
||||||
|
"""Get data from PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA number to get data from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on the PGA n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pga", parameters=n)
|
||||||
|
|
||||||
|
async def pgacount(self) -> dict:
|
||||||
|
"""Get data fon all PGAs.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on the PGAs connected.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pgacount")
|
||||||
|
|
||||||
|
async def switchpool(self, n: int) -> dict:
|
||||||
|
"""Switch pools to pool n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The pool to switch to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of switching to pool n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("switchpool", parameters=n)
|
||||||
|
|
||||||
|
async def enablepool(self, n: int) -> dict:
|
||||||
|
"""Enable pool n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The pool to enable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of enabling pool n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("enablepool", parameters=n)
|
||||||
|
|
||||||
|
async def addpool(self, url: str, username: str, password: str) -> dict:
|
||||||
|
"""Add a pool to the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
url: The URL of the new pool to add.
|
||||||
|
username: The users username on the new pool.
|
||||||
|
password: The worker password on the new pool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of adding the pool.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command(
|
||||||
|
"addpool", parameters=f"{url},{username},{password}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def poolpriority(self, *n: int) -> dict:
|
||||||
|
"""Set pool priority.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
*n: Pools in order of priority.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of setting pool priority.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
pools = f"{','.join([str(item) for item in n])}"
|
||||||
|
return await self.send_command("poolpriority", parameters=pools)
|
||||||
|
|
||||||
|
async def poolquota(self, n: int, q: int) -> dict:
|
||||||
|
"""Set pool quota.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: Pool number to set quota on.
|
||||||
|
q: Quota to set the pool to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of setting pool quota.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("poolquota", parameters=f"{n},{q}")
|
||||||
|
|
||||||
|
async def disablepool(self, n: int) -> dict:
|
||||||
|
"""Disable a pool.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: Pool to disable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of diabling the pool.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("disablepool", parameters=n)
|
||||||
|
|
||||||
|
async def removepool(self, n: int) -> dict:
|
||||||
|
"""Remove a pool.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: Pool to remove.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of removing the pool.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("removepool", parameters=n)
|
||||||
|
|
||||||
|
async def save(self, filename: str = None) -> dict:
|
||||||
|
"""Save the config.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
filename: Filename to save the config as.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of saving the config.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if filename:
|
||||||
|
return await self.send_command("save", parameters=filename)
|
||||||
|
else:
|
||||||
|
return await self.send_command("save")
|
||||||
|
|
||||||
|
async def quit(self) -> dict:
|
||||||
|
"""Quit CGMiner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A single "BYE" before CGMiner quits.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("quit")
|
||||||
|
|
||||||
|
async def notify(self) -> dict:
|
||||||
|
"""Notify the user of past errors.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The last status and count of each devices problem(s).
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("notify")
|
||||||
|
|
||||||
|
async def privileged(self) -> dict:
|
||||||
|
"""Check if you have privileged access.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The STATUS section with an error if you have no privileged access, or success if you have privileged access.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("privileged")
|
||||||
|
|
||||||
|
async def pgaenable(self, n: int) -> dict:
|
||||||
|
"""Enable PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA to enable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of enabling PGA n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pgaenable", parameters=n)
|
||||||
|
|
||||||
|
async def pgadisable(self, n: int) -> dict:
|
||||||
|
"""Disable PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA to disable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of disabling PGA n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pgadisable", parameters=n)
|
||||||
|
|
||||||
|
async def pgaidentify(self, n: int) -> dict:
|
||||||
|
"""Identify PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA to identify.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation of identifying PGA n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("pgaidentify", parameters=n)
|
||||||
|
|
||||||
|
async def devdetails(self) -> dict:
|
||||||
|
"""Get data on all devices with their static details.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on all devices with their static details.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("devdetails")
|
||||||
|
|
||||||
|
async def restart(self) -> dict:
|
||||||
|
"""Restart CGMiner using the API.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A reply informing of the restart.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("restart")
|
||||||
|
|
||||||
|
async def stats(self) -> dict:
|
||||||
|
"""Get stats of each device/pool with more than 1 getwork.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Stats of each device/pool with more than 1 getwork.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("stats")
|
||||||
|
|
||||||
|
async def estats(self, old: bool = False) -> dict:
|
||||||
|
"""Get stats of each device/pool with more than 1 getwork, ignoring zombie devices.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old: Include zombie devices that became zombies less than 'old' seconds ago.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Stats of each device/pool with more than 1 getwork, ignoring zombie devices.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if old:
|
||||||
|
return await self.send_command("estats", parameters=old)
|
||||||
|
else:
|
||||||
|
return await self.send_command("estats")
|
||||||
|
|
||||||
|
async def check(self, command: str) -> dict:
|
||||||
|
"""Check if the command command exists in CGMiner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
command: The command to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
## Information about a command:
|
||||||
|
* Exists (Y/N) <- the command exists in this version
|
||||||
|
* Access (Y/N) <- you have access to use the command
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("check", parameters=command)
|
||||||
|
|
||||||
|
async def failover_only(self, failover: bool) -> dict:
|
||||||
|
"""Set failover-only.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
failover: What to set failover-only to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of setting failover-only.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("failover-only", parameters=failover)
|
||||||
|
|
||||||
|
async def coin(self) -> dict:
|
||||||
|
"""Get information on the current coin.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
## Information about the current coin being mined:
|
||||||
|
* Hash Method <- the hashing algorithm
|
||||||
|
* Current Block Time <- blocktime as a float, 0 means none
|
||||||
|
* Current Block Hash <- the hash of the current block, blank means none
|
||||||
|
* LP <- whether LP is in use on at least 1 pool
|
||||||
|
* Network Difficulty: the current network difficulty
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("coin")
|
||||||
|
|
||||||
|
async def debug(self, setting: str) -> dict:
|
||||||
|
"""Set a debug setting.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
setting: Which setting to switch to.
|
||||||
|
## Options are:
|
||||||
|
* Silent
|
||||||
|
* Quiet
|
||||||
|
* Verbose
|
||||||
|
* Debug
|
||||||
|
* RPCProto
|
||||||
|
* PerDevice
|
||||||
|
* WorkTime
|
||||||
|
* Normal
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on which debug setting was enabled or disabled.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("debug", parameters=setting)
|
||||||
|
|
||||||
|
async def setconfig(self, name: str, n: int) -> dict:
|
||||||
|
"""Set config of name to value n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name: The name of the config setting to set.
|
||||||
|
## Options are:
|
||||||
|
* queue
|
||||||
|
* scantime
|
||||||
|
* expiry
|
||||||
|
n: The value to set the 'name' setting to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The results of setting config of name to n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("setconfig", parameters=f"{name},{n}")
|
||||||
|
|
||||||
|
async def usbstats(self) -> dict:
|
||||||
|
"""Get stats of all USB devices except ztex.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The stats of all USB devices except ztex.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("usbstats")
|
||||||
|
|
||||||
|
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
|
||||||
|
"""Set PGA option opt to val on PGA n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
```
|
||||||
|
MMQ -
|
||||||
|
opt: clock
|
||||||
|
val: 160 - 230 (multiple of 2)
|
||||||
|
CMR -
|
||||||
|
opt: clock
|
||||||
|
val: 100 - 220
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The PGA to set the options on.
|
||||||
|
opt: The option to set. Setting this to 'help' returns a help message.
|
||||||
|
val: The value to set the option to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of setting PGA n with opt[,val].
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if val:
|
||||||
|
return await self.send_command("pgaset", parameters=f"{n},{opt},{val}")
|
||||||
|
else:
|
||||||
|
return await self.send_command("pgaset", parameters=f"{n},{opt}")
|
||||||
|
|
||||||
|
async def zero(self, which: str, summary: bool) -> dict:
|
||||||
|
"""Zero a device.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
which: Which device to zero. Setting this to 'all' zeros all devices. Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
|
||||||
|
summary: Whether or not to show a full summary.
|
||||||
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the STATUS section with info on the zero and optional summary.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("zero", parameters=f"{which},{summary}")
|
||||||
|
|
||||||
|
async def hotplug(self, n: int) -> dict:
|
||||||
|
"""Enable hotplug.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device number to set hotplug on.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Information on hotplug status.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("hotplug", parameters=n)
|
||||||
|
|
||||||
|
async def asc(self, n: int) -> dict:
|
||||||
|
"""Get data for ASC device n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device to get data for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The data for ASC device n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("asc", parameters=n)
|
||||||
|
|
||||||
|
async def ascenable(self, n: int) -> dict:
|
||||||
|
"""Enable ASC device n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device to enable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of enabling ASC device n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("ascenable", parameters=n)
|
||||||
|
|
||||||
|
async def ascdisable(self, n: int) -> dict:
|
||||||
|
"""Disable ASC device n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device to disable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of disabling ASC device n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("ascdisable", parameters=n)
|
||||||
|
|
||||||
|
async def ascidentify(self, n: int) -> dict:
|
||||||
|
"""Identify ASC device n.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The device to identify.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of identifying ASC device n.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("ascidentify", parameters=n)
|
||||||
|
|
||||||
|
async def asccount(self) -> dict:
|
||||||
|
"""Get data on the number of ASC devices and their info.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data on all ASC devices.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("asccount")
|
||||||
|
|
||||||
|
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
|
||||||
|
"""Set ASC n option opt to value val.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Sets an option on the ASC n to a value. Allowed options are:
|
||||||
|
```
|
||||||
|
AVA+BTB -
|
||||||
|
opt: freq
|
||||||
|
val: 256 - 1024 (chip frequency)
|
||||||
|
BTB -
|
||||||
|
opt: millivolts
|
||||||
|
val: 1000 - 1400 (core voltage)
|
||||||
|
MBA -
|
||||||
|
opt: reset
|
||||||
|
val: 0 - # of chips (reset a chip)
|
||||||
|
|
||||||
|
opt: freq
|
||||||
|
val: 0 - # of chips, 100 - 1400 (chip frequency)
|
||||||
|
|
||||||
|
opt: ledcount
|
||||||
|
val: 0 - 100 (chip count for LED)
|
||||||
|
|
||||||
|
opt: ledlimit
|
||||||
|
val: 0 - 200 (LED off below GH/s)
|
||||||
|
|
||||||
|
opt: spidelay
|
||||||
|
val: 0 - 9999 (SPI per I/O delay)
|
||||||
|
|
||||||
|
opt: spireset
|
||||||
|
val: i or s, 0 - 9999 (SPI regular reset)
|
||||||
|
|
||||||
|
opt: spisleep
|
||||||
|
val: 0 - 9999 (SPI reset sleep in ms)
|
||||||
|
BMA -
|
||||||
|
opt: volt
|
||||||
|
val: 0 - 9
|
||||||
|
|
||||||
|
opt: clock
|
||||||
|
val: 0 - 15
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: The ASC to set the options on.
|
||||||
|
opt: The option to set. Setting this to 'help' returns a help message.
|
||||||
|
val: The value to set the option to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation of setting option opt to value val.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
if val:
|
||||||
|
return await self.send_command("ascset", parameters=f"{n},{opt},{val}")
|
||||||
|
else:
|
||||||
|
return await self.send_command("ascset", parameters=f"{n},{opt}")
|
||||||
|
|
||||||
|
async def lcd(self) -> dict:
|
||||||
|
"""Get a general all-in-one status summary of the miner.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An all-in-one status summary of the miner.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("lcd")
|
||||||
|
|
||||||
|
async def lockstats(self) -> dict:
|
||||||
|
"""Write lockstats to STDERR.
|
||||||
|
<details>
|
||||||
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The result of writing the lock stats to STDERR.
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
return await self.send_command("lockstats")
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
from 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.
|
||||||
|
|
||||||
|
This class is designed to try to be a intersection of as many miner APIs
|
||||||
|
and API commands as possible (API ⋂ API), to ensure that it can be used
|
||||||
|
with as many APIs as possible.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, ip, port=4028):
|
def __init__(self, ip, port=4028):
|
||||||
super().__init__(ip, port)
|
super().__init__(ip, port)
|
||||||
|
|
||||||
@@ -65,7 +72,7 @@ class UnknownAPI(BaseMinerAPI):
|
|||||||
async def addpool(self, url: str, username: str, password: str) -> dict:
|
async def addpool(self, url: str, username: str, password: str) -> dict:
|
||||||
# BOS has not implemented this yet, they will in the future
|
# BOS has not implemented this yet, they will in the future
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
# return await self.send_command("addpool", parameters=f"{url}, {username}, {password}")
|
# return await self.send_command("addpool", parameters=f"{url},{username},{password}")
|
||||||
|
|
||||||
async def removepool(self, n: int) -> dict:
|
async def removepool(self, n: int) -> dict:
|
||||||
# BOS has not implemented this yet, they will in the future
|
# BOS has not implemented this yet, they will in the future
|
||||||
366
pyasic/config/__init__.py
Normal file
366
pyasic/config/__init__.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import List, Literal
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
import toml
|
||||||
|
import yaml
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _Pool:
|
||||||
|
"""A dataclass for pool information.
|
||||||
|
|
||||||
|
:param url: URL of the pool.
|
||||||
|
:param username: Username on the pool.
|
||||||
|
:param password: Worker password on the pool.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url: str = ""
|
||||||
|
username: str = ""
|
||||||
|
password: str = ""
|
||||||
|
|
||||||
|
def from_dict(self, data: dict):
|
||||||
|
"""Convert raw pool data as a dict to usable data and save it to this class.
|
||||||
|
|
||||||
|
:param data: The raw config data to convert.
|
||||||
|
"""
|
||||||
|
for key in data.keys():
|
||||||
|
if key == "url":
|
||||||
|
self.url = data[key]
|
||||||
|
if key in ["user", "username"]:
|
||||||
|
self.username = data[key]
|
||||||
|
if key in ["pass", "password"]:
|
||||||
|
self.password = data[key]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def as_x19(self, user_suffix: str = None):
|
||||||
|
"""Convert the data in this class to a dict usable by an X19 device.
|
||||||
|
|
||||||
|
:param 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_avalon(self, user_suffix: str = None):
|
||||||
|
username = self.username
|
||||||
|
if user_suffix:
|
||||||
|
username = f"{username}{user_suffix}"
|
||||||
|
|
||||||
|
pool = ",".join([self.url, username, self.password])
|
||||||
|
return pool
|
||||||
|
|
||||||
|
def as_bos(self, user_suffix: str = None):
|
||||||
|
"""Convert the data in this class to a dict usable by an BOSMiner device.
|
||||||
|
|
||||||
|
:param 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, "password": self.password}
|
||||||
|
return pool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _PoolGroup:
|
||||||
|
"""A dataclass for pool group information.
|
||||||
|
|
||||||
|
:param quota: The group quota.
|
||||||
|
:param group_name: The name of the pool group.
|
||||||
|
:param pools: A list of pools in this group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
quota: int = 1
|
||||||
|
group_name: str = None
|
||||||
|
pools: List[_Pool] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if not self.group_name:
|
||||||
|
self.group_name = "".join(
|
||||||
|
random.choice(string.ascii_uppercase + string.digits) for _ in range(6)
|
||||||
|
) # generate random pool group name in case it isn't set
|
||||||
|
|
||||||
|
def from_dict(self, data: dict):
|
||||||
|
"""Convert raw pool group data as a dict to usable data and save it to this class.
|
||||||
|
|
||||||
|
:param data: The raw config data to convert.
|
||||||
|
"""
|
||||||
|
pools = []
|
||||||
|
for key in data.keys():
|
||||||
|
if key in ["name", "group_name"]:
|
||||||
|
self.group_name = data[key]
|
||||||
|
if key == "quota":
|
||||||
|
self.quota = data[key]
|
||||||
|
if key in ["pools", "pool"]:
|
||||||
|
for pool in data[key]:
|
||||||
|
pools.append(_Pool().from_dict(pool))
|
||||||
|
self.pools = pools
|
||||||
|
return self
|
||||||
|
|
||||||
|
def as_x19(self, user_suffix: str = None):
|
||||||
|
"""Convert the data in this class to a dict usable by an X19 device.
|
||||||
|
|
||||||
|
:param user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
pools = []
|
||||||
|
for pool in self.pools[:3]:
|
||||||
|
pools.append(pool.as_x19(user_suffix=user_suffix))
|
||||||
|
return pools
|
||||||
|
|
||||||
|
def as_avalon(self, user_suffix: str = None):
|
||||||
|
pool = self.pools[0].as_avalon(user_suffix=user_suffix)
|
||||||
|
return pool
|
||||||
|
|
||||||
|
def as_bos(self, user_suffix: str = None):
|
||||||
|
"""Convert the data in this class to a dict usable by an BOSMiner device.
|
||||||
|
|
||||||
|
:param user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
group = {
|
||||||
|
"name": self.group_name,
|
||||||
|
"quota": self.quota,
|
||||||
|
"pool": [pool.as_bos(user_suffix=user_suffix) for pool in self.pools],
|
||||||
|
}
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MinerConfig:
|
||||||
|
"""A dataclass for miner configuration information.
|
||||||
|
|
||||||
|
:param pool_groups: A list of pool groups in this config.
|
||||||
|
:param temp_mode: The temperature control mode.
|
||||||
|
:param temp_target: The target temp.
|
||||||
|
:param temp_hot: The hot temp (100% fans).
|
||||||
|
:param temp_dangerous: The dangerous temp (shutdown).
|
||||||
|
:param minimum_fans: The minimum numbers of fans to run the miner.
|
||||||
|
:param fan_speed: Manual fan speed to run the fan at (only if temp_mode == "manual").
|
||||||
|
:param asicboost: Whether or not to enable asicboost.
|
||||||
|
:param autotuning_enabled: Whether or not to enable autotuning.
|
||||||
|
:param autotuning_wattage: The wattage to use when autotuning.
|
||||||
|
:param dps_enabled: Whether or not to enable dynamic power scaling.
|
||||||
|
:param dps_power_step: The amount of power to reduce autotuning by when the miner reaches dangerous temp.
|
||||||
|
:param dps_min_power: The minimum power to reduce autotuning to.
|
||||||
|
:param dps_shutdown_enabled: Whether or not to shutdown the miner when `dps_min_power` is reached.
|
||||||
|
:param dps_shutdown_duration: The amount of time to shutdown for (in hours).
|
||||||
|
"""
|
||||||
|
|
||||||
|
pool_groups: List[_PoolGroup] = None
|
||||||
|
|
||||||
|
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
|
||||||
|
temp_target: float = 70.0
|
||||||
|
temp_hot: float = 80.0
|
||||||
|
temp_dangerous: float = 10.0
|
||||||
|
|
||||||
|
minimum_fans: int = None
|
||||||
|
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
|
||||||
|
|
||||||
|
asicboost: bool = None
|
||||||
|
|
||||||
|
autotuning_enabled: bool = True
|
||||||
|
autotuning_wattage: int = 900
|
||||||
|
|
||||||
|
dps_enabled: bool = None
|
||||||
|
dps_power_step: int = None
|
||||||
|
dps_min_power: int = None
|
||||||
|
dps_shutdown_enabled: bool = None
|
||||||
|
dps_shutdown_duration: float = None
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
"""Convert the data in this class to a dict."""
|
||||||
|
|
||||||
|
data_dict = asdict(self)
|
||||||
|
for key in asdict(self).keys():
|
||||||
|
if data_dict[key] is None:
|
||||||
|
del data_dict[key]
|
||||||
|
return data_dict
|
||||||
|
|
||||||
|
def as_toml(self):
|
||||||
|
"""Convert the data in this class to toml."""
|
||||||
|
return toml.dumps(self.as_dict())
|
||||||
|
|
||||||
|
def as_yaml(self):
|
||||||
|
"""Convert the data in this class to yaml."""
|
||||||
|
return yaml.dump(self.as_dict(), sort_keys=False)
|
||||||
|
|
||||||
|
def from_raw(self, data: dict):
|
||||||
|
"""Convert raw config data as a dict to usable data and save it to this class.
|
||||||
|
|
||||||
|
:param data: The raw config data to convert.
|
||||||
|
"""
|
||||||
|
pool_groups = []
|
||||||
|
for key in data.keys():
|
||||||
|
if key == "pools":
|
||||||
|
pool_groups.append(_PoolGroup().from_dict({"pools": data[key]}))
|
||||||
|
elif key == "group":
|
||||||
|
for group in data[key]:
|
||||||
|
pool_groups.append(_PoolGroup().from_dict(group))
|
||||||
|
|
||||||
|
if key == "bitmain-fan-ctrl":
|
||||||
|
if data[key]:
|
||||||
|
self.temp_mode = "manual"
|
||||||
|
if data.get("bitmain-fan-pwm"):
|
||||||
|
self.fan_speed = int(data["bitmain-fan-pwm"])
|
||||||
|
elif key == "fan_control":
|
||||||
|
for _key in data[key].keys():
|
||||||
|
if _key == "min_fans":
|
||||||
|
self.minimum_fans = data[key][_key]
|
||||||
|
elif _key == "speed":
|
||||||
|
self.fan_speed = data[key][_key]
|
||||||
|
elif key == "temp_control":
|
||||||
|
for _key in data[key].keys():
|
||||||
|
if _key == "mode":
|
||||||
|
self.temp_mode = data[key][_key]
|
||||||
|
elif _key == "target_temp":
|
||||||
|
self.temp_target = data[key][_key]
|
||||||
|
elif _key == "hot_temp":
|
||||||
|
self.temp_hot = data[key][_key]
|
||||||
|
elif _key == "dangerous_temp":
|
||||||
|
self.temp_dangerous = data[key][_key]
|
||||||
|
|
||||||
|
if key == "hash_chain_global":
|
||||||
|
if data[key].get("asic_boost"):
|
||||||
|
self.asicboost = data[key]["asic_boost"]
|
||||||
|
|
||||||
|
if key == "autotuning":
|
||||||
|
for _key in data[key].keys():
|
||||||
|
if _key == "enabled":
|
||||||
|
self.autotuning_enabled = data[key][_key]
|
||||||
|
elif _key == "psu_power_limit":
|
||||||
|
self.autotuning_wattage = data[key][_key]
|
||||||
|
|
||||||
|
if key == "power_scaling":
|
||||||
|
for _key in data[key].keys():
|
||||||
|
if _key == "enabled":
|
||||||
|
self.dps_enabled = data[key][_key]
|
||||||
|
elif _key == "power_step":
|
||||||
|
self.dps_power_step = data[key][_key]
|
||||||
|
elif _key == "min_psu_power_limit":
|
||||||
|
self.dps_min_power = data[key][_key]
|
||||||
|
elif _key == "shutdown_enabled":
|
||||||
|
self.dps_shutdown_enabled = data[key][_key]
|
||||||
|
elif _key == "shutdown_duration":
|
||||||
|
self.dps_shutdown_duration = data[key][_key]
|
||||||
|
|
||||||
|
self.pool_groups = pool_groups
|
||||||
|
return self
|
||||||
|
|
||||||
|
def from_dict(self, data: dict):
|
||||||
|
"""Convert an output dict of this class back into usable data and save it to this class.
|
||||||
|
|
||||||
|
:param data: The raw config data to convert.
|
||||||
|
"""
|
||||||
|
pool_groups = []
|
||||||
|
for group in data["pool_groups"]:
|
||||||
|
pool_groups.append(_PoolGroup().from_dict(group))
|
||||||
|
for key in data.keys():
|
||||||
|
if getattr(self, key) and not key == "pool_groups":
|
||||||
|
setattr(self, key, data[key])
|
||||||
|
self.pool_groups = pool_groups
|
||||||
|
return self
|
||||||
|
|
||||||
|
def from_toml(self, data: str):
|
||||||
|
"""Convert output toml of this class back into usable data and save it to this class.
|
||||||
|
|
||||||
|
:param data: The raw config data to convert.
|
||||||
|
"""
|
||||||
|
return self.from_dict(toml.loads(data))
|
||||||
|
|
||||||
|
def from_yaml(self, data: str):
|
||||||
|
"""Convert output yaml of this class back into usable data and save it to this class.
|
||||||
|
|
||||||
|
:param data: The raw config data to convert.
|
||||||
|
"""
|
||||||
|
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
|
||||||
|
|
||||||
|
def as_x19(self, user_suffix: str = None) -> str:
|
||||||
|
"""Convert the data in this class to a config usable by an X19 device.
|
||||||
|
|
||||||
|
:param user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
cfg = {
|
||||||
|
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
|
||||||
|
"bitmain-fan-ctrl": False,
|
||||||
|
"bitmain-fan-pwn": 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not self.temp_mode == "auto":
|
||||||
|
cfg["bitmain-fan-ctrl"] = True
|
||||||
|
|
||||||
|
if self.fan_speed:
|
||||||
|
cfg["bitmain-fan-ctrl"] = str(self.fan_speed)
|
||||||
|
|
||||||
|
return json.dumps(cfg)
|
||||||
|
|
||||||
|
def as_avalon(self, user_suffix: str = None) -> str:
|
||||||
|
cfg = self.pool_groups[0].as_avalon()
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
def as_bos(self, model: str = "S9", user_suffix: str = None) -> str:
|
||||||
|
"""Convert the data in this class to a config usable by an BOSMiner device.
|
||||||
|
|
||||||
|
:param model: The model of the miner to be used in the format portion of the config.
|
||||||
|
:param user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
cfg = {
|
||||||
|
"format": {
|
||||||
|
"version": "1.2+",
|
||||||
|
"model": f"Antminer {model}",
|
||||||
|
"generator": "Upstream Config Utility",
|
||||||
|
"timestamp": int(time.time()),
|
||||||
|
},
|
||||||
|
"group": [
|
||||||
|
group.as_bos(user_suffix=user_suffix) for group in self.pool_groups
|
||||||
|
],
|
||||||
|
"temp_control": {
|
||||||
|
"mode": self.temp_mode,
|
||||||
|
"target_temp": self.temp_target,
|
||||||
|
"hot_temp": self.temp_hot,
|
||||||
|
"dangerous_temp": self.temp_dangerous,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.autotuning_enabled or self.autotuning_wattage:
|
||||||
|
cfg["autotuning"] = {}
|
||||||
|
if self.autotuning_enabled:
|
||||||
|
cfg["autotuning"]["enabled"] = self.autotuning_enabled
|
||||||
|
if self.autotuning_wattage:
|
||||||
|
cfg["autotuning"]["psu_power_limit"] = self.autotuning_wattage
|
||||||
|
|
||||||
|
if self.asicboost:
|
||||||
|
cfg["hash_chain_global"] = {}
|
||||||
|
cfg["hash_chain_global"]["asic_boost"] = self.asicboost
|
||||||
|
|
||||||
|
if any(
|
||||||
|
[
|
||||||
|
getattr(self, item)
|
||||||
|
for item in [
|
||||||
|
"dps_enabled",
|
||||||
|
"dps_power_step",
|
||||||
|
"dps_min_power",
|
||||||
|
"dps_shutdown_enabled",
|
||||||
|
"dps_shutdown_duration",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
):
|
||||||
|
cfg["power_scaling"] = {}
|
||||||
|
if self.dps_enabled:
|
||||||
|
cfg["power_scaling"]["enabled"] = self.dps_enabled
|
||||||
|
if self.dps_power_step:
|
||||||
|
cfg["power_scaling"]["power_step"] = self.dps_power_step
|
||||||
|
if self.dps_min_power:
|
||||||
|
cfg["power_scaling"]["min_psu_power_limit"] = self.dps_min_power
|
||||||
|
if self.dps_shutdown_enabled:
|
||||||
|
cfg["power_scaling"]["shutdown_enabled"] = self.dps_shutdown_enabled
|
||||||
|
if self.dps_shutdown_duration:
|
||||||
|
cfg["power_scaling"]["shutdown_duration"] = self.dps_shutdown_duration
|
||||||
|
|
||||||
|
return toml.dumps(cfg)
|
||||||
132
pyasic/data/__init__.py
Normal file
132
pyasic/data/__init__.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MinerData:
|
||||||
|
"""A Dataclass to standardize data returned from miners (specifically `AnyMiner().get_data()`)
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ip: The IP of the miner as a str.
|
||||||
|
datetime: The time and date this data was generated.
|
||||||
|
model: The model 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.
|
||||||
|
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.
|
||||||
|
right_board_hashrate: The hashrate of the right board of the miner in TH/s as a float.
|
||||||
|
temperature_avg: The average temperature across the boards. Calculated automatically.
|
||||||
|
env_temp: The environment temps as a float.
|
||||||
|
left_board_temp: The temp of the left PCB as an int.
|
||||||
|
left_board_chip_temp: The temp of the left board chips as an int.
|
||||||
|
center_board_temp: The temp of the center PCB as an int.
|
||||||
|
center_board_chip_temp: The temp of the center board chips as an int.
|
||||||
|
right_board_temp: The temp of the right PCB as an int.
|
||||||
|
right_board_chip_temp: The temp of the right board chips as an int.
|
||||||
|
wattage: Current power draw of the miner as an int.
|
||||||
|
wattage_limit: Power limit of the miner as an int.
|
||||||
|
fan_1: The speed of the first 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_4: The speed of the fourth fan 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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
nominal: The nominal amount of chips in the miner. Calculated automatically.
|
||||||
|
pool_split: The pool split 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_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.
|
||||||
|
errors: A list of errors on the miner.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ip: str
|
||||||
|
datetime: datetime = None
|
||||||
|
mac: str = "00:00:00:00:00:00"
|
||||||
|
model: str = "Unknown"
|
||||||
|
hostname: str = "Unknown"
|
||||||
|
hashrate: float = 0
|
||||||
|
left_board_hashrate: float = 0
|
||||||
|
center_board_hashrate: float = 0
|
||||||
|
right_board_hashrate: float = 0
|
||||||
|
temperature_avg: int = field(init=False)
|
||||||
|
env_temp: float = 0
|
||||||
|
left_board_temp: int = 0
|
||||||
|
left_board_chip_temp: int = 0
|
||||||
|
center_board_temp: int = 0
|
||||||
|
center_board_chip_temp: int = 0
|
||||||
|
right_board_temp: int = 0
|
||||||
|
right_board_chip_temp: int = 0
|
||||||
|
wattage: int = 0
|
||||||
|
wattage_limit: int = 0
|
||||||
|
fan_1: int = -1
|
||||||
|
fan_2: int = -1
|
||||||
|
fan_3: int = -1
|
||||||
|
fan_4: int = -1
|
||||||
|
left_chips: int = 0
|
||||||
|
center_chips: int = 0
|
||||||
|
right_chips: int = 0
|
||||||
|
total_chips: int = field(init=False)
|
||||||
|
ideal_chips: int = 1
|
||||||
|
percent_ideal: float = field(init=False)
|
||||||
|
nominal: int = field(init=False)
|
||||||
|
pool_split: str = "0"
|
||||||
|
pool_1_url: str = "Unknown"
|
||||||
|
pool_1_user: str = "Unknown"
|
||||||
|
pool_2_url: str = ""
|
||||||
|
pool_2_user: str = ""
|
||||||
|
errors: list = field(default_factory=list)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.datetime = datetime.now()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_chips(self): # noqa - Skip PyCharm inspection
|
||||||
|
return self.right_chips + self.center_chips + self.left_chips
|
||||||
|
|
||||||
|
@total_chips.setter
|
||||||
|
def total_chips(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nominal(self): # noqa - Skip PyCharm inspection
|
||||||
|
return self.ideal_chips == self.total_chips
|
||||||
|
|
||||||
|
@nominal.setter
|
||||||
|
def nominal(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percent_ideal(self): # noqa - Skip PyCharm inspection
|
||||||
|
return round((self.total_chips / self.ideal_chips) * 100)
|
||||||
|
|
||||||
|
@percent_ideal.setter
|
||||||
|
def percent_ideal(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature_avg(self): # noqa - Skip PyCharm inspection
|
||||||
|
total_temp = 0
|
||||||
|
temp_count = 0
|
||||||
|
for temp in [
|
||||||
|
self.left_board_chip_temp,
|
||||||
|
self.center_board_chip_temp,
|
||||||
|
self.right_board_chip_temp,
|
||||||
|
]:
|
||||||
|
if temp and not temp == 0:
|
||||||
|
total_temp += temp
|
||||||
|
temp_count += 1
|
||||||
|
if not temp_count > 0:
|
||||||
|
return 0
|
||||||
|
return round(total_temp / temp_count)
|
||||||
|
|
||||||
|
@temperature_avg.setter
|
||||||
|
def temperature_avg(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
return asdict(self)
|
||||||
2
pyasic/data/error_codes/__init__.py
Normal file
2
pyasic/data/error_codes/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .whatsminer import WhatsminerError
|
||||||
|
from .bos import BraiinsOSError
|
||||||
11
pyasic/data/error_codes/bos.py
Normal file
11
pyasic/data/error_codes/bos.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BraiinsOSError:
|
||||||
|
"""A Dataclass to handle error codes of BraiinsOS+ miners."""
|
||||||
|
|
||||||
|
error_message: str
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
return asdict(self)
|
||||||
152
pyasic/data/error_codes/whatsminer.py
Normal file
152
pyasic/data/error_codes/whatsminer.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WhatsminerError:
|
||||||
|
"""A Dataclass to handle error codes of Whatsminers."""
|
||||||
|
|
||||||
|
error_code: int
|
||||||
|
error_message: str = field(init=False)
|
||||||
|
|
||||||
|
@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 = {
|
||||||
|
110: "Intake fan speed error.",
|
||||||
|
111: "Exhaust fan speed error.",
|
||||||
|
120: "Intake fan speed error. Fan speed deviates by more than 2000.",
|
||||||
|
121: "Exhaust fan speed error. Fan speed deviates by more than 2000.",
|
||||||
|
130: "Intake fan speed error. Fan speed deviates by more than 3000.",
|
||||||
|
131: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
|
||||||
|
140: "Fan speed too high.",
|
||||||
|
200: "Power probing error. No power found.",
|
||||||
|
201: "Power supply and configuration file don't match.",
|
||||||
|
202: "Power output voltage error.",
|
||||||
|
203: "Power protecting due to high environment temperature.",
|
||||||
|
204: "Power current protecting due to high environment temperature.",
|
||||||
|
205: "Power current error.",
|
||||||
|
206: "Power input low voltage error.",
|
||||||
|
207: "Power input current protecting due to bad power input.",
|
||||||
|
210: "Power error.",
|
||||||
|
213: "Power input voltage and current do not match power output.",
|
||||||
|
216: "Power remained unchanged for a long time.",
|
||||||
|
217: "Power set enable error.",
|
||||||
|
218: "Power input voltage is lower than 230V for high power mode.",
|
||||||
|
233: "Power output high temperature protection error.",
|
||||||
|
234: "Power output high temperature protection error.",
|
||||||
|
235: "Power output high temperature protection error.",
|
||||||
|
236: "Power output high current protection error.",
|
||||||
|
237: "Power output high current protection error.",
|
||||||
|
238: "Power output high current protection error.",
|
||||||
|
239: "Power output high voltage protection error.",
|
||||||
|
240: "Power output low voltage protection error.",
|
||||||
|
241: "Power output current imbalance error.",
|
||||||
|
243: "Power input high temperature protection error.",
|
||||||
|
244: "Power input high temperature protection error.",
|
||||||
|
245: "Power input high temperature protection error.",
|
||||||
|
246: "Power input high voltage protection error.",
|
||||||
|
247: "Power input high voltage protection error.",
|
||||||
|
248: "Power input high current protection error.",
|
||||||
|
249: "Power input high current protection error.",
|
||||||
|
250: "Power input low voltage protection error.",
|
||||||
|
251: "Power input low voltage protection error.",
|
||||||
|
253: "Power supply fan error.",
|
||||||
|
254: "Power supply fan error.",
|
||||||
|
255: "Power output high power protection error.",
|
||||||
|
256: "Power output high power protection error.",
|
||||||
|
257: "Input over current protection of power supply on primary side.",
|
||||||
|
263: "Power communication warning.",
|
||||||
|
264: "Power communication error.",
|
||||||
|
267: "Power watchdog protection.",
|
||||||
|
268: "Power output high current protection.",
|
||||||
|
269: "Power input high current protection.",
|
||||||
|
270: "Power input high voltage protection.",
|
||||||
|
271: "Power input low voltage protection.",
|
||||||
|
272: "Excessive power supply output warning.",
|
||||||
|
273: "Power input too high warning.",
|
||||||
|
274: "Power fan warning.",
|
||||||
|
275: "Power high temperature warning.",
|
||||||
|
300: "Right board temperature sensor detection error.",
|
||||||
|
301: "Center board temperature sensor detection error.",
|
||||||
|
302: "Left board temperature sensor detection error.",
|
||||||
|
320: "Right board temperature reading error.",
|
||||||
|
321: "Center board temperature reading error.",
|
||||||
|
322: "Left board temperature reading error.",
|
||||||
|
329: "Control board temperature sensor communication error.",
|
||||||
|
350: "Right board temperature protecting.",
|
||||||
|
351: "Center board temperature protecting.",
|
||||||
|
352: "Left board temperature protecting.",
|
||||||
|
360: "Hashboard high temperature error.",
|
||||||
|
410: "Right board eeprom detection error.",
|
||||||
|
411: "Center board eeprom detection error.",
|
||||||
|
412: "Left board eeprom detection error.",
|
||||||
|
420: "Right board eeprom parsing error.",
|
||||||
|
421: "Center board eeprom parsing error.",
|
||||||
|
422: "Left board eeprom parsing error.",
|
||||||
|
430: "Right board chip bin type error.",
|
||||||
|
431: "Center board chip bin type error.",
|
||||||
|
432: "Left board chip bin type error.",
|
||||||
|
440: "Right board eeprom chip number X error.",
|
||||||
|
441: "Center board eeprom chip number X error.",
|
||||||
|
442: "Left board eeprom chip number X error.",
|
||||||
|
450: "Right board eeprom xfer error.",
|
||||||
|
451: "Center board eeprom xfer error.",
|
||||||
|
452: "Left board eeprom xfer error.",
|
||||||
|
510: "Right board miner type error.",
|
||||||
|
511: "Center board miner type error.",
|
||||||
|
512: "Left board miner type error.",
|
||||||
|
520: "Right board bin type error.",
|
||||||
|
521: "Center board bin type error.",
|
||||||
|
522: "Left board bin type error.",
|
||||||
|
530: "Right board not found.",
|
||||||
|
531: "Center board not found.",
|
||||||
|
532: "Left board not found.",
|
||||||
|
540: "Right board error reading chip id.",
|
||||||
|
541: "Center board error reading chip id.",
|
||||||
|
542: "Left board error reading chip id.",
|
||||||
|
550: "Right board has bad chips.",
|
||||||
|
551: "Center board has bad chips.",
|
||||||
|
552: "Left board has bad chips.",
|
||||||
|
560: "Right board loss of balance error.",
|
||||||
|
561: "Center board loss of balance error.",
|
||||||
|
562: "Left board loss of balance error.",
|
||||||
|
600: "Environment temperature is too high.",
|
||||||
|
610: "Environment temperature is too high for high performance mode.",
|
||||||
|
701: "Control board no support chip.",
|
||||||
|
710: "Control board rebooted as an exception.",
|
||||||
|
712: "Control board rebooted as an exception.",
|
||||||
|
800: "CGMiner checksum error.",
|
||||||
|
801: "System monitor checksum error.",
|
||||||
|
802: "Remote daemon checksum error.",
|
||||||
|
2010: "All pools are disabled.",
|
||||||
|
2020: "Pool 0 connection failed.",
|
||||||
|
2021: "Pool 1 connection failed.",
|
||||||
|
2022: "Pool 2 connection failed.",
|
||||||
|
2030: "High rejection rate on pool.",
|
||||||
|
2040: "The pool does not support asicboost mode.",
|
||||||
|
2310: "Hashrate is too low.",
|
||||||
|
2320: "Hashrate is too low.",
|
||||||
|
2340: "Hashrate loss is too high.",
|
||||||
|
2350: "Hashrate loss is too high.",
|
||||||
|
5070: "Right hashboard water velocity is abnormal.",
|
||||||
|
5071: "Center hashboard water velocity is abnormal.",
|
||||||
|
5072: "Left hashboard water velocity is abnormal.",
|
||||||
|
5110: "Right hashboard frequency up timeout.",
|
||||||
|
5111: "Center hashboard frequency up timeout.",
|
||||||
|
5112: "Left hashboard frequency up timeout.",
|
||||||
|
8410: "Software version error.",
|
||||||
|
100001: "/antiv/signature illegal.",
|
||||||
|
100002: "/antiv/dig/init.d illegal.",
|
||||||
|
100003: "/antiv/dig/pf_partial.dig illegal.",
|
||||||
|
}
|
||||||
31
pyasic/logger/__init__.py
Normal file
31
pyasic/logger/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import logging
|
||||||
|
from pyasic.settings import DEBUG, LOGFILE
|
||||||
|
|
||||||
|
|
||||||
|
def init_logger():
|
||||||
|
if LOGFILE:
|
||||||
|
logging.basicConfig(
|
||||||
|
filename="logfile.txt",
|
||||||
|
filemode="a",
|
||||||
|
format="%(pathname)s:%(lineno)d in %(funcName)s\n[%(levelname)s][%(asctime)s](%(name)s) - %(message)s",
|
||||||
|
datefmt="%x %X",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(pathname)s:%(lineno)d in %(funcName)s\n[%(levelname)s][%(asctime)s](%(name)s) - %(message)s",
|
||||||
|
datefmt="%x %X",
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger = logging.getLogger()
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
_logger.setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger("asyncssh").setLevel(logging.DEBUG)
|
||||||
|
else:
|
||||||
|
_logger.setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("asyncssh").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
return _logger
|
||||||
|
|
||||||
|
|
||||||
|
logger = init_logger()
|
||||||
104
pyasic/miners/__init__.py
Normal file
104
pyasic/miners/__init__.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import asyncssh
|
||||||
|
import logging
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMiner:
|
||||||
|
def __init__(self, *args) -> None:
|
||||||
|
self.ip = None
|
||||||
|
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))
|
||||||
5
pyasic/miners/_backends/__init__.py
Normal file
5
pyasic/miners/_backends/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .bmminer import BMMiner
|
||||||
|
from .bosminer import BOSMiner
|
||||||
|
from .btminer import BTMiner
|
||||||
|
from .cgminer import CGMiner
|
||||||
|
from .hiveon import Hiveon
|
||||||
274
pyasic/miners/_backends/bmminer.py
Normal file
274
pyasic/miners/_backends/bmminer.py
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
from pyasic.API.bmminer import BMMinerAPI
|
||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
|
||||||
|
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||||
|
|
||||||
|
|
||||||
|
class BMMiner(BaseMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.ip = ipaddress.ip_address(ip)
|
||||||
|
self.api = BMMinerAPI(ip)
|
||||||
|
self.api_type = "BMMiner"
|
||||||
|
self.uname = "root"
|
||||||
|
self.pwd = "admin"
|
||||||
|
|
||||||
|
async def get_model(self) -> str or None:
|
||||||
|
"""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
|
||||||
|
|
||||||
|
# open an ssh connection
|
||||||
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
|
# 3 retries
|
||||||
|
for i in range(3):
|
||||||
|
try:
|
||||||
|
# run the command and get the result
|
||||||
|
result = await conn.run(cmd)
|
||||||
|
result = result.stdout
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# if the command fails, log it
|
||||||
|
logging.warning(f"{self} command {cmd} error: {e}")
|
||||||
|
|
||||||
|
# on the 3rd retry, return None
|
||||||
|
if i == 3:
|
||||||
|
return
|
||||||
|
continue
|
||||||
|
# return the result, either command output or None
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def get_config(self) -> list or None:
|
||||||
|
"""Get the pool configuration of the miner.
|
||||||
|
|
||||||
|
:return: Pool config data or None.
|
||||||
|
"""
|
||||||
|
# get pool data
|
||||||
|
pools = await self.api.pools()
|
||||||
|
pool_data = []
|
||||||
|
|
||||||
|
# ensure we got pool data
|
||||||
|
if not pools:
|
||||||
|
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:
|
||||||
|
logging.debug(f"{self}: Sending reboot command.")
|
||||||
|
_ret = await self.send_ssh_command("reboot")
|
||||||
|
logging.debug(f"{self}: Reboot command completed.")
|
||||||
|
if isinstance(_ret, str):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_data(self) -> MinerData:
|
||||||
|
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||||
|
|
||||||
|
board_offset = -1
|
||||||
|
fan_offset = -1
|
||||||
|
|
||||||
|
model = await self.get_model()
|
||||||
|
hostname = await self.get_hostname()
|
||||||
|
mac = await self.get_mac()
|
||||||
|
|
||||||
|
if model:
|
||||||
|
data.model = model
|
||||||
|
|
||||||
|
if hostname:
|
||||||
|
data.hostname = hostname
|
||||||
|
|
||||||
|
if mac:
|
||||||
|
data.mac = mac
|
||||||
|
|
||||||
|
miner_data = None
|
||||||
|
for i in range(DATA_RETRIES):
|
||||||
|
miner_data = await self.api.multicommand(
|
||||||
|
"summary", "pools", "stats", ignore_x19_error=True
|
||||||
|
)
|
||||||
|
if miner_data:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not miner_data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
summary = miner_data.get("summary")[0]
|
||||||
|
pools = miner_data.get("pools")[0]
|
||||||
|
stats = miner_data.get("stats")[0]
|
||||||
|
|
||||||
|
if summary:
|
||||||
|
hr = summary.get("SUMMARY")
|
||||||
|
if hr:
|
||||||
|
if len(hr) > 0:
|
||||||
|
hr = hr[0].get("GHS av")
|
||||||
|
if hr:
|
||||||
|
data.hashrate = round(hr / 1000, 2)
|
||||||
|
|
||||||
|
if stats:
|
||||||
|
boards = stats.get("STATS")
|
||||||
|
if boards:
|
||||||
|
if len(boards) > 0:
|
||||||
|
for board_num in range(1, 16, 5):
|
||||||
|
for _b_num in range(5):
|
||||||
|
b = boards[1].get(f"chain_acn{board_num + _b_num}")
|
||||||
|
|
||||||
|
if b and not b == 0 and board_offset == -1:
|
||||||
|
board_offset = board_num
|
||||||
|
if board_offset == -1:
|
||||||
|
board_offset = 1
|
||||||
|
|
||||||
|
data.left_chips = boards[1].get(f"chain_acn{board_offset}")
|
||||||
|
data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
|
||||||
|
data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
|
||||||
|
|
||||||
|
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"}
|
||||||
|
env_temp_list = []
|
||||||
|
for item in range(3):
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
return data
|
||||||
452
pyasic/miners/_backends/bosminer.py
Normal file
452
pyasic/miners/_backends/bosminer.py
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
import toml
|
||||||
|
|
||||||
|
|
||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
from pyasic.API.bosminer import BOSMinerAPI
|
||||||
|
from pyasic.API import APIError
|
||||||
|
|
||||||
|
from pyasic.data.error_codes import BraiinsOSError
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
|
||||||
|
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||||
|
|
||||||
|
|
||||||
|
class BOSMiner(BaseMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.ip = ipaddress.ip_address(ip)
|
||||||
|
self.api = BOSMinerAPI(ip)
|
||||||
|
self.api_type = "BOSMiner"
|
||||||
|
self.uname = "root"
|
||||||
|
self.pwd = "admin"
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
async def send_ssh_command(self, cmd: str) -> str or None:
|
||||||
|
"""Send a command to the miner over ssh.
|
||||||
|
|
||||||
|
:return: Result of the command or None.
|
||||||
|
"""
|
||||||
|
result = None
|
||||||
|
|
||||||
|
# open an ssh connection
|
||||||
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
|
# 3 retries
|
||||||
|
for i in range(3):
|
||||||
|
try:
|
||||||
|
# run the command and get the result
|
||||||
|
result = await conn.run(cmd)
|
||||||
|
result = result.stdout
|
||||||
|
except Exception as e:
|
||||||
|
# if the command fails, log it
|
||||||
|
logging.warning(f"{self} command {cmd} error: {e}")
|
||||||
|
|
||||||
|
# on the 3rd retry, return None
|
||||||
|
if i == 3:
|
||||||
|
return
|
||||||
|
continue
|
||||||
|
# return the result, either command output or None
|
||||||
|
return str(result)
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
"""Sends command to turn on fault light on the miner."""
|
||||||
|
logging.debug(f"{self}: Sending fault_light on command.")
|
||||||
|
self.light = True
|
||||||
|
_ret = await self.send_ssh_command("miner fault_light on")
|
||||||
|
logging.debug(f"{self}: fault_light on command completed.")
|
||||||
|
if isinstance(_ret, str):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
"""Sends command to turn off fault light on the miner."""
|
||||||
|
logging.debug(f"{self}: Sending fault_light off command.")
|
||||||
|
self.light = False
|
||||||
|
_ret = await self.send_ssh_command("miner fault_light off")
|
||||||
|
logging.debug(f"{self}: fault_light off command completed.")
|
||||||
|
if isinstance(_ret, str):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
return await self.restart_bosminer()
|
||||||
|
|
||||||
|
async def restart_bosminer(self) -> bool:
|
||||||
|
"""Restart bosminer hashing process."""
|
||||||
|
logging.debug(f"{self}: Sending bosminer restart command.")
|
||||||
|
_ret = await self.send_ssh_command("/etc/init.d/bosminer restart")
|
||||||
|
logging.debug(f"{self}: bosminer restart command completed.")
|
||||||
|
if isinstance(_ret, str):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
"""Reboots power to the physical miner."""
|
||||||
|
logging.debug(f"{self}: Sending reboot command.")
|
||||||
|
_ret = await self.send_ssh_command("/sbin/reboot")
|
||||||
|
logging.debug(f"{self}: Reboot command completed.")
|
||||||
|
if isinstance(_ret, str):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_config(self) -> None:
|
||||||
|
logging.debug(f"{self}: Getting config.")
|
||||||
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
|
logging.debug(f"{self}: Opening SFTP connection.")
|
||||||
|
async with conn.start_sftp_client() as sftp:
|
||||||
|
logging.debug(f"{self}: Reading config file.")
|
||||||
|
async with sftp.open("/etc/bosminer.toml") as file:
|
||||||
|
toml_data = toml.loads(await file.read())
|
||||||
|
logging.debug(f"{self}: Converting config file.")
|
||||||
|
cfg = MinerConfig().from_raw(toml_data)
|
||||||
|
self.config = cfg
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
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:
|
||||||
|
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()
|
||||||
|
logging.debug(f"Found hostname for {self.ip}: {host}")
|
||||||
|
self.hostname = host
|
||||||
|
return self.hostname
|
||||||
|
else:
|
||||||
|
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||||
|
return "?"
|
||||||
|
except Exception:
|
||||||
|
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||||
|
return "?"
|
||||||
|
|
||||||
|
async def get_model(self) -> str or None:
|
||||||
|
"""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} (BOS)")
|
||||||
|
return self.model + " (BOS)"
|
||||||
|
|
||||||
|
# get devdetails data
|
||||||
|
try:
|
||||||
|
version_data = await self.api.devdetails()
|
||||||
|
except APIError as e:
|
||||||
|
version_data = None
|
||||||
|
if e.message == "Not ready":
|
||||||
|
cfg = json.loads(await self.send_ssh_command("bosminer config --data"))
|
||||||
|
model = cfg.get("data").get("format").get("model")
|
||||||
|
if model:
|
||||||
|
model = model.replace("Antminer ", "")
|
||||||
|
self.model = model
|
||||||
|
return self.model + " (BOS)"
|
||||||
|
|
||||||
|
# if we get data back, parse it for model
|
||||||
|
if version_data:
|
||||||
|
if not version_data["DEVDETAILS"] == []:
|
||||||
|
# handle Antminer BOSMiner as a base
|
||||||
|
self.model = version_data["DEVDETAILS"][0]["Model"].replace(
|
||||||
|
"Antminer ", ""
|
||||||
|
)
|
||||||
|
logging.debug(f"Found model for {self.ip}: {self.model} (BOS)")
|
||||||
|
return self.model + " (BOS)"
|
||||||
|
|
||||||
|
# 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_version(self):
|
||||||
|
"""Get miner firmware version.
|
||||||
|
|
||||||
|
:return: Miner firmware version or None.
|
||||||
|
"""
|
||||||
|
# check if version is cached
|
||||||
|
if self.version:
|
||||||
|
logging.debug(f"Found version for {self.ip}: {self.version}")
|
||||||
|
return self.version
|
||||||
|
|
||||||
|
# get output of bos version file
|
||||||
|
version_data = await self.send_ssh_command("cat /etc/bos_version")
|
||||||
|
|
||||||
|
# if we get the version data, parse it
|
||||||
|
if version_data:
|
||||||
|
self.version = version_data.split("-")[5]
|
||||||
|
logging.debug(f"Found version for {self.ip}: {self.version}")
|
||||||
|
return self.version
|
||||||
|
|
||||||
|
# if we fail to get version, log a failed attempt
|
||||||
|
logging.warning(f"Failed to get model for miner: {self}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
|
||||||
|
"""Configures miner with yaml config."""
|
||||||
|
logging.debug(f"{self}: Sending config.")
|
||||||
|
if ip_user:
|
||||||
|
suffix = str(self.ip).split(".")[-1]
|
||||||
|
toml_conf = (
|
||||||
|
MinerConfig()
|
||||||
|
.from_yaml(yaml_config)
|
||||||
|
.as_bos(model=self.model.replace(" (BOS)", ""), user_suffix=suffix)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
toml_conf = (
|
||||||
|
MinerConfig()
|
||||||
|
.from_yaml(yaml_config)
|
||||||
|
.as_bos(model=self.model.replace(" (BOS)", ""))
|
||||||
|
)
|
||||||
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
|
logging.debug(f"{self}: Opening SFTP connection.")
|
||||||
|
async with conn.start_sftp_client() as sftp:
|
||||||
|
logging.debug(f"{self}: Opening config file.")
|
||||||
|
async with sftp.open("/etc/bosminer.toml", "w+") as file:
|
||||||
|
await file.write(toml_conf)
|
||||||
|
logging.debug(f"{self}: Restarting BOSMiner")
|
||||||
|
await conn.run("/etc/init.d/bosminer restart")
|
||||||
|
|
||||||
|
async def get_board_info(self) -> dict:
|
||||||
|
"""Gets data on each board and chain in the miner."""
|
||||||
|
logging.debug(f"{self}: Getting board info.")
|
||||||
|
devdetails = await self.api.devdetails()
|
||||||
|
if not devdetails.get("DEVDETAILS"):
|
||||||
|
print("devdetails error", devdetails)
|
||||||
|
return {0: [], 1: [], 2: []}
|
||||||
|
devs = devdetails["DEVDETAILS"]
|
||||||
|
boards = {}
|
||||||
|
offset = devs[0]["ID"]
|
||||||
|
for board in devs:
|
||||||
|
boards[board["ID"] - offset] = []
|
||||||
|
if not board["Chips"] == self.nominal_chips:
|
||||||
|
nominal = False
|
||||||
|
else:
|
||||||
|
nominal = True
|
||||||
|
boards[board["ID"] - offset].append(
|
||||||
|
{
|
||||||
|
"chain": board["ID"] - offset,
|
||||||
|
"chip_count": board["Chips"],
|
||||||
|
"chip_status": "o" * board["Chips"],
|
||||||
|
"nominal": nominal,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logging.debug(f"Found board data for {self}: {boards}")
|
||||||
|
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"] == 63:
|
||||||
|
if board not in bad_boards.keys():
|
||||||
|
bad_boards[board] = []
|
||||||
|
bad_boards[board].append(chain)
|
||||||
|
return bad_boards
|
||||||
|
|
||||||
|
async def check_good_boards(self) -> str:
|
||||||
|
"""Checks for and provides list for working boards."""
|
||||||
|
devs = await self.api.devdetails()
|
||||||
|
bad = 0
|
||||||
|
chains = devs["DEVDETAILS"]
|
||||||
|
for chain in chains:
|
||||||
|
if chain["Chips"] == 0:
|
||||||
|
bad += 1
|
||||||
|
if not bad > 0:
|
||||||
|
return str(self.ip)
|
||||||
|
|
||||||
|
async def get_data(self) -> MinerData:
|
||||||
|
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||||
|
|
||||||
|
board_offset = -1
|
||||||
|
fan_offset = -1
|
||||||
|
|
||||||
|
model = await self.get_model()
|
||||||
|
hostname = await self.get_hostname()
|
||||||
|
mac = await self.get_mac()
|
||||||
|
|
||||||
|
if model:
|
||||||
|
data.model = model
|
||||||
|
|
||||||
|
if hostname:
|
||||||
|
data.hostname = hostname
|
||||||
|
|
||||||
|
if mac:
|
||||||
|
data.mac = mac
|
||||||
|
|
||||||
|
miner_data = None
|
||||||
|
for i in range(DATA_RETRIES):
|
||||||
|
try:
|
||||||
|
miner_data = await self.api.multicommand(
|
||||||
|
"summary",
|
||||||
|
"temps",
|
||||||
|
"tunerstatus",
|
||||||
|
"pools",
|
||||||
|
"devdetails",
|
||||||
|
"fans",
|
||||||
|
"devs",
|
||||||
|
)
|
||||||
|
except APIError as e:
|
||||||
|
if str(e.message) == "Not ready":
|
||||||
|
miner_data = await self.api.multicommand(
|
||||||
|
"summary", "tunerstatus", "pools", "devs"
|
||||||
|
)
|
||||||
|
if miner_data:
|
||||||
|
break
|
||||||
|
if not miner_data:
|
||||||
|
return data
|
||||||
|
summary = miner_data.get("summary")
|
||||||
|
temps = miner_data.get("temps")
|
||||||
|
tunerstatus = miner_data.get("tunerstatus")
|
||||||
|
pools = miner_data.get("pools")
|
||||||
|
devdetails = miner_data.get("devdetails")
|
||||||
|
devs = miner_data.get("devs")
|
||||||
|
fans = miner_data.get("fans")
|
||||||
|
|
||||||
|
if summary:
|
||||||
|
hr = summary[0].get("SUMMARY")
|
||||||
|
if hr:
|
||||||
|
if len(hr) > 0:
|
||||||
|
hr = hr[0].get("MHS 1m")
|
||||||
|
if hr:
|
||||||
|
data.hashrate = round(hr / 1000000, 2)
|
||||||
|
|
||||||
|
if temps:
|
||||||
|
temp = temps[0].get("TEMPS")
|
||||||
|
if temp:
|
||||||
|
if len(temp) > 0:
|
||||||
|
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
|
||||||
|
offset = 6 if temp[0]["ID"] in [6, 7, 8] else temp[0]["ID"]
|
||||||
|
for board in temp:
|
||||||
|
_id = board["ID"] - offset
|
||||||
|
chip_temp = round(board["Chip"])
|
||||||
|
board_temp = round(board["Board"])
|
||||||
|
setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
|
||||||
|
setattr(data, f"{board_map[_id]}_temp", board_temp)
|
||||||
|
|
||||||
|
if fans:
|
||||||
|
fan_data = fans[0].get("FANS")
|
||||||
|
if fan_data:
|
||||||
|
for fan in range(self.fan_count):
|
||||||
|
setattr(data, f"fan_{fan+1}", fan_data[fan]["RPM"])
|
||||||
|
|
||||||
|
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[0].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 tunerstatus:
|
||||||
|
tuner = tunerstatus[0].get("TUNERSTATUS")
|
||||||
|
if tuner:
|
||||||
|
if len(tuner) > 0:
|
||||||
|
wattage = tuner[0].get("ApproximateMinerPowerConsumption")
|
||||||
|
wattage_limit = tuner[0].get("PowerLimit")
|
||||||
|
if wattage_limit:
|
||||||
|
data.wattage_limit = wattage_limit
|
||||||
|
if wattage:
|
||||||
|
data.wattage = wattage
|
||||||
|
|
||||||
|
chain_status = tuner[0].get("TunerChainStatus")
|
||||||
|
if chain_status and len(chain_status) > 0:
|
||||||
|
board_map = {
|
||||||
|
0: "Left board",
|
||||||
|
1: "Center board",
|
||||||
|
2: "Right board",
|
||||||
|
}
|
||||||
|
offset = (
|
||||||
|
6
|
||||||
|
if chain_status[0]["HashchainIndex"] in [6, 7, 8]
|
||||||
|
else chain_status[0]["HashchainIndex"]
|
||||||
|
)
|
||||||
|
for board in chain_status:
|
||||||
|
_id = board["HashchainIndex"] - offset
|
||||||
|
if board["Status"] not in [
|
||||||
|
"Stable",
|
||||||
|
"Testing performance profile",
|
||||||
|
]:
|
||||||
|
_error = board["Status"]
|
||||||
|
_error = _error[0].lower() + _error[1:]
|
||||||
|
data.errors.append(
|
||||||
|
BraiinsOSError(f"{board_map[_id]} {_error}")
|
||||||
|
)
|
||||||
|
|
||||||
|
if devdetails:
|
||||||
|
boards = devdetails[0].get("DEVDETAILS")
|
||||||
|
if boards:
|
||||||
|
if len(boards) > 0:
|
||||||
|
board_map = {0: "left_chips", 1: "center_chips", 2: "right_chips"}
|
||||||
|
offset = 6 if boards[0]["ID"] in [6, 7, 8] else boards[0]["ID"]
|
||||||
|
for board in boards:
|
||||||
|
_id = board["ID"] - offset
|
||||||
|
chips = board["Chips"]
|
||||||
|
setattr(data, board_map[_id], chips)
|
||||||
|
|
||||||
|
if devs:
|
||||||
|
boards = devs[0].get("DEVS")
|
||||||
|
if boards:
|
||||||
|
if len(boards) > 0:
|
||||||
|
board_map = {
|
||||||
|
0: "left_board_hashrate",
|
||||||
|
1: "center_board_hashrate",
|
||||||
|
2: "right_board_hashrate",
|
||||||
|
}
|
||||||
|
offset = 6 if boards[0]["ID"] in [6, 7, 8] else boards[0]["ID"]
|
||||||
|
for board in boards:
|
||||||
|
_id = board["ID"] - offset
|
||||||
|
hashrate = round(board["MHS 1m"] / 1000000, 2)
|
||||||
|
setattr(data, board_map[_id], hashrate)
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def get_mac(self):
|
||||||
|
result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
|
||||||
|
return result.upper().strip()
|
||||||
50
pyasic/miners/_backends/bosminer_old.py
Normal file
50
pyasic/miners/_backends/bosminer_old.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
from pyasic.API.bosminer import BOSMinerAPI
|
||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class BOSMinerOld(BaseMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.ip = ipaddress.ip_address(ip)
|
||||||
|
self.api = BOSMinerAPI(ip)
|
||||||
|
self.api_type = "BOSMiner"
|
||||||
|
self.uname = "root"
|
||||||
|
self.pwd = "admin"
|
||||||
|
|
||||||
|
async def send_ssh_command(self, cmd: str) -> str or None:
|
||||||
|
"""Send a command to the miner over ssh.
|
||||||
|
|
||||||
|
:return: Result of the command or None.
|
||||||
|
"""
|
||||||
|
result = None
|
||||||
|
|
||||||
|
# open an ssh connection
|
||||||
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
|
# 3 retries
|
||||||
|
for i in range(3):
|
||||||
|
try:
|
||||||
|
# run the command and get the result
|
||||||
|
result = await conn.run(cmd)
|
||||||
|
if result.stdout:
|
||||||
|
result = result.stdout
|
||||||
|
except Exception as e:
|
||||||
|
if e == "SSH connection closed":
|
||||||
|
return "Update completed."
|
||||||
|
# if the command fails, log it
|
||||||
|
logging.warning(f"{self} command {cmd} error: {e}")
|
||||||
|
|
||||||
|
# on the 3rd retry, return None
|
||||||
|
if i == 3:
|
||||||
|
return
|
||||||
|
continue
|
||||||
|
# return the result, either command output or None
|
||||||
|
return str(result)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_to_plus(self):
|
||||||
|
result = await self.send_ssh_command("opkg update && opkg install bos_plus")
|
||||||
|
return result
|
||||||
267
pyasic/miners/_backends/btminer.py
Normal file
267
pyasic/miners/_backends/btminer.py
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
from pyasic.API.btminer import BTMinerAPI
|
||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
from pyasic.API import APIError
|
||||||
|
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
from pyasic.data.error_codes import WhatsminerError
|
||||||
|
|
||||||
|
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||||
|
|
||||||
|
|
||||||
|
class BTMiner(BaseMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.ip = ipaddress.ip_address(ip)
|
||||||
|
self.api = BTMinerAPI(ip)
|
||||||
|
self.api_type = "BTMiner"
|
||||||
|
|
||||||
|
async def get_model(self):
|
||||||
|
if self.model:
|
||||||
|
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||||
|
return self.model
|
||||||
|
version_data = await self.api.devdetails()
|
||||||
|
if version_data:
|
||||||
|
self.model = version_data["DEVDETAILS"][0]["Model"].split("V")[0]
|
||||||
|
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||||
|
return self.model
|
||||||
|
logging.warning(f"Failed to get model for miner: {self}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_hostname(self) -> str or None:
|
||||||
|
if 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:
|
||||||
|
"""Gets data on each board and chain in the miner."""
|
||||||
|
logging.debug(f"{self}: Getting board info.")
|
||||||
|
devs = await self.api.devs()
|
||||||
|
if not devs.get("DEVS"):
|
||||||
|
print("devs error", devs)
|
||||||
|
return {0: [], 1: [], 2: []}
|
||||||
|
devs = devs["DEVS"]
|
||||||
|
boards = {}
|
||||||
|
offset = devs[0]["ID"]
|
||||||
|
for board in devs:
|
||||||
|
boards[board["ID"] - offset] = []
|
||||||
|
if "Effective Chips" in board.keys():
|
||||||
|
if not board["Effective Chips"] in self.nominal_chips:
|
||||||
|
nominal = False
|
||||||
|
else:
|
||||||
|
nominal = True
|
||||||
|
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):
|
||||||
|
mac = ""
|
||||||
|
data = await self.api.summary()
|
||||||
|
if data:
|
||||||
|
if data.get("SUMMARY"):
|
||||||
|
if len(data["SUMMARY"]) > 0:
|
||||||
|
_mac = data["SUMMARY"][0].get("MAC")
|
||||||
|
if _mac:
|
||||||
|
mac = _mac
|
||||||
|
if mac == "":
|
||||||
|
try:
|
||||||
|
data = await self.api.get_miner_info()
|
||||||
|
if data:
|
||||||
|
if "Msg" in data.keys():
|
||||||
|
if "mac" in data["Msg"].keys():
|
||||||
|
mac = data["Msg"]["mac"]
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return str(mac).upper()
|
||||||
|
|
||||||
|
async def get_data(self):
|
||||||
|
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||||
|
|
||||||
|
mac = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
model = await self.get_model()
|
||||||
|
except APIError:
|
||||||
|
logging.info(f"Failed to get model: {self}")
|
||||||
|
model = None
|
||||||
|
data.model = "Whatsminer"
|
||||||
|
|
||||||
|
try:
|
||||||
|
hostname = await self.get_hostname()
|
||||||
|
except APIError:
|
||||||
|
logging.info(f"Failed to get hostname: {self}")
|
||||||
|
hostname = None
|
||||||
|
data.hostname = "Whatsminer"
|
||||||
|
|
||||||
|
if model:
|
||||||
|
data.model = model
|
||||||
|
|
||||||
|
if hostname:
|
||||||
|
data.hostname = hostname
|
||||||
|
|
||||||
|
miner_data = None
|
||||||
|
for i in range(DATA_RETRIES):
|
||||||
|
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
|
||||||
258
pyasic/miners/_backends/cgminer.py
Normal file
258
pyasic/miners/_backends/cgminer.py
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
from pyasic.API.cgminer import CGMinerAPI
|
||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
from pyasic.API import APIError
|
||||||
|
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
|
||||||
|
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||||
|
|
||||||
|
|
||||||
|
class CGMiner(BaseMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.ip = ipaddress.ip_address(ip)
|
||||||
|
self.api = CGMinerAPI(ip)
|
||||||
|
self.api_type = "CGMiner"
|
||||||
|
self.uname = "root"
|
||||||
|
self.pwd = "admin"
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
async def get_model(self):
|
||||||
|
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
|
||||||
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
|
for i in range(3):
|
||||||
|
try:
|
||||||
|
result = await conn.run(cmd)
|
||||||
|
result = result.stdout
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{cmd} error: {e}")
|
||||||
|
if i == 3:
|
||||||
|
return
|
||||||
|
continue
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
return await self.restart_cgminer()
|
||||||
|
|
||||||
|
async def restart_cgminer(self) -> bool:
|
||||||
|
commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"]
|
||||||
|
commands = ";".join(commands)
|
||||||
|
_ret = await self.send_ssh_command(commands)
|
||||||
|
if isinstance(_ret, str):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
logging.debug(f"{self}: Sending reboot command.")
|
||||||
|
_ret = await self.send_ssh_command("reboot")
|
||||||
|
logging.debug(f"{self}: Reboot command completed.")
|
||||||
|
if isinstance(_ret, str):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def start_cgminer(self) -> None:
|
||||||
|
commands = [
|
||||||
|
"mkdir -p /etc/tmp/",
|
||||||
|
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
|
||||||
|
"crontab -u root /etc/tmp/root",
|
||||||
|
"/usr/bin/cgminer-monitor >/dev/null 2>&1",
|
||||||
|
]
|
||||||
|
commands = ";".join(commands)
|
||||||
|
await self.send_ssh_command(commands)
|
||||||
|
|
||||||
|
async def stop_cgminer(self) -> None:
|
||||||
|
commands = [
|
||||||
|
"mkdir -p /etc/tmp/",
|
||||||
|
'echo "" > /etc/tmp/root',
|
||||||
|
"crontab -u root /etc/tmp/root",
|
||||||
|
"killall cgminer",
|
||||||
|
]
|
||||||
|
commands = ";".join(commands)
|
||||||
|
await self.send_ssh_command(commands)
|
||||||
|
|
||||||
|
async def get_config(self) -> None:
|
||||||
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
|
command = "cat /etc/config/cgminer"
|
||||||
|
result = await conn.run(command, check=True)
|
||||||
|
self.config = result.stdout
|
||||||
|
print(str(self.config))
|
||||||
|
|
||||||
|
async def get_data(self):
|
||||||
|
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||||
|
|
||||||
|
board_offset = -1
|
||||||
|
fan_offset = -1
|
||||||
|
|
||||||
|
model = await self.get_model()
|
||||||
|
hostname = await self.get_hostname()
|
||||||
|
mac = await self.get_mac()
|
||||||
|
|
||||||
|
if model:
|
||||||
|
data.model = model
|
||||||
|
|
||||||
|
if hostname:
|
||||||
|
data.hostname = hostname
|
||||||
|
|
||||||
|
if mac:
|
||||||
|
data.mac = mac
|
||||||
|
|
||||||
|
miner_data = None
|
||||||
|
for i in range(DATA_RETRIES):
|
||||||
|
miner_data = await self.api.multicommand(
|
||||||
|
"summary", "pools", "stats", ignore_x19_error=True
|
||||||
|
)
|
||||||
|
if miner_data:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not miner_data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
summary = miner_data.get("summary")[0]
|
||||||
|
pools = miner_data.get("pools")[0]
|
||||||
|
stats = miner_data.get("stats")[0]
|
||||||
|
|
||||||
|
if summary:
|
||||||
|
hr = summary.get("SUMMARY")
|
||||||
|
if hr:
|
||||||
|
if len(hr) > 0:
|
||||||
|
hr = hr[0].get("GHS av")
|
||||||
|
if hr:
|
||||||
|
data.hashrate = round(hr / 1000, 2)
|
||||||
|
|
||||||
|
if stats:
|
||||||
|
boards = stats.get("STATS")
|
||||||
|
if boards:
|
||||||
|
if len(boards) > 0:
|
||||||
|
for board_num in range(1, 16, 5):
|
||||||
|
for _b_num in range(5):
|
||||||
|
b = boards[1].get(f"chain_acn{board_num + _b_num}")
|
||||||
|
|
||||||
|
if b and not b == 0 and board_offset == -1:
|
||||||
|
board_offset = board_num
|
||||||
|
if board_offset == -1:
|
||||||
|
board_offset = 1
|
||||||
|
|
||||||
|
data.left_chips = boards[1].get(f"chain_acn{board_offset}")
|
||||||
|
data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
|
||||||
|
data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
|
||||||
|
|
||||||
|
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"}
|
||||||
|
env_temp_list = []
|
||||||
|
for item in range(3):
|
||||||
|
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:
|
||||||
|
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"]
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
return data
|
||||||
48
pyasic/miners/_backends/hiveon.py
Normal file
48
pyasic/miners/_backends/hiveon.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from pyasic.miners._backends import BMMiner
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
|
||||||
|
class Hiveon(BMMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.ip = ipaddress.ip_address(ip)
|
||||||
|
self.api_type = "Hiveon"
|
||||||
|
self.uname = "root"
|
||||||
|
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
|
||||||
3
pyasic/miners/_types/__init__.py
Normal file
3
pyasic/miners/_types/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .antminer import *
|
||||||
|
from .avalonminer import *
|
||||||
|
from .whatsminer import *
|
||||||
10
pyasic/miners/_types/antminer/X17/S17.py
Normal file
10
pyasic/miners/_types/antminer/X17/S17.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S17(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S17"
|
||||||
|
self.nominal_chips = 48
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X17/S17_Plus.py
Normal file
10
pyasic/miners/_types/antminer/X17/S17_Plus.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S17Plus(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S17+"
|
||||||
|
self.nominal_chips = 65
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X17/S17_Pro.py
Normal file
10
pyasic/miners/_types/antminer/X17/S17_Pro.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S17Pro(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S17 Pro"
|
||||||
|
self.nominal_chips = 48
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X17/S17e.py
Normal file
10
pyasic/miners/_types/antminer/X17/S17e.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S17e(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S17e"
|
||||||
|
self.nominal_chips = 135
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X17/T17.py
Normal file
10
pyasic/miners/_types/antminer/X17/T17.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class T17(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "T17"
|
||||||
|
self.nominal_chips = 30
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X17/T17_Plus.py
Normal file
10
pyasic/miners/_types/antminer/X17/T17_Plus.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class T17Plus(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "T17+"
|
||||||
|
self.nominal_chips = 44
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X17/T17e.py
Normal file
10
pyasic/miners/_types/antminer/X17/T17e.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class T17e(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "T17e"
|
||||||
|
self.nominal_chips = 78
|
||||||
|
self.fan_count = 4
|
||||||
8
pyasic/miners/_types/antminer/X17/__init__.py
Normal file
8
pyasic/miners/_types/antminer/X17/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from .S17 import S17
|
||||||
|
from .S17_Plus import S17Plus
|
||||||
|
from .S17_Pro import S17Pro
|
||||||
|
from .S17e import S17e
|
||||||
|
|
||||||
|
from .T17 import T17
|
||||||
|
from .T17_Plus import T17Plus
|
||||||
|
from .T17e import T17e
|
||||||
10
pyasic/miners/_types/antminer/X19/S19.py
Normal file
10
pyasic/miners/_types/antminer/X19/S19.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S19(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S19"
|
||||||
|
self.nominal_chips = 76
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X19/S19_Pro.py
Normal file
10
pyasic/miners/_types/antminer/X19/S19_Pro.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S19Pro(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S19 Pro"
|
||||||
|
self.nominal_chips = 114
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X19/S19a.py
Normal file
10
pyasic/miners/_types/antminer/X19/S19a.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S19a(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S19a"
|
||||||
|
self.nominal_chips = 72
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X19/S19j.py
Normal file
10
pyasic/miners/_types/antminer/X19/S19j.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S19j(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S19j"
|
||||||
|
self.nominal_chips = 114
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X19/S19j_Pro.py
Normal file
10
pyasic/miners/_types/antminer/X19/S19j_Pro.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S19jPro(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S19j Pro"
|
||||||
|
self.nominal_chips = 126
|
||||||
|
self.fan_count = 4
|
||||||
10
pyasic/miners/_types/antminer/X19/T19.py
Normal file
10
pyasic/miners/_types/antminer/X19/T19.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class T19(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "T19"
|
||||||
|
self.nominal_chips = 76
|
||||||
|
self.fan_count = 4
|
||||||
9
pyasic/miners/_types/antminer/X19/__init__.py
Normal file
9
pyasic/miners/_types/antminer/X19/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from .S19 import S19
|
||||||
|
from .S19_Pro import S19Pro
|
||||||
|
|
||||||
|
from .S19j import S19j
|
||||||
|
from .S19j_Pro import S19jPro
|
||||||
|
|
||||||
|
from .S19a import S19a
|
||||||
|
|
||||||
|
from .T19 import T19
|
||||||
10
pyasic/miners/_types/antminer/X9/S9.py
Normal file
10
pyasic/miners/_types/antminer/X9/S9.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S9(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S9"
|
||||||
|
self.nominal_chips = 63
|
||||||
|
self.fan_count = 2
|
||||||
10
pyasic/miners/_types/antminer/X9/S9i.py
Normal file
10
pyasic/miners/_types/antminer/X9/S9i.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class S9i(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "S9i"
|
||||||
|
self.nominal_chips = 63
|
||||||
|
self.fan_count = 2
|
||||||
10
pyasic/miners/_types/antminer/X9/T9.py
Normal file
10
pyasic/miners/_types/antminer/X9/T9.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class T9(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "T9"
|
||||||
|
self.nominal_chips = 57
|
||||||
|
self.fan_count = 2
|
||||||
3
pyasic/miners/_types/antminer/X9/__init__.py
Normal file
3
pyasic/miners/_types/antminer/X9/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .S9 import S9
|
||||||
|
from .S9i import S9i
|
||||||
|
from .T9 import T9
|
||||||
3
pyasic/miners/_types/antminer/__init__.py
Normal file
3
pyasic/miners/_types/antminer/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .X9 import *
|
||||||
|
from .X17 import *
|
||||||
|
from .X19 import *
|
||||||
10
pyasic/miners/_types/avalonminer/A10X/A1026.py
Normal file
10
pyasic/miners/_types/avalonminer/A10X/A1026.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class Avalon1026(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "Avalon 1026"
|
||||||
|
self.nominal_chips = 80
|
||||||
|
self.fan_count = 2
|
||||||
10
pyasic/miners/_types/avalonminer/A10X/A1047.py
Normal file
10
pyasic/miners/_types/avalonminer/A10X/A1047.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class Avalon1047(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "Avalon 1047"
|
||||||
|
self.nominal_chips = 80
|
||||||
|
self.fan_count = 2
|
||||||
10
pyasic/miners/_types/avalonminer/A10X/A1066.py
Normal file
10
pyasic/miners/_types/avalonminer/A10X/A1066.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class Avalon1066(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "Avalon 1066"
|
||||||
|
self.nominal_chips = 114
|
||||||
|
self.fan_count = 4
|
||||||
3
pyasic/miners/_types/avalonminer/A10X/__init__.py
Normal file
3
pyasic/miners/_types/avalonminer/A10X/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .A1026 import Avalon1026
|
||||||
|
from .A1047 import Avalon1047
|
||||||
|
from .A1066 import Avalon1066
|
||||||
10
pyasic/miners/_types/avalonminer/A7X/A721.py
Normal file
10
pyasic/miners/_types/avalonminer/A7X/A721.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class Avalon721(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "Avalon 721"
|
||||||
|
self.chip_count = 18 # This miner has 4 boards totaling 72
|
||||||
|
self.fan_count = 1 # also only 1 fan
|
||||||
10
pyasic/miners/_types/avalonminer/A7X/A741.py
Normal file
10
pyasic/miners/_types/avalonminer/A7X/A741.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class Avalon741(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "Avalon 741"
|
||||||
|
self.chip_count = 22 # This miner has 4 boards totaling 88
|
||||||
|
self.fan_count = 1 # also only 1 fan
|
||||||
10
pyasic/miners/_types/avalonminer/A7X/A761.py
Normal file
10
pyasic/miners/_types/avalonminer/A7X/A761.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pyasic.miners import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class Avalon761(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "Avalon 761"
|
||||||
|
self.chip_count = 18 # This miner has 4 boards totaling 72
|
||||||
|
self.fan_count = 1 # also only 1 fan
|
||||||
3
pyasic/miners/_types/avalonminer/A7X/__init__.py
Normal file
3
pyasic/miners/_types/avalonminer/A7X/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .A721 import Avalon721
|
||||||
|
from .A741 import Avalon741
|
||||||
|
from .A761 import Avalon761
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user