Compare commits

...

229 Commits

Author SHA1 Message Date
Upstream Data
c5c4bb10ee version: bump version number. 2023-12-16 10:59:23 -07:00
Upstream Data
c4dfdda448 Merge branch 'dev_bugs'
# Conflicts:
#	pyasic/miners/miner_factory.py
#	pyasic/miners/types/whatsminer/M6X/M60.py
#	pyasic/miners/types/whatsminer/M6X/M60S.py
#	pyasic/miners/types/whatsminer/M6X/M63.py
#	pyasic/miners/types/whatsminer/M6X/M63S.py
#	pyasic/miners/types/whatsminer/M6X/M66.py
#	pyasic/miners/types/whatsminer/M6X/M66S.py
#	pyasic/miners/types/whatsminer/M6X/__init__.py
#	pyasic/miners/whatsminer/btminer/M6X/M60.py
#	pyasic/miners/whatsminer/btminer/M6X/M60S.py
#	pyasic/miners/whatsminer/btminer/M6X/M66S.py
#	pyasic/miners/whatsminer/btminer/M6X/__init__.py
#	pyasic/miners/whatsminer/btminer/__init__.py
2023-12-16 10:55:27 -07:00
Upstream Data
4459de2260 feature: add support for S19kProNoPIC BOS. Reformat. 2023-12-16 10:54:51 -07:00
UpstreamData
201cfd7ef9 docs: update documentation to be more readable on the main page. 2023-12-13 11:15:03 -07:00
UpstreamData
4201905fdd bug: fix some tasks not being cancelled properly in miner factory. 2023-12-13 10:18:28 -07:00
checksum0
497ffb5bc0 Add all the currently known Whatsminer M6X machines (#77)
* Create new BTMiner M6X backend class to represent Whatsminer new M6X generation

* Add all new known types of Whatsminer M6X

* Ensure all new types are imported in their respective __init__.py

* Create all BTMiner API class for known types of new M6X generation

* Ensure all new BTMiner API class are imported in __init__.py

* Fix erroneous M6X models data

* Ensure M6X miners are imported and add them to their MinerTypes dictionary in miner_factory.py
2023-12-12 19:38:36 -07:00
checksum0
2f762c95db Add all the currently known Whatsminer M6X machines (#77)
* Create new BTMiner M6X backend class to represent Whatsminer new M6X generation

* Add all new known types of Whatsminer M6X

* Ensure all new types are imported in their respective __init__.py

* Create all BTMiner API class for known types of new M6X generation

* Ensure all new BTMiner API class are imported in __init__.py

* Fix erroneous M6X models data

* Ensure M6X miners are imported and add them to their MinerTypes dictionary in miner_factory.py
2023-12-12 19:32:12 -07:00
UpstreamData
67aed79330 bug: fix mode spec in bosminer config. 2023-12-12 13:21:50 -07:00
UpstreamData
073e048726 bug: fix bosminer config missing format information. 2023-12-12 13:11:49 -07:00
UpstreamData
02234f3d1e feature: improve dict merging speed 2023-12-12 09:25:43 -07:00
UpstreamData
dc22df0280 refactor: remove innosilicon pool comment, as it is correct. 2023-12-12 08:54:24 -07:00
UpstreamData
02056b8c88 refactor: remove config prints. 2023-12-11 15:36:02 -07:00
UpstreamData
3a43cd293c bug: Fix improper naming of fan mode. 2023-12-11 15:18:23 -07:00
UpstreamData
6941d9f349 bug: add default case for work mode when there is no work mode returned from bitmain. 2023-12-11 15:08:57 -07:00
UpstreamData
f6b0b64d86 bug: set default quota to 1. 2023-12-11 14:07:17 -07:00
UpstreamData
8d68dd9dac refactor: re-order config keys 2023-12-11 14:06:22 -07:00
UpstreamData
27368a9bd2 bug: fix some issues, and remove unused imports. 2023-12-11 13:48:26 -07:00
UpstreamData
c919b00312 feature: allow config conversion to and from dict. 2023-12-11 13:40:10 -07:00
UpstreamData
f162529883 feature: allow dps conversion for bos grpc. 2023-12-11 11:40:46 -07:00
Upstream Data
bb182bb22d bug: fix some issues with return types and missing return statements. 2023-12-10 20:28:06 -07:00
Upstream Data
af15c4fbd1 bug: pin working betterproto version. 2023-12-10 20:25:27 -07:00
Upstream Data
47c2eb9f0e feature: use betterproto + grpclib. 2023-12-10 20:10:11 -07:00
Upstream Data
1ab39f5873 bug: fix bosminer config parsing. 2023-12-10 17:40:39 -07:00
Upstream Data
43200a7354 feature: Add bosminer.toml parser. 2023-12-10 13:20:03 -07:00
Upstream Data
4fc57832e1 feature: Finish fixing get and send config handlers for miners. 2023-12-10 10:14:57 -07:00
Upstream Data
9ee63cc3ab feature: Update get and send config methods for most miners, and add as_inno. 2023-12-10 10:10:55 -07:00
Upstream Data
b22b506d55 feature: Add whatsminer send_config. 2023-12-10 09:55:05 -07:00
Upstream Data
468fba3465 feature: Add whatsminer set mode commands. 2023-12-10 09:49:24 -07:00
Upstream Data
0399094197 feature: add AM old and goldshell configs. 2023-12-10 09:45:34 -07:00
Upstream Data
bfdfa8a6ab feature: Add AM modern send and get config. 2023-12-10 09:30:31 -07:00
Upstream Data
83d0d09b0d feature: Add whatsminer get_config. 2023-12-09 17:35:47 -07:00
Upstream Data
f892c3a0fd feature: Add from am_modern to config. 2023-12-09 16:59:39 -07:00
Upstream Data
81b974f565 bug: fix bad indentation. 2023-12-09 15:12:36 -07:00
UpstreamData
5eaf876c6d feature: add bos to config miner types. 2023-12-09 13:27:23 -07:00
Upstream Data
d7d1b845a7 feature: add MinerConfig.from_api(). 2023-12-09 13:06:52 -07:00
UpstreamData
242517a36a feature: add inno to config miner types. 2023-12-08 11:03:36 -07:00
UpstreamData
791249bf3d feature: add avalon and goldshell to miner config types. 2023-12-08 10:57:57 -07:00
UpstreamData
5a70a27f07 reformat: remove some useless files. 2023-12-08 10:11:43 -07:00
UpstreamData
bca81f3bca feature: add AM old and modern, and WM config implementation. 2023-12-08 10:10:21 -07:00
UpstreamData
6d75565baf feature: start adding new config implementation. 2023-12-08 09:16:04 -07:00
JP Compagnone
9f42e6a3be add new Antminer models (S19jPro+ and S19k Pro) (#75)
* Add S19jPro+ and S19K Pro

* typo
2023-12-08 08:34:30 -07:00
Upstream Data
362b204c91 version: bump version number. 2023-11-29 20:45:46 -07:00
Upstream Data
952b660c05 bug: re-add missing socket check during scan. 2023-11-29 20:45:25 -07:00
UpstreamData
fbd73881d4 version: bump version number. 2023-11-28 16:42:15 -07:00
JP Compagnone
68c4dadb63 hotfix: fix epic api error handling (#74)
* hotfix: fix epic api error handling

* much cleaner way to handle the retry
2023-11-28 16:39:57 -07:00
UpstreamData
87016670d4 version: bump version number. 2023-11-28 10:50:56 -07:00
JP Compagnone
8701bbe4e2 Feature - Add initial ePIC UMC support (#71)
* prelim support of ePIC UMC

* slowly adding things

* add most api calls

* add some guards

* fix post commands

* remove print

* catch when API returns error

* missing guard

* remove semicolon

* recommended changes

* add docs and changes

* respect ignore_errors
2023-11-28 10:49:49 -07:00
UpstreamData
7d1f125b0b docs: update settings docs to list. 2023-11-27 11:07:43 -07:00
UpstreamData
e433902bd5 docs: update settings docs. 2023-11-27 10:57:13 -07:00
UpstreamData
a653772968 docs: update network docs. 2023-11-27 10:53:40 -07:00
UpstreamData
d8b938cd5b version: bump version number. 2023-11-27 10:39:10 -07:00
UpstreamData
47d76e325a docs: update docs to include new information. 2023-11-27 10:37:38 -07:00
UpstreamData
7ee7868094 feature: change so_linger_time to socket_linger_time. 2023-11-27 10:19:30 -07:00
UpstreamData
3f1183a4f9 feature: add so_linger option to settings. 2023-11-27 09:24:28 -07:00
Upstream Data
2b443497ea bug: improve handling of whatsminers in get_miner. 2023-11-25 12:48:45 -07:00
Upstream Data
c3972f9524 feature: add default ssl ctx to all httpx clients to speed up initialization. 2023-11-25 01:08:04 -07:00
Upstream Data
92bbb21c11 bug: handle OSError as ConnectionError, and handle Vnish Msg bug because of missing id key. 2023-11-25 00:05:03 -07:00
Upstream Data
1980ff2563 bug: do additional checks on refused connection when scanning. 2023-11-24 23:32:22 -07:00
Upstream Data
93d09a1483 bug: handle unhandled error in pings. 2023-11-24 23:28:16 -07:00
Upstream Data
690d0d99df feature: added new setting for api command timeouts. 2023-11-24 23:19:14 -07:00
Upstream Data
78f689eb2c feature: update scan method to use port 80 when possible, and add .scan() method. 2023-11-24 23:13:56 -07:00
Upstream Data
e68f188e8f bug: fix timeout references in MinerFactory and fix MinerNetwork instantiation. 2023-11-24 23:00:06 -07:00
Upstream Data
7eda611fe9 bug: fix scanning size being too large. 2023-11-24 22:50:43 -07:00
Upstream Data
1d12817942 feature: improve network parsing and implementation. 2023-11-24 22:38:43 -07:00
UpstreamData
b24efd4c69 bug: fix miner network not working with lists. 2023-11-24 13:27:09 -07:00
UpstreamData
5533135b05 docs: update supported miners. 2023-11-23 11:54:01 -07:00
UpstreamData
475054fbe0 feature: finish support for most whatsminer subtypes. 2023-11-23 11:52:44 -07:00
Daniel Sokil
06bad1bbe0 Add More Whatsminer Models, and Additional Config For Existing Models (#70) 2023-11-23 11:35:18 -07:00
UpstreamData
f3746ff756 version: bump version number. 2023-11-20 11:19:45 -07:00
UpstreamData
9f16d37c8b feature: hide GRPC and GQL if BOSer is not found. 2023-11-20 11:19:13 -07:00
UpstreamData
8a13c7940a docs: update pyproject.toml description. 2023-11-20 10:33:25 -07:00
UpstreamData
8bea76ff67 feature: add chip count for M30S+VG50. 2023-11-20 10:32:51 -07:00
Upstream Data
1504bd744c version: bump version number. 2023-11-18 22:45:38 -07:00
Upstream Data
6449f10615 feature: implement GPRC set commands properly. 2023-11-18 22:45:09 -07:00
UpstreamData
d79509bda7 version: bump version number. Pin httpx to 0.25.0 min. 2023-11-12 18:36:45 -07:00
UpstreamData
630b847466 version: bump version number. Pin httpx to 0.25.0 min. 2023-11-12 18:35:52 -07:00
Colin Crossman
ed11611919 Bump version number
Note: some issues with HTTPX may be resolved by using 1.0.0b, but I did not bump the requirement at this time to the beta.
2023-11-11 13:59:14 -07:00
Colin Crossman
e2431c938d Address unknown password issue on Whatsminers
When a whatsminer had an unknown password (not the default one), it would result in a timeout error. By moving the password check to before the data pull step, the timeout issue can be caught and addressed efficiently.
2023-11-11 13:52:04 -07:00
Colin Crossman
60f4b4a5ed Address a situation which causes many asyncio errors 2023-11-11 13:49:51 -07:00
UpstreamData
d41097af20 version: bump vbersion number. 2023-11-08 11:13:24 -07:00
UpstreamData
8a5d505731 bug: fix anyio stream error on some linux distros when getting miner. 2023-11-08 11:12:46 -07:00
UpstreamData
36e76c6f41 Add support for the grpc set_performance_mode command. 2023-11-07 08:54:06 -07:00
UpstreamData
717b9421dd Merge branch 'dev_grpc'
# Conflicts:
#	pyasic/web/bosminer.py
#	pyproject.toml
2023-10-30 16:36:06 -06:00
UpstreamData
d2f71e8c94 version: bump version number. 2023-10-30 16:34:05 -06:00
UpstreamData
697991f28f bug: fix some cases where a warning could still be passed when it was unexpected. 2023-10-30 16:33:01 -06:00
UpstreamData
b0e18ab766 feature: implement most of the GRPC functions for BOS, except for some configuration options which will require complex enums. 2023-10-27 16:35:09 -06:00
UpstreamData
e39a6921d0 refactor: improve settings handling to not use a dataclass, and not use singleton. 2023-10-26 10:28:59 -06:00
UpstreamData
aac1be0565 feature: refactor BOS web class into multiple classes. 2023-10-26 10:28:57 -06:00
UpstreamData
683fcb2138 version: bump version number. 2023-10-26 10:28:56 -06:00
UpstreamData
9fbbef9b18 bug: fix an issue with bosminer not responding correctly on older models with fans. 2023-10-26 10:28:56 -06:00
UpstreamData
6e0b9a0a7b version: bump version number. 2023-10-26 10:28:56 -06:00
UpstreamData
7f472f6f4f bug: fix possible missing value for bitmain work mode when checking is_mining. 2023-10-26 10:28:55 -06:00
UpstreamData
b7d7b33ab9 bug: round hashrate data in MinerData, and remove some unused imports. 2023-10-26 10:28:55 -06:00
UpstreamData
da11c0bb1f version: bump version number. 2023-10-26 10:28:55 -06:00
UpstreamData
eae433d2bd bug: update get_miner to work with latest whatsminer version. 2023-10-26 10:28:54 -06:00
UpstreamData
c16bc37aff refactor: improve settings handling to not use a dataclass, and not use singleton. 2023-10-26 10:28:53 -06:00
UpstreamData
255b06ac9e version: bump version number. 2023-10-23 13:01:44 -06:00
UpstreamData
29ec619126 bug: fix an issue with bosminer not responding correctly on older models with fans. 2023-10-23 12:59:52 -06:00
UpstreamData
247def04ff version: bump version number. 2023-10-12 13:19:38 -06:00
UpstreamData
4600e7d953 bug: fix possible missing value for bitmain work mode when checking is_mining. 2023-10-12 13:19:11 -06:00
UpstreamData
850c266555 bug: round hashrate data in MinerData, and remove some unused imports. 2023-10-10 13:59:28 -06:00
UpstreamData
ad374fe2fb version: bump version number. 2023-10-05 10:18:10 -06:00
UpstreamData
5ca39b6fe7 bug: update get_miner to work with latest whatsminer version. 2023-10-05 10:17:45 -06:00
UpstreamData
b50dd26e6f feature: refactor BOS web class into multiple classes. 2023-10-03 15:07:39 -06:00
UpstreamData
53eaccaa9b docs: update documentation. 2023-10-03 15:07:39 -06:00
UpstreamData
91f207316a version: bump version number. 2023-10-03 15:07:39 -06:00
UpstreamData
1e37418909 bug: fix some issues with early version of whatsminers, and handle some possible errors with BOS. 2023-10-03 15:07:38 -06:00
UpstreamData
4c09ba5529 version: bump version number. 2023-10-03 15:07:38 -06:00
UpstreamData
7bab4747ad refactor: improve settings handling to not use a dataclass, and not use singleton. 2023-10-03 15:07:37 -06:00
UpstreamData
fd8cc7378c version: bump version number. 2023-10-03 15:07:33 -06:00
UpstreamData
8aeef4d5e7 feature: add support for M20P, and add chips for M20SV30. 2023-10-03 15:07:33 -06:00
UpstreamData
4bafde9da7 docs: update documentation. 2023-10-03 14:59:25 -06:00
UpstreamData
5a3107aecf version: bump version number. 2023-10-03 11:12:11 -06:00
UpstreamData
7e758720f0 bug: fix some issues with early version of whatsminers, and handle some possible errors with BOS. 2023-10-03 11:11:32 -06:00
UpstreamData
39e3e249f8 version: bump version number. 2023-10-02 13:14:21 -06:00
UpstreamData
118c5b056e refactor: improve settings handling to not use a dataclass, and not use singleton. 2023-10-02 13:13:31 -06:00
UpstreamData
2c3b5599fe version: bump version number. 2023-10-02 09:20:24 -06:00
UpstreamData
e421eaa324 feature: add support for M20P, and add chips for M20SV30. 2023-10-02 09:20:01 -06:00
UpstreamData
75d6bc6808 version: bump version number. 2023-09-28 15:49:23 -06:00
UpstreamData
98c547e416 bug: fAdd new commands added in whatsminer API 2.0.5. 2023-09-28 15:49:23 -06:00
UpstreamData
45250e36e4 bug: fix whatsminer identification to work with backwards incompatible changes in API 2.0.5. 2023-09-28 15:49:23 -06:00
UpstreamData
fa7544d052 Update README.md 2023-09-28 15:49:22 -06:00
UpstreamData
53f3fc5ee9 version: bump version number. 2023-09-28 15:47:49 -06:00
UpstreamData
1b36de4131 bug: fAdd new commands added in whatsminer API 2.0.5. 2023-09-28 15:47:20 -06:00
UpstreamData
6f0c6f6284 bug: fix whatsminer identification to work with backwards incompatible changes in API 2.0.5. 2023-09-28 15:42:12 -06:00
UpstreamData
b7dda5bf87 Update README.md 2023-09-26 11:50:56 -06:00
UpstreamData
14f33a40c3 feature: add grpc BOS class and add grpc requests to requirements. 2023-09-22 09:44:25 -06:00
UpstreamData
5c904aced0 feature: refactor BOS web class into multiple classes. 2023-09-22 09:32:59 -06:00
UpstreamData
53a3bbf531 version: bump version number. 2023-09-19 13:59:56 -06:00
UpstreamData
50586f1ce7 feature: add S19+. 2023-09-19 13:59:03 -06:00
UpstreamData
9f6235a0fc feature: add S19i. 2023-09-19 13:56:40 -06:00
UpstreamData
4d21f150ce version: bump version number. 2023-09-18 09:35:38 -06:00
UpstreamData
7c0dfc49dd bug: fix wrong fault light setting when setting fault light to off. 2023-09-18 09:35:19 -06:00
UpstreamData
269b13f6c1 version: bump version number. 2023-09-15 08:57:56 -06:00
Elias Kunnas
a9bb7d2e5a Fix btminer pre_power_on (#62) 2023-09-15 08:56:29 -06:00
Upstream Data
11295f27a7 version: bump version number. 2023-09-12 19:21:04 -06:00
Upstream Data
55aa3dd85b bug: handle edge cases where a missed get_config on bosminer can cause an empty config to be applied to a miner. 2023-09-12 19:20:48 -06:00
UpstreamData
20272d4360 version: bump version number. 2023-09-11 13:45:52 -06:00
UpstreamData
623dc92ef2 feature: Add MinerData.as_dict(). 2023-09-11 13:45:23 -06:00
Upstream Data
2d59394b1e version: bump version number. 2023-09-07 19:07:11 -06:00
Upstream Data
26c2095ff1 bug: fix uncaught error in get_hashboards with BMMiner if a key doesnt exist. 2023-09-07 19:06:51 -06:00
Upstream Data
ec7d241caa version: bump version number. 2023-09-05 17:22:23 -06:00
Upstream Data
d0432ed1aa bug: handle for some weird edge cases with boards plugged into the wrong slots on X19. 2023-09-05 17:22:02 -06:00
Upstream Data
8c5503d002 version: bump version number. 2023-08-30 17:47:20 -06:00
Upstream Data
6d6f950c95 bug: add modified changed from [Issue 57](https://github.com/UpstreamData/pyasic/issues/57#issuecomment-1699984187) 2023-08-30 17:46:23 -06:00
UpstreamData
30745e54ba feature: add chip count for M30S+VE50 2023-08-30 11:18:25 -06:00
UpstreamData
c3fd94e79e version: bump version number. 2023-08-28 08:53:59 -06:00
UpstreamData
2924a8d67b feature: add more whatsminer error codes. 2023-08-28 08:53:27 -06:00
UpstreamData
9f4c4bb9cf feature: add exclude to get_data, and change data_to_get to include. 2023-08-28 08:32:29 -06:00
UpstreamData
3d6eebf06e bug: fix a bug with hostname gathering on some Avalons. 2023-08-28 08:31:54 -06:00
Upstream Data
b3d9b6ff7e version: bump version number. 2023-08-26 11:21:21 -06:00
Upstream Data
60facacc48 bug: fix a bug with bosminer commands. 2023-08-26 11:21:10 -06:00
Upstream Data
b8a6063838 version: bumnp version number. 2023-08-26 10:57:40 -06:00
Upstream Data
bcba2be524 bug: remove bad await calls to httpx response.json(). 2023-08-26 10:56:53 -06:00
UpstreamData
f7187d2017 bug: add chip count for M29V10. 2023-08-25 08:58:34 -06:00
Upstream Data
d91b7c4406 version: bump version number. 2023-08-07 17:02:50 -06:00
Upstream Data
248a7e6d69 bug: fix some WM models reporting https first and being identified as BOS+. 2023-08-07 17:02:26 -06:00
Upstream Data
4f2c3e772a version: bump version number. 2023-08-06 17:25:21 -06:00
Upstream Data
95f7146eef feature: add VNish pause/resume commands. 2023-08-06 17:24:36 -06:00
UpstreamData
9d5d19cc6b version: bump version number. 2023-07-27 20:45:42 -06:00
UpstreamData
cc38129571 bug: add back pwd for ssh connections. 2023-07-27 20:45:08 -06:00
UpstreamData
3dfd9f237d version: bump version number. 2023-07-27 20:18:58 -06:00
UpstreamData
f3fe478dbb feature: add support for S19J Pro No PIC. 2023-07-27 20:18:36 -06:00
UpstreamData
e10f32ae3d feature: speed up getting older antminer types with concurrent web and api requests. 2023-07-24 21:05:07 -06:00
UpstreamData
4e0924aa0e feature: add support for AML vnish miners. 2023-07-24 20:45:30 -06:00
UpstreamData
d0d3fd3117 bug: fix failed verification of SSL cert on whatsminer. 2023-07-24 20:19:00 -06:00
UpstreamData
4de950d8f4 feature: revert miner_factory to use httpx, as it now seems to be the same speed, and aiohttp doesnt support digest auth. 2023-07-24 13:09:30 -06:00
UpstreamData
03f2a1f9ba feature: optimize multicommand on new X19 models. 2023-07-24 11:34:16 -06:00
UpstreamData
2653db90e3 feature: optimize the way multicommand is handled on BTMiner. 2023-07-24 09:44:23 -06:00
UpstreamData
ddc8c53eb9 feature: add chip count for M50 VH60. 2023-07-13 10:59:27 -06:00
UpstreamData
eb5d1a24ea version: bump version number. 2023-07-12 08:56:59 -06:00
UpstreamData
6c0e80265b bug: revert X19 miner mode to string. 2023-07-12 08:56:23 -06:00
UpstreamData
ad3a4ae414 docs: update some bad code, and add references to new miner types and API types. 2023-07-11 11:18:28 -06:00
UpstreamData
3484d43510 version: bump version number. 2023-07-07 14:09:22 -06:00
UpstreamData
dd7e352391 bug: fix some addition issues with MinerData sums. 2023-07-07 14:09:08 -06:00
UpstreamData
a32b61fe5d version: bump version number. 2023-07-07 12:37:34 -06:00
UpstreamData
597a178009 feature: Update MinerData to use None. 2023-07-07 12:37:20 -06:00
Michael Schmid
409b2527f0 use None instead of -1 for temps and wattages (#55)
* use `None` instead of `-1` for temps and wattages
this way it's easier for other tools like HomeAssistant to understand if the temperature is really negative or not available

* also handle cases where we look for `-1`
2023-07-07 12:06:24 -06:00
UpstreamData
58234fcf7f version: bump version number. 2023-07-07 12:03:28 -06:00
UpstreamData
1bf863cca8 bug: set miner_mode to int instead of str to fix some issues with some X19 models. 2023-07-07 12:03:00 -06:00
UpstreamData
6482d04185 version: bump version number. 2023-07-07 11:56:41 -06:00
UpstreamData
3b58b11501 bug: remove 0 frequency level when setting sleep mode on X19, as it seems to bug some types. 2023-07-07 11:56:06 -06:00
UpstreamData
7485b8ef77 version: bump version number. 2023-07-04 10:04:20 -06:00
UpstreamData
d2bea227db bug: fix an issue with a possible SSH command in BOS+. 2023-07-04 10:01:24 -06:00
UpstreamData
1b7afaaf7e version: bump version number. 2023-06-30 08:49:31 -06:00
UpstreamData
96898d639c bug: fix some handling errors with graphql. 2023-06-30 08:49:08 -06:00
UpstreamData
eb439f4dcf version: bump version number. 2023-06-30 08:44:01 -06:00
UpstreamData
69f4349393 feature: create pwd and username property in miner object that sets web and api passwords and usernames. 2023-06-30 08:43:30 -06:00
UpstreamData
e371bb577c version: bump version number. 2023-06-29 18:10:35 -06:00
UpstreamData
2500ec3869 bug: fix possible None return from some bosminer webcommands. 2023-06-29 18:10:10 -06:00
UpstreamData
5be3187eec version: bump version number. 2023-06-29 16:51:29 -06:00
UpstreamData
be1e9127b0 bug: fix weird pool info when multiple groups are defined. 2023-06-29 16:51:15 -06:00
UpstreamData
13572c4770 version: bump version number. 2023-06-29 16:48:09 -06:00
UpstreamData
08fa3961fe bug: fix bosminer on X19 not reporting pause mode correctly. 2023-06-29 16:47:27 -06:00
UpstreamData
b5d2809e9c format: remove random print statements. 2023-06-29 15:53:53 -06:00
UpstreamData
aa538d3079 docs: add miner docs generation file. 2023-06-28 11:21:19 -06:00
UpstreamData
e1500bb75c docs: update docs. 2023-06-28 11:20:22 -06:00
UpstreamData
7f00a65598 version: bump version number. 2023-06-27 16:23:46 -06:00
UpstreamData
64c473a7d4 bug: fix improper stats key when getting uptime. 2023-06-27 16:23:21 -06:00
UpstreamData
96d9fe8e6c version: bump version number. 2023-06-27 14:56:11 -06:00
UpstreamData
0b27400d27 feature: add set_static_ip and set_dhcp for bosminer. 2023-06-27 14:55:05 -06:00
UpstreamData
666b9dfe94 version: bump version number. 2023-06-27 09:36:06 -06:00
UpstreamData
df3a080c9d feature: add uptime check for some miners, and fix a bug with get_mac on BOS. 2023-06-27 09:35:07 -06:00
UpstreamData
bf3bd7c2b9 feature: add basic support for LuxOS 2023-06-26 15:35:52 -06:00
UpstreamData
37fd60b462 version: bump version number. 2023-06-26 09:53:21 -06:00
UpstreamData
2245904740 feature: remove ssh references when getting MAC on bosminer. 2023-06-26 09:52:30 -06:00
UpstreamData
7b1b23016e feature: add support for VNish S19 No PIC. 2023-06-26 08:20:45 -06:00
UpstreamData
b5fcd62e23 version: bump version number. 2023-06-23 14:50:14 -06:00
UpstreamData
9057cde274 docs: add is_mining to docs. 2023-06-23 14:49:38 -06:00
UpstreamData
f6d35888fe feature: add is_mining to MinerData and get_data. 2023-06-23 14:47:12 -06:00
UpstreamData
f2abe9fd9e feature: add is_mining to all miner types. 2023-06-23 14:38:38 -06:00
UpstreamData
7d1a702804 feature: make is_mining abstract method. 2023-06-23 14:34:44 -06:00
UpstreamData
65d1695ce4 bug: fix is_mining for some miners. 2023-06-23 14:33:32 -06:00
UpstreamData
65fd66b8bf feature: add is_mining for antminer. 2023-06-23 14:08:54 -06:00
UpstreamData
5db52c46f3 feature: add is_mining for btminer, and fix some minor bugs. 2023-06-23 13:38:45 -06:00
UpstreamData
d06cb19da3 feature: add is_mining for bosminers. 2023-06-23 12:39:00 -06:00
UpstreamData
4530d086da bug: fix some cases where web command data can fail. 2023-06-23 11:23:20 -06:00
UpstreamData
0bd679f259 version: bump version number. 2023-06-22 15:07:25 -06:00
UpstreamData
1ce5bd0566 Update miner data and fix various bugs related to the fix. (#47)
* feature: fix influxdb data.

* bug: fix an issue with some avalon stats parsing.

* bug: add chip count for 1166 Pro.

* bug: fix some issues with bosminer scanning and some data bugs.

* bug: remove print statement.

* bug: fix failed data gathering multicommand via graphql.

* feature: add partial support for M50S+VK20

* version: bump version number.

* bug: add chip count for M50S+VK20.

* version: bump version number.

* bug: attempt to fix offset check issue on BOS+.

* bug: fix NoneType subscription on BOS+.

* bug: add support for Vnish S17+.

* bug: remove web references for Avalons.

* bug: add support for VNish S17Pro.

* bug: Try secondary identification method for antminers.

* feature: fix a bunch of functionality for avalonminers.

* bug: fix avalonminer fan speed being set as str.

* bug: fix fans speeds being represented as strings.

* bug: fix some get_fan formatting.

* docs: update supported miners list, and fix A10X model name.

* docs: update MinerData docstrings.

* docs: update factory documentation.
2023-06-22 15:06:30 -06:00
UpstreamData
67c3d05ac3 version: bump version number. 2023-06-20 12:11:43 -06:00
UpstreamData
c691868e9b bug: add chip count for M50S+VK20. 2023-06-20 12:11:05 -06:00
UpstreamData
f5e15b4046 version: bump version number. 2023-06-19 16:10:34 -06:00
UpstreamData
e14df696ee feature: add partial support for M50S+VK20 2023-06-19 16:10:00 -06:00
UpstreamData
ce5dfad850 version: bump version number. 2023-06-12 13:04:11 -06:00
UpstreamData
5cb45390be bug: add chip count for avalon 1246. 2023-06-12 13:03:41 -06:00
UpstreamData
b5216a24a6 version: bump version number. 2023-06-12 12:47:58 -06:00
UpstreamData
dd175ff3a2 bug: fix an issue with 2020 versions of Braiins OS not identifying correctly. 2023-06-12 12:47:38 -06:00
UpstreamData
9b504a3157 version: bump version number. 2023-06-12 12:42:16 -06:00
UpstreamData
0bc3bf20ee bug: fix possible missing models. 2023-06-12 12:41:50 -06:00
177 changed files with 11542 additions and 3827 deletions

View File

@@ -1,5 +1,5 @@
# pyasic
*A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH.*
*A simplified and standardized interface for Bitcoin ASICs.*
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![pypi](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)

View File

@@ -15,6 +15,7 @@ Use these instead -
#### [BOSMiner API][pyasic.API.bosminer.BOSMinerAPI]
#### [BTMiner API][pyasic.API.btminer.BTMinerAPI]
#### [CGMiner API][pyasic.API.cgminer.CGMinerAPI]
#### [LUXMiner API][pyasic.API.luxminer.LUXMinerAPI]
#### [Unknown API][pyasic.API.unknown.UnknownAPI]
<br>

7
docs/API/luxminer.md Normal file
View File

@@ -0,0 +1,7 @@
# pyasic
## LUXMinerAPI
::: pyasic.API.luxminer.LUXMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -6,19 +6,3 @@
options:
show_root_heading: false
heading_level: 4
## Pool Groups
::: pyasic.config._PoolGroup
handler: python
options:
show_root_heading: false
heading_level: 4
## Pools
::: pyasic.config._Pool
handler: python
options:
show_root_heading: false
heading_level: 4

165
docs/generate_miners.py Normal file
View File

@@ -0,0 +1,165 @@
import asyncio
import importlib
import os
import warnings
from pyasic.miners.miner_factory import MINER_CLASSES, MinerTypes
warnings.filterwarnings("ignore")
def path(cls):
module = importlib.import_module(cls.__module__)
return module.__name__ + "." + cls.__name__
def make(cls):
p = path(cls)
return p.split(".")[2]
def model_type(cls):
p = path(cls)
return p.split(".")[4]
def backend_str(backend: MinerTypes) -> str:
match backend:
case MinerTypes.ANTMINER:
return "Stock Firmware Antminers"
case MinerTypes.AVALONMINER:
return "Stock Firmware Avalonminers"
case MinerTypes.VNISH:
return "Vnish Firmware Miners"
case MinerTypes.EPIC:
return "ePIC Firmware Miners"
case MinerTypes.BRAIINS_OS:
return "BOS+ Firmware Miners"
case MinerTypes.HIVEON:
return "HiveOS Firmware Miners"
case MinerTypes.INNOSILICON:
return "Stock Firmware Innosilicons"
case MinerTypes.WHATSMINER:
return "Stock Firmware Whatsminers"
case MinerTypes.GOLDSHELL:
return "Stock Firmware Goldshells"
case MinerTypes.LUX_OS:
return "LuxOS Firmware Miners"
def create_url_str(mtype: str):
return (
mtype.lower()
.replace(" ", "-")
.replace("(", "")
.replace(")", "")
.replace("+", "_1")
)
HEADER_FORMAT = "# pyasic\n## {} Models\n\n"
MINER_HEADER_FORMAT = "## {}\n"
DATA_FORMAT = """::: {}
handler: python
options:
show_root_heading: false
heading_level: 4
"""
SUPPORTED_TYPES_HEADER = """# pyasic
## Supported Miners
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
##### pyasic currently supports the following miners and subtypes:
<style>
details {
margin:0px;
padding-top:0px;
padding-bottom:0px;
}
</style>
"""
BACKEND_TYPE_HEADER = """
<details>
<summary>{}:</summary>
<ul>"""
MINER_TYPE_HEADER = """
<details>
<summary>{} Series:</summary>
<ul>"""
MINER_DETAILS = """
<li><a href="../{}/{}#{}">{}</a></li>"""
MINER_TYPE_CLOSER = """
</ul>
</details>"""
BACKEND_TYPE_CLOSER = """
</ul>
</details>"""
m_data = {}
for m in MINER_CLASSES:
for t in MINER_CLASSES[m]:
if t is not None:
miner = MINER_CLASSES[m][t]
if make(miner) not in m_data:
m_data[make(miner)] = {}
if model_type(miner) not in m_data[make(miner)]:
m_data[make(miner)][model_type(miner)] = []
m_data[make(miner)][model_type(miner)].append(miner)
async def create_directory_structure(directory, data):
if not os.path.exists(directory):
os.makedirs(directory)
for key, value in data.items():
subdirectory = os.path.join(directory, key)
if isinstance(value, dict):
await create_directory_structure(subdirectory, value)
elif isinstance(value, list):
file_path = os.path.join(subdirectory + ".md")
with open(file_path, "w") as file:
file.write(HEADER_FORMAT.format(key))
for item in value:
header = await item("1.1.1.1").get_model()
file.write(MINER_HEADER_FORMAT.format(header))
file.write(DATA_FORMAT.format(path(item)))
async def create_supported_types(directory):
with open(os.path.join(directory, "supported_types.md"), "w") as file:
file.write(SUPPORTED_TYPES_HEADER)
for mback in MINER_CLASSES:
backend_types = {}
file.write(BACKEND_TYPE_HEADER.format(backend_str(mback)))
for mtype in MINER_CLASSES[mback]:
if mtype is None:
continue
m = MINER_CLASSES[mback][mtype]
if model_type(m) not in backend_types:
backend_types[model_type(m)] = []
backend_types[model_type(m)].append(m)
for mtype in backend_types:
file.write(MINER_TYPE_HEADER.format(mtype))
for minstance in backend_types[mtype]:
model = await minstance("1.1.1.1").get_model()
file.write(
MINER_DETAILS.format(
make(minstance), mtype, create_url_str(model), model
)
)
file.write(MINER_TYPE_CLOSER)
file.write(BACKEND_TYPE_CLOSER)
root_directory = os.path.join(os.getcwd(), "miners")
asyncio.run(create_directory_structure(root_directory, m_data))
asyncio.run(create_supported_types(root_directory))

View File

@@ -8,18 +8,21 @@
[![GitHub](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
---
## Intro
Welcome to pyasic! Pyasic uses an asynchronous method of communicating with asic miners on your network, which makes it super fast.
---
Welcome to `pyasic`! `pyasic` uses an asynchronous method of communicating with ASIC miners on your network, which makes it super fast.
[Supported Miner Types](miners/supported_types.md)
[Click here to view supported miner types](miners/supported_types.md)
Getting started with pyasic is easy. First, find your miner (or miners) on the network by scanning for them or getting the correct class automatically for them if you know the IP.
---
## Getting started
---
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.
##### 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()`][pyasic.network.MinerNetwork.scan] 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
@@ -28,22 +31,21 @@ 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
network = MinerNetwork.from_subnet("192.168.1.50/24") # 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()
miners = await network.scan()
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, or an abstraction of its functionality, [`get_miner()`][pyasic.miners.miner_factory.MinerFactory.get_miner].
The function [`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.
---
##### 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, or an abstraction of its functionality, [`get_miner()`][pyasic.miners.get_miner].
The function [`get_miner()`][pyasic.miners.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 import get_miner # handles miner creation
@@ -58,6 +60,8 @@ async def get_miners(): # define async scan function to allow awaiting
print(miner_1, miner_2)
# can also gather these, since they are async
# gathering them will get them both at the same time
# this makes it much faster to get a lot of miners at a time
tasks = [get_miner("192.168.1.75"), get_miner("192.168.1.76")]
miners = await asyncio.gather(*tasks)
print(miners)
@@ -67,27 +71,30 @@ 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()`.
---
## Data gathering
---
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 an 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].
##### One miner
```python
import asyncio
from pyasic.miners.miner_factory import MinerFactory
from pyasic import get_miner
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
miner = await get_miner("192.168.1.75")
if miner is not None:
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())
```
---
##### Multiple miners
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
@@ -95,8 +102,8 @@ 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()
network = MinerNetwork.from_subnet("192.168.1.50/24")
miners = await network.scan()
# 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])
@@ -108,152 +115,60 @@ if __name__ == "__main__":
asyncio.run(gather_miner_data())
```
<br>
## Controlling miners via pyasic
Every miner class in pyasic must implement all the control functions defined in [`BaseMiner`][pyasic.miners.BaseMiner].
---
## Miner control
---
`pyasic` exposes a standard interface for each miner using control functions.
Every miner class in `pyasic` must implement all the control functions defined in [`BaseMiner`][pyasic.miners.BaseMiner].
These functions are
[`check_light`](#check-light),
[`fault_light_off`](#fault-light-off),
[`fault_light_on`](#fault-light-on),
[`get_config`](#get-config),
[`get_data`](#get-data),
[`get_errors`](#get-errors),
[`get_hostname`](#get-hostname),
[`get_model`](#get-model),
[`reboot`](#reboot),
[`restart_backend`](#restart-backend),
[`stop_mining`](#stop-mining),
[`resume_mining`](#resume-mining),
[`send_config`](#send-config), and
[`set_power_limit`](#set-power-limit).
[`check_light`][pyasic.miners.BaseMiner.check_light],
[`fault_light_off`][pyasic.miners.BaseMiner.fault_light_off],
[`fault_light_on`][pyasic.miners.BaseMiner.fault_light_on],
[`get_config`][pyasic.miners.BaseMiner.get_config],
[`get_data`][pyasic.miners.BaseMiner.get_data],
[`get_errors`][pyasic.miners.BaseMiner.get_errors],
[`get_hostname`][pyasic.miners.BaseMiner.get_hostname],
[`get_model`][pyasic.miners.BaseMiner.get_model],
[`reboot`][pyasic.miners.BaseMiner.reboot],
[`restart_backend`][pyasic.miners.BaseMiner.restart_backend],
[`stop_mining`][pyasic.miners.BaseMiner.stop_mining],
[`resume_mining`][pyasic.miners.BaseMiner.resume_mining],
[`is_mining`][pyasic.miners.BaseMiner.is_mining],
[`send_config`][pyasic.miners.BaseMiner.send_config], and
[`set_power_limit`][pyasic.miners.BaseMiner.set_power_limit].
<br>
##### Usage
```python
import asyncio
from pyasic import get_miner
### Check Light
::: pyasic.miners.BaseMiner.check_light
handler: python
options:
heading_level: 4
<br>
async def set_fault_light():
miner = await get_miner("192.168.1.20")
### Fault Light Off
::: pyasic.miners.BaseMiner.fault_light_off
handler: python
options:
heading_level: 4
# call control function
await miner.fault_light_on()
<br>
if __name__ == "__main__":
asyncio.run(set_fault_light())
```
### Fault Light On
::: pyasic.miners.BaseMiner.fault_light_on
handler: python
options:
heading_level: 4
---
## Helper dataclasses
---
<br>
##### [`MinerConfig`][pyasic.config.MinerConfig] and [`MinerData`][pyasic.data.MinerData]
### Get Config
::: pyasic.miners.BaseMiner.get_config
handler: python
options:
heading_level: 4
`pyasic` implements a few dataclasses as helpers to make data return types consistent across different miners and miner APIs. The different fields of these dataclasses can all be viewed with the classmethod `cls.fields()`.
<br>
---
### Get Data
::: pyasic.miners.BaseMiner.get_data
handler: python
options:
heading_level: 4
<br>
### Get Errors
::: pyasic.miners.BaseMiner.get_errors
handler: python
options:
heading_level: 4
<br>
### Get Hostname
::: pyasic.miners.BaseMiner.get_hostname
handler: python
options:
heading_level: 4
<br>
### Get Model
::: pyasic.miners.BaseMiner.get_model
handler: python
options:
heading_level: 4
<br>
### Reboot
::: pyasic.miners.BaseMiner.reboot
handler: python
options:
heading_level: 4
<br>
### Restart Backend
::: pyasic.miners.BaseMiner.restart_backend
handler: python
options:
heading_level: 4
<br>
### Stop Mining
::: pyasic.miners.BaseMiner.stop_mining
handler: python
options:
heading_level: 4
<br>
### Resume Mining
::: pyasic.miners.BaseMiner.resume_mining
handler: python
options:
heading_level: 4
<br>
### Send Config
::: pyasic.miners.BaseMiner.send_config
handler: python
options:
heading_level: 4
<br>
### Set Power Limit
::: pyasic.miners.BaseMiner.set_power_limit
handler: python
options:
heading_level: 4
<br>
## [`MinerConfig`][pyasic.config.MinerConfig] and [`MinerData`][pyasic.data.MinerData]
Pyasic implements a few dataclasses as helpers to make data return types consistent across different miners and miner APIs. The different fields of these dataclasses can all be viewed with the classmethod `cls.fields()`.
<br>
### [`MinerData`][pyasic.data.MinerData]
##### [`MinerData`][pyasic.data.MinerData]
[`MinerData`][pyasic.data.MinerData] is a return from the [`get_data()`](#get-data) function, and is used to have a consistent dataset across all returns.
You can call [`MinerData.asdict()`][pyasic.data.MinerData.asdict] to get the dataclass as a dictionary, and there are many other helper functions contained in the class to convert to different data formats.
You can call [`MinerData.as_dict()`][pyasic.data.MinerData.as_dict] to get the dataclass as a dictionary, and there are many other helper functions contained in the class to convert to different data formats.
[`MinerData`][pyasic.data.MinerData] instances can also be added to each other to combine their data and can be divided by a number to divide all their data, allowing you to get average data from many miners by doing -
```python
@@ -268,13 +183,64 @@ list_of_miner_data = [d1, d2]
average_data = sum(list_of_miner_data, start=MinerData("0.0.0.0"))/len(list_of_miner_data)
```
---
<br>
##### [`MinerConfig`][pyasic.config.MinerConfig]
### [`MinerConfig`][pyasic.config.MinerConfig]
[`MinerConfig`][pyasic.config.MinerConfig] is pyasic's way to represent a configuration file from a miner.
It is the return from [`get_config()`](#get-config).
[`MinerConfig`][pyasic.config.MinerConfig] is `pyasic`'s way to represent a configuration file from a miner.
It is designed to unionize the configuration of all supported miner types, and is the return from [`get_config()`](#get-config).
Each miner has a unique way to convert the [`MinerConfig`][pyasic.config.MinerConfig] to their specific type, there are helper functions in the class.
In most cases these helper functions should not be used, as [`send_config()`](#send-config) takes a [`MinerConfig`][pyasic.config.MinerConfig] and will do the conversion to the right type for you.
You can use the [`MinerConfig`][pyasic.config.MinerConfig] as follows:
```python
import asyncio
from pyasic import get_miner
async def set_fault_light():
miner = await get_miner("192.168.1.20")
# get config
cfg = await miner.get_config()
# send config
await miner.send_config(cfg)
if __name__ == "__main__":
asyncio.run(set_fault_light())
```
---
## Settings
---
`pyasic` has settings designed to make using large groups of miners easier. You can set the default password for all types of miners using the `pyasic.settings` module, used as follows:
```python
from pyasic import settings
settings.update("default_antminer_password", "my_pwd")
```
##### Default values:
```
"network_ping_retries": 1,
"network_ping_timeout": 3,
"network_scan_threads": 300,
"factory_get_retries": 1,
"factory_get_timeout": 3,
"get_data_retries": 1,
"api_function_timeout": 5,
"default_whatsminer_password": "admin",
"default_innosilicon_password": "admin",
"default_antminer_password": "root",
"default_bosminer_password": "root",
"default_vnish_password": "admin",
"default_goldshell_password": "123456789",
# ADVANCED
# Only use this if you know what you are doing
"socket_linger_time": 1000,
```

View File

@@ -1,11 +1,10 @@
# pyasic
## X15 Models
## Z15
::: pyasic.miners.zec.antminer.cgminer.X15.Z15.CGMinerZ15
::: pyasic.miners.antminer.cgminer.X15.Z15.CGMinerZ15
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -2,116 +2,114 @@
## X17 Models
## S17
::: pyasic.miners.btc.antminer.bmminer.X17.S17.BMMinerS17
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17
handler: python
options:
show_root_heading: false
heading_level: 4
## S17+
::: pyasic.miners.btc.antminer.bmminer.X17.S17_Plus.BMMinerS17Plus
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## S17 Pro
::: pyasic.miners.btc.antminer.bmminer.X17.S17_Pro.BMMinerS17Pro
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S17e
::: pyasic.miners.btc.antminer.bmminer.X17.S17e.BMMinerS17e
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17e
handler: python
options:
show_root_heading: false
heading_level: 4
## T17
::: pyasic.miners.btc.antminer.bmminer.X17.T17.BMMinerT17
::: pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17
handler: python
options:
show_root_heading: false
heading_level: 4
## T17+
::: pyasic.miners.btc.antminer.bmminer.X17.T17_Plus.BMMinerT17Plus
::: pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## T17e
::: pyasic.miners.btc.antminer.bmminer.X17.T17e.BMMinerT17e
::: pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17e
handler: python
options:
show_root_heading: false
heading_level: 4
## S17 (BOS)
::: pyasic.miners.btc.antminer.bosminer.X17.S17.BOSMinerS17
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17
handler: python
options:
show_root_heading: false
heading_level: 4
## S17+ (BOS)
::: pyasic.miners.btc.antminer.bosminer.X17.S17_Plus.BOSMinerS17Plus
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## S17 Pro (BOS)
::: pyasic.miners.btc.antminer.bosminer.X17.S17_Pro.BOSMinerS17Pro
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S17e (BOS)
::: pyasic.miners.btc.antminer.bosminer.X17.S17e.BOSMinerS17e
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17e
handler: python
options:
show_root_heading: false
heading_level: 4
## T17 (BOS)
::: pyasic.miners.btc.antminer.bosminer.X17.T17.BOSMinerT17
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17
handler: python
options:
show_root_heading: false
heading_level: 4
## T17+ (BOS)
::: pyasic.miners.btc.antminer.bosminer.X17.T17_Plus.BOSMinerT17Plus
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## T17e (BOS)
::: pyasic.miners.btc.antminer.bosminer.X17.T17e.BOSMinerT17e
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17e
handler: python
options:
show_root_heading: false
heading_level: 4
## S17+ (VNish)
::: pyasic.miners.antminer.vnish.X17.S17.VNishS17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## S17 Pro (VNish)
::: pyasic.miners.antminer.vnish.X17.S17.VNishS17Pro
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -2,109 +2,233 @@
## X19 Models
## S19
::: pyasic.miners.btc.antminer.bmminer.X19.S19.BMMinerS19
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19L
::: pyasic.miners.btc.antminer.bmminer.X19.S19L.BMMinerS19L
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19L
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro
::: pyasic.miners.btc.antminer.bmminer.X19.S19_Pro.BMMinerS19Pro
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19a
::: pyasic.miners.btc.antminer.bmminer.X19.S19a.BMMinerS19a
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19j
handler: python
options:
show_root_heading: false
heading_level: 4
::: pyasic.miners.btc.antminer.bmminer.X19.S19j.BMMinerS19j
## S19i
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19i
handler: python
options:
show_root_heading: false
heading_level: 4
## S19+
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j No PIC
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19jNoPIC
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro+
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19ProPlus
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro
::: pyasic.miners.btc.antminer.bmminer.X19.S19j_Pro.BMMinerS19jPro
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 XP
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19XP
handler: python
options:
show_root_heading: false
heading_level: 4
::: pyasic.miners.btc.antminer.bmminer.X19.S19_XP.BMMinerS19XP
## S19a
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19a
handler: python
options:
show_root_heading: false
heading_level: 4
## S19a Pro
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19aPro
handler: python
options:
show_root_heading: false
heading_level: 4
## T19
::: pyasic.miners.btc.antminer.bmminer.X19.T19.BMMinerT19
::: pyasic.miners.antminer.bmminer.X19.T19.BMMinerT19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 (BOS)
::: pyasic.miners.btc.antminer.bosminer.X19.S19.BOSMinerS19
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro (BOS)
::: pyasic.miners.btc.antminer.bosminer.X19.S19_Pro.BOSMinerS19Pro
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19j
handler: python
options:
show_root_heading: false
heading_level: 4
::: pyasic.miners.btc.antminer.bosminer.X19.S19j.BOSMinerS19j
## S19j No PIC (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19jNoPIC
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
::: pyasic.miners.btc.antminer.bosminer.X19.S19j_Pro.BOSMinerS19jPro
## S19j Pro (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## T19 (BOS)
::: pyasic.miners.btc.antminer.bosminer.X19.T19.BOSMinerT19
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 No PIC (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19NoPIC
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19j
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19a (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19a
handler: python
options:
show_root_heading: false
heading_level: 4
## S19a Pro (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19aPro
handler: python
options:
show_root_heading: false
heading_level: 4
## T19 (VNish)
::: pyasic.miners.antminer.vnish.X19.T19.VNishT19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19j
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 XP (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19XP
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,11 +1,31 @@
# pyasic
## X3 Models
## HS3
::: pyasic.miners.hns.antminer.cgminer.X3.HS3.CGMinerHS3
## D3
::: pyasic.miners.antminer.cgminer.X3.D3.CGMinerD3
handler: python
options:
show_root_heading: false
heading_level: 4
## HS3
::: pyasic.miners.antminer.bmminer.X3.HS3.BMMinerHS3
handler: python
options:
show_root_heading: false
heading_level: 4
## L3+
::: pyasic.miners.antminer.bmminer.X3.L3.BMMinerL3Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## L3+ (VNish)
::: pyasic.miners.antminer.vnish.X3.L3.VnishL3Plus
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,11 +1,10 @@
# pyasic
## X5 Models
## DR5
::: pyasic.miners.dcr.antminer.cgminer.X5.DR5.CGMinerDR5
::: pyasic.miners.antminer.cgminer.X5.DR5.CGMinerDR5
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,11 +1,10 @@
# pyasic
## X7 Models
## L7
::: pyasic.miners.ltc.antminer.bmminer.X7.L7.BMMinerL7
::: pyasic.miners.antminer.bmminer.X7.L7.BMMinerL7
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,44 +1,59 @@
# pyasic
## X9 Models
## X9 (BOS)
::: pyasic.miners.btc.antminer.bosminer.X9.S9.BOSMinerS9
## E9Pro
::: pyasic.miners.antminer.bmminer.X9.E9.BMMinerE9Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S9
::: pyasic.miners.btc.antminer.bmminer.X9.S9.BMMinerS9
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9
handler: python
options:
show_root_heading: false
heading_level: 4
## S9i
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9i
handler: python
options:
show_root_heading: false
heading_level: 4
::: pyasic.miners.btc.antminer.bmminer.X9.S9i.BMMinerS9i
## S9j
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9j
handler: python
options:
show_root_heading: false
heading_level: 4
## T9
::: pyasic.miners.btc.antminer.bmminer.X9.T9.BMMinerT9
::: pyasic.miners.antminer.bmminer.X9.T9.BMMinerT9
handler: python
options:
show_root_heading: false
heading_level: 4
## E9 Pro
::: pyasic.miners.etc.antminer.cgminer.X9.E9_Pro.CGMinerE9Pro
## S9 (BOS)
::: pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9
handler: python
options:
show_root_heading: false
heading_level: 4
## T9 (Hiveon)
::: pyasic.miners.antminer.hiveon.X9.T9.HiveonT9
handler: python
options:
show_root_heading: false
heading_level: 4
## S9 (LuxOS)
::: pyasic.miners.antminer.luxos.X9.S9.LUXMinerS9
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,26 +1,24 @@
# pyasic
## A10X Models
## A1026
::: pyasic.miners.btc.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026
## Avalon 1026
::: pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026
handler: python
options:
show_root_heading: false
heading_level: 4
## A1047
::: pyasic.miners.btc.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047
## Avalon 1047
::: pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047
handler: python
options:
show_root_heading: false
heading_level: 4
## A1066
::: pyasic.miners.btc.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066
## Avalon 1066
::: pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,10 @@
# pyasic
## A11X Models
## Avalon 1166 Pro
::: pyasic.miners.avalonminer.cgminer.A11X.A1166.CGMinerAvalon1166Pro
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,10 @@
# pyasic
## A12X Models
## Avalon 1246
::: pyasic.miners.avalonminer.cgminer.A12X.A1246.CGMinerAvalon1246
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,26 +1,24 @@
# pyasic
## A7X Models
## A721
::: pyasic.miners.btc.avalonminer.cgminer.A7X.A721.CGMinerAvalon721
## Avalon 721
::: pyasic.miners.avalonminer.cgminer.A7X.A721.CGMinerAvalon721
handler: python
options:
show_root_heading: false
heading_level: 4
## A741
::: pyasic.miners.btc.avalonminer.cgminer.A7X.A741.CGMinerAvalon741
## Avalon 741
::: pyasic.miners.avalonminer.cgminer.A7X.A741.CGMinerAvalon741
handler: python
options:
show_root_heading: false
heading_level: 4
## A761
::: pyasic.miners.btc.avalonminer.cgminer.A7X.A761.CGMinerAvalon761
## Avalon 761
::: pyasic.miners.avalonminer.cgminer.A7X.A761.CGMinerAvalon761
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,26 +1,24 @@
# pyasic
## A8X Models
## A821
::: pyasic.miners.btc.avalonminer.cgminer.A8X.A821.CGMinerAvalon821
## Avalon 821
::: pyasic.miners.avalonminer.cgminer.A8X.A821.CGMinerAvalon821
handler: python
options:
show_root_heading: false
heading_level: 4
## A841
::: pyasic.miners.btc.avalonminer.cgminer.A8X.A841.CGMinerAvalon841
## Avalon 841
::: pyasic.miners.avalonminer.cgminer.A8X.A841.CGMinerAvalon841
handler: python
options:
show_root_heading: false
heading_level: 4
## A851
::: pyasic.miners.btc.avalonminer.cgminer.A8X.A851.CGMinerAvalon851
## Avalon 851
::: pyasic.miners.avalonminer.cgminer.A8X.A851.CGMinerAvalon851
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,10 +1,10 @@
# pyasic
## A9X Models
## A921
::: pyasic.miners.btc.avalonminer.cgminer.A9X.A921.CGMinerAvalon921
## Avalon 921
::: pyasic.miners.avalonminer.cgminer.A9X.A921.CGMinerAvalon921
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,9 +1,7 @@
# pyasic
## CKX Models
## ePIC Backend
## CK5
::: pyasic.miners.ckb.goldshell.bfgminer.CKX.CK5.BFGMinerCK5
::: pyasic.miners.backends.epic.ePIC
handler: python
options:
show_root_heading: false

View File

@@ -1,9 +1,7 @@
# pyasic
## HSX Models
## LUXMiner Backend
## HS5
::: pyasic.miners.hns.goldshell.bfgminer.HSX.HS5.BFGMinerHS5
::: pyasic.miners.backends.luxminer.LUXMiner
handler: python
options:
show_root_heading: false

View File

@@ -0,0 +1,8 @@
# pyasic
## VNish Backend
::: pyasic.miners.backends.vnish.VNish
handler: python
options:
show_root_heading: false
heading_level: 4

91
docs/miners/functions.md Normal file
View File

@@ -0,0 +1,91 @@
## Control functionality
### Check Light
::: pyasic.miners.BaseMiner.check_light
handler: python
options:
heading_level: 4
### Fault Light Off
::: pyasic.miners.BaseMiner.fault_light_off
handler: python
options:
heading_level: 4
### Fault Light On
::: pyasic.miners.BaseMiner.fault_light_on
handler: python
options:
heading_level: 4
### Get Config
::: pyasic.miners.BaseMiner.get_config
handler: python
options:
heading_level: 4
### Get Data
::: pyasic.miners.BaseMiner.get_data
handler: python
options:
heading_level: 4
### Get Errors
::: pyasic.miners.BaseMiner.get_errors
handler: python
options:
heading_level: 4
### Get Hostname
::: pyasic.miners.BaseMiner.get_hostname
handler: python
options:
heading_level: 4
### Get Model
::: pyasic.miners.BaseMiner.get_model
handler: python
options:
heading_level: 4
### Reboot
::: pyasic.miners.BaseMiner.reboot
handler: python
options:
heading_level: 4
### Restart Backend
::: pyasic.miners.BaseMiner.restart_backend
handler: python
options:
heading_level: 4
### Stop Mining
::: pyasic.miners.BaseMiner.stop_mining
handler: python
options:
heading_level: 4
### Resume Mining
::: pyasic.miners.BaseMiner.resume_mining
handler: python
options:
heading_level: 4
### Is Mining
::: pyasic.miners.BaseMiner.is_mining
handler: python
options:
heading_level: 4
### Send Config
::: pyasic.miners.BaseMiner.send_config
handler: python
options:
heading_level: 4
### Set Power Limit
::: pyasic.miners.BaseMiner.set_power_limit
handler: python
options:
heading_level: 4

View File

@@ -1,19 +0,0 @@
# pyasic
## KDX Models
## KD5
::: pyasic.miners.kda.goldshell.bfgminer.KDX.KD5.BFGMinerKD5
handler: python
options:
show_root_heading: false
heading_level: 4
## KD Max
::: pyasic.miners.kda.goldshell.bfgminer.KDX.KDMax.BFGMinerKDMax
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,24 @@
# pyasic
## X5 Models
## CK5
::: pyasic.miners.goldshell.bfgminer.X5.CK5.BFGMinerCK5
handler: python
options:
show_root_heading: false
heading_level: 4
## HS5
::: pyasic.miners.goldshell.bfgminer.X5.HS5.BFGMinerHS5
handler: python
options:
show_root_heading: false
heading_level: 4
## KD5
::: pyasic.miners.goldshell.bfgminer.X5.KD5.BFGMinerKD5
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,10 @@
# pyasic
## XMax Models
## KD Max
::: pyasic.miners.goldshell.bfgminer.XMax.KDMax.BFGMinerKDMax
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -2,9 +2,9 @@
## A10X Models
## A10X
::: pyasic.miners.etc.innosilicon.cgminer.A10X.A10X.CGMinerA10X
::: pyasic.miners.innosilicon.cgminer.A10X.A10X.CGMinerA10X
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -2,9 +2,9 @@
## T3X Models
## T3H+
::: pyasic.miners.btc.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus
::: pyasic.miners.innosilicon.cgminer.T3X.T3H.CGMinerT3HPlus
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,6 +1,14 @@
# pyasic
## Miner Factory
[`MinerFactory`][pyasic.miners.miner_factory.MinerFactory] is the way to create miner types in `pyasic`. The most important method is [`get_miner()`][pyasic.get_miner], which is mapped to [`pyasic.get_miner()`][pyasic.get_miner], and should be used from there.
The instance used for [`pyasic.get_miner()`][pyasic.get_miner] is `pyasic.miner_factory`.
[`MinerFactory`][pyasic.MinerFactory] also keeps a cache, which can be cleared if needed with `pyasic.miner_factory.clear_cached_miners()`.
Finally, there is functionality to get multiple miners without using `asyncio.gather()` explicitly. Use `pyasic.miner_factory.get_multiple_miners()` with a list of IPs as strings to get a list of miner instances. You can also get multiple miners with an `AsyncGenerator` by using `pyasic.miner_factory.get_miner_generator()`.
::: pyasic.miners.miner_factory.MinerFactory
handler: python
options:
@@ -8,6 +16,14 @@
heading_level: 4
<br>
## Get Miner
::: pyasic.miners.get_miner
handler: python
options:
show_root_heading: false
heading_level: 4
<br>
## AnyMiner
::: pyasic.miners.miner_factory.AnyMiner
handler: python

View File

@@ -11,37 +11,74 @@ details {
padding-bottom:0px;
}
</style>
<details style="margin:0px; padding-top:0px; padding-bottom:0px;">
<summary>Braiins OS+ Devices:</summary>
<details>
<summary>Stock Firmware Antminers:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<summary>X3 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-bos">S19</a></li>
<li><a href="../antminer/X19#s19-pro-bos">S19 Pro</a></li>
<li><a href="../antminer/X19#s19j-bos">S19j</a></li>
<li><a href="../antminer/X19#s19j-pro-bos">S19j Pro</a></li>
<li><a href="../antminer/X19#t19-bos">T19</a></li>
<li><a href="../antminer/X3#d3">D3</a></li>
<li><a href="../antminer/X3#hs3">HS3</a></li>
<li><a href="../antminer/X3#l3_1">L3+</a></li>
</ul>
</details>
<details>
<summary>X17 Series:</summary>
<summary>X5 Series:</summary>
<ul>
<li><a href="../antminer/X17#s17-bos">S17</a></li>
<li><a href="../antminer/X17#s17-plus-bos">S17+</a></li>
<li><a href="../antminer/X17#s17-pro-bos">S17 Pro</a></li>
<li><a href="../antminer/X17#s17e-bos">S17e</a></li>
<li><a href="../antminer/X17#t17-bos">T17</a></li>
<li><a href="../antminer/X17#t17-plus-bos">T17+</a></li>
<li><a href="../antminer/X17#t17e-bos">T17e</a></li>
<li><a href="../antminer/X5#dr5">DR5</a></li>
</ul>
</details>
<details>
<summary>X7 Series:</summary>
<ul>
<li><a href="../antminer/X7#l7">L7</a></li>
</ul>
</details>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9#s9-bos">S9</a></li>
<li><a href="../antminer/X9#s9-bos">S9i</a></li>
<li><a href="../antminer/X9#s9-bos">S9j</a></li>
<li><a href="../antminer/X9#e9pro">E9Pro</a></li>
<li><a href="../antminer/X9#s9">S9</a></li>
<li><a href="../antminer/X9#s9i">S9i</a></li>
<li><a href="../antminer/X9#s9j">S9j</a></li>
<li><a href="../antminer/X9#t9">T9</a></li>
</ul>
</details>
<details>
<summary>X15 Series:</summary>
<ul>
<li><a href="../antminer/X15#z15">Z15</a></li>
</ul>
</details>
<details>
<summary>X17 Series:</summary>
<ul>
<li><a href="../antminer/X17#s17">S17</a></li>
<li><a href="../antminer/X17#s17_1">S17+</a></li>
<li><a href="../antminer/X17#s17-pro">S17 Pro</a></li>
<li><a href="../antminer/X17#s17e">S17e</a></li>
<li><a href="../antminer/X17#t17">T17</a></li>
<li><a href="../antminer/X17#t17_1">T17+</a></li>
<li><a href="../antminer/X17#t17e">T17e</a></li>
</ul>
</details>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19">S19</a></li>
<li><a href="../antminer/X19#s19l">S19L</a></li>
<li><a href="../antminer/X19#s19-pro">S19 Pro</a></li>
<li><a href="../antminer/X19#s19j">S19j</a></li>
<li><a href="../antminer/X19#s19i">S19i</a></li>
<li><a href="../antminer/X19#s19_1">S19+</a></li>
<li><a href="../antminer/X19#s19j-no-pic">S19j No PIC</a></li>
<li><a href="../antminer/X19#s19-pro_1">S19 Pro+</a></li>
<li><a href="../antminer/X19#s19j-pro">S19j Pro</a></li>
<li><a href="../antminer/X19#s19-xp">S19 XP</a></li>
<li><a href="../antminer/X19#s19a">S19a</a></li>
<li><a href="../antminer/X19#s19a-pro">S19a Pro</a></li>
<li><a href="../antminer/X19#t19">T19</a></li>
</ul>
</details>
</ul>
@@ -51,440 +88,210 @@ details {
<ul>
<details>
<summary>M2X Series:</summary>
<ul>
<details>
<summary><a href='../whatsminer/M2X/#M20'>M20</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M20V10'>M20V10</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M20S'>M20S</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M20SV10'>M20SV10</a></li>
<li><a href='../whatsminer/M2X/#M20SV20'>M20SV20</a></li>
<li><a href='../whatsminer/M2X/#M20SV30'>M20SV30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M20S_1'>M20S+</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M20S_1V30'>M20S+V30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M21'>M21</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M21V10'>M21V10</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M21S'>M21S</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M21SV20'>M21SV20</a></li>
<li><a href='../whatsminer/M2X/#M21SV60'>M21SV60</a></li>
<li><a href='../whatsminer/M2X/#M21SV70'>M21SV70</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M21S_1'>M21S+</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M21S_1V20'>M21S+V20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M29'>M29</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M29V10'>M29V10</a></li>
</ul>
</details>
</ul>
<ul>
<li><a href="../whatsminer/M2X#m20-v10">M20 V10</a></li>
<li><a href="../whatsminer/M2X#m20s-v10">M20S V10</a></li>
<li><a href="../whatsminer/M2X#m20s-v20">M20S V20</a></li>
<li><a href="../whatsminer/M2X#m20s-v30">M20S V30</a></li>
<li><a href="../whatsminer/M2X#m20p-v10">M20P V10</a></li>
<li><a href="../whatsminer/M2X#m20p-v30">M20P V30</a></li>
<li><a href="../whatsminer/M2X#m20s_1-v30">M20S+ V30</a></li>
<li><a href="../whatsminer/M2X#m21-v10">M21 V10</a></li>
<li><a href="../whatsminer/M2X#m21s-v20">M21S V20</a></li>
<li><a href="../whatsminer/M2X#m21s-v60">M21S V60</a></li>
<li><a href="../whatsminer/M2X#m21s-v70">M21S V70</a></li>
<li><a href="../whatsminer/M2X#m21s_1-v20">M21S+ V20</a></li>
<li><a href="../whatsminer/M2X#m29-v10">M29 V10</a></li>
</ul>
</details>
<details>
<summary>M3X Series:</summary>
<ul>
<details>
<summary><a href='../whatsminer/M3X/#M30'>M30</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M30V10'>M30V10</a></li>
<li><a href='../whatsminer/M3X/#M30V20'>M30V20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M30S'>M30S</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M30SV10'>M30SV10</a></li>
<li><a href='../whatsminer/M3X/#M30SV20'>M30SV20</a></li>
<li><a href='../whatsminer/M3X/#M30SV30'>M30SV30</a></li>
<li><a href='../whatsminer/M3X/#M30SV40'>M30SV40</a></li>
<li><a href='../whatsminer/M3X/#M30SV50'>M30SV50</a></li>
<li><a href='../whatsminer/M3X/#M30SV60'>M30SV60</a></li>
<li><a href='../whatsminer/M3X/#M30SV70'>M30SV70</a></li>
<li><a href='../whatsminer/M3X/#M30SV80'>M30SV80</a></li>
<li><a href='../whatsminer/M3X/#M30SVE10'>M30SVE10</a></li>
<li><a href='../whatsminer/M3X/#M30SVE20'>M30SVE20</a></li>
<li><a href='../whatsminer/M3X/#M30SVE30'>M30SVE30</a></li>
<li><a href='../whatsminer/M3X/#M30SVE40'>M30SVE40</a></li>
<li><a href='../whatsminer/M3X/#M30SVE50'>M30SVE50</a></li>
<li><a href='../whatsminer/M3X/#M30SVE60'>M30SVE60</a></li>
<li><a href='../whatsminer/M3X/#M30SVE70'>M30SVE70</a></li>
<li><a href='../whatsminer/M3X/#M30SVF10'>M30SVF10</a></li>
<li><a href='../whatsminer/M3X/#M30SVF20'>M30SVF20</a></li>
<li><a href='../whatsminer/M3X/#M30SVF30'>M30SVF30</a></li>
<li><a href='../whatsminer/M3X/#M30SVG10'>M30SVG10</a></li>
<li><a href='../whatsminer/M3X/#M30SVG20'>M30SVG20</a></li>
<li><a href='../whatsminer/M3X/#M30SVG30'>M30SVG30</a></li>
<li><a href='../whatsminer/M3X/#M30SVG40'>M30SVG40</a></li>
<li><a href='../whatsminer/M3X/#M30SVH10'>M30SVH10</a></li>
<li><a href='../whatsminer/M3X/#M30SVH20'>M30SVH20</a></li>
<li><a href='../whatsminer/M3X/#M30SVH30'>M30SVH30</a></li>
<li><a href='../whatsminer/M3X/#M30SVH40'>M30SVH40</a></li>
<li><a href='../whatsminer/M3X/#M30SVH50'>M30SVH50</a></li>
<li><a href='../whatsminer/M3X/#M30SVH60'>M30SVH60</a></li>
<li><a href='../whatsminer/M3X/#M30SVI20'>M30SVI20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M30S_1'>M30S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M30S_1V10'>M30S+V10</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V20'>M30S+V20</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V30'>M30S+V30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V40'>M30S+V40</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V50'>M30S+V50</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V60'>M30S+V60</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V70'>M30S+V70</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V80'>M30S+V80</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V90'>M30S+V90</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V100'>M30S+V100</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE30'>M30S+VE30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE40'>M30S+VE40</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE50'>M30S+VE50</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE60'>M30S+VE60</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE70'>M30S+VE70</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE80'>M30S+VE80</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE90'>M30S+VE90</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE100'>M30S+VE100</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VF20'>M30S+VF20</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VF30'>M30S+VF30</a></li>
<li><a href='../whatsminer/M3X/#M36S_1VG30'>M36S+VG30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VG30'>M30S+VG30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VG40'>M30S+VG40</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VG50'>M30S+VG50</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VG60'>M30S+VG60</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH10'>M30S+VH10</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH20'>M30S+VH20</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH30'>M30S+VH30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH40'>M30S+VH40</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH50'>M30S+VH50</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH60'>M30S+VH60</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M30S_2'>M30S++</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M30S_2V10'>M30S++V10</a></li>
<li><a href='../whatsminer/M3X/#M30S_2V20'>M30S++V20</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VE30'>M30S++VE30</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VE40'>M30S++VE40</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VE50'>M30S++VE50</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VF40'>M30S++VF40</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VG30'>M30S++VG30</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VG40'>M30S++VG40</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VG50'>M30S++VG50</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH10'>M30S++VH10</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH20'>M30S++VH20</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH30'>M30S++VH30</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH40'>M30S++VH40</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH50'>M30S++VH50</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH60'>M30S++VH60</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH70'>M30S++VH70</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH80'>M30S++VH80</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH90'>M30S++VH90</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH100'>M30S++VH100</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VJ20'>M30S++VJ20</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VJ30'>M30S++VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31'>M31</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31V10'>M31V10</a></li>
<li><a href='../whatsminer/M3X/#M31V20'>M31V20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31S'>M31S</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31SV10'>M31SV10</a></li>
<li><a href='../whatsminer/M3X/#M31SV20'>M31SV20</a></li>
<li><a href='../whatsminer/M3X/#M31SV30'>M31SV30</a></li>
<li><a href='../whatsminer/M3X/#M31SV40'>M31SV40</a></li>
<li><a href='../whatsminer/M3X/#M31SV50'>M31SV50</a></li>
<li><a href='../whatsminer/M3X/#M31SV60'>M31SV60</a></li>
<li><a href='../whatsminer/M3X/#M31SV70'>M31SV70</a></li>
<li><a href='../whatsminer/M3X/#M31SV80'>M31SV80</a></li>
<li><a href='../whatsminer/M3X/#M31SV90'>M31SV90</a></li>
<li><a href='../whatsminer/M3X/#M31SVE10'>M31SVE10</a></li>
<li><a href='../whatsminer/M3X/#M31SVE20'>M31SVE20</a></li>
<li><a href='../whatsminer/M3X/#M31SVE30'>M31SVE30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31SE'>M31SE</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31SEV10'>M31SEV10</a></li>
<li><a href='../whatsminer/M3X/#M31SEV20'>M31SEV20</a></li>
<li><a href='../whatsminer/M3X/#M31SEV30'>M31SEV30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31H'>M31H</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31HV40'>M31HV40</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31S_1'>M31S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31S_1V10'>M31S+V10</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V20'>M31S+V20</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V30'>M31S+V30</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V40'>M31S+V40</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V50'>M31S+V50</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V60'>M31S+V60</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V80'>M31S+V80</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V90'>M31S+V90</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V100'>M31S+V100</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE10'>M31S+VE10</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE20'>M31S+VE20</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE30'>M31S+VE30</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE40'>M31S+VE40</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE50'>M31S+VE50</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE60'>M31S+VE60</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE80'>M31S+VE80</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VF20'>M31S+VF20</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VF30'>M31S+VF30</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VG20'>M31S+VG20</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VG30'>M31S+VG30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M32'>M32</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M32V10'>M32V10</a></li>
<li><a href='../whatsminer/M3X/#M32V20'>M32V20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M33'>M33</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M33V10'>M33V10</a></li>
<li><a href='../whatsminer/M3X/#M33V20'>M33V20</a></li>
<li><a href='../whatsminer/M3X/#M33V30'>M33V30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M33S'>M33S</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M33SVG30'>M33SVG30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M33S_1'>M33S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M33S_1VH20'>M33S+VH20</a></li>
<li><a href='../whatsminer/M3X/#M33S_1VH30'>M33S+VH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M33S_2'>M33S++</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M33S_2VH20'>M33S++VH20</a></li>
<li><a href='../whatsminer/M3X/#M33S_2VH30'>M33S++VH30</a></li>
<li><a href='../whatsminer/M3X/#M33S_2VG40'>M33S++VG40</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M34S_1'>M34S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M34S_1VE10'>M34S+VE10</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M36S'>M36S</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M36SVE10'>M36SVE10</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M36S_1'>M36S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M36S_1VG30'>M36S+VG30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M36S_2'>M36S++</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M36S_2VH30'>M36S++VH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M39'>M39</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M39V20'>M39V20</a></li>
</ul>
</details>
</ul>
<ul>
<li><a href="../whatsminer/M3X#m30-v10">M30 V10</a></li>
<li><a href="../whatsminer/M3X#m30-v20">M30 V20</a></li>
<li><a href="../whatsminer/M3X#m30k-v10">M30K V10</a></li>
<li><a href="../whatsminer/M3X#m30l-v10">M30L V10</a></li>
<li><a href="../whatsminer/M3X#m30s-v10">M30S V10</a></li>
<li><a href="../whatsminer/M3X#m30s-v20">M30S V20</a></li>
<li><a href="../whatsminer/M3X#m30s-v30">M30S V30</a></li>
<li><a href="../whatsminer/M3X#m30s-v40">M30S V40</a></li>
<li><a href="../whatsminer/M3X#m30s-v50">M30S V50</a></li>
<li><a href="../whatsminer/M3X#m30s-v60">M30S V60</a></li>
<li><a href="../whatsminer/M3X#m30s-v70">M30S V70</a></li>
<li><a href="../whatsminer/M3X#m30s-v80">M30S V80</a></li>
<li><a href="../whatsminer/M3X#m30s-ve10">M30S VE10</a></li>
<li><a href="../whatsminer/M3X#m30s-ve20">M30S VE20</a></li>
<li><a href="../whatsminer/M3X#m30s-ve30">M30S VE30</a></li>
<li><a href="../whatsminer/M3X#m30s-ve40">M30S VE40</a></li>
<li><a href="../whatsminer/M3X#m30s-ve50">M30S VE50</a></li>
<li><a href="../whatsminer/M3X#m30s-ve60">M30S VE60</a></li>
<li><a href="../whatsminer/M3X#m30s-ve70">M30S VE70</a></li>
<li><a href="../whatsminer/M3X#m30s-vf10">M30S VF10</a></li>
<li><a href="../whatsminer/M3X#m30s-vf20">M30S VF20</a></li>
<li><a href="../whatsminer/M3X#m30s-vf30">M30S VF30</a></li>
<li><a href="../whatsminer/M3X#m30s-vg10">M30S VG10</a></li>
<li><a href="../whatsminer/M3X#m30s-vg20">M30S VG20</a></li>
<li><a href="../whatsminer/M3X#m30s-vg30">M30S VG30</a></li>
<li><a href="../whatsminer/M3X#m30s-vg40">M30S VG40</a></li>
<li><a href="../whatsminer/M3X#m30s-vh10">M30S VH10</a></li>
<li><a href="../whatsminer/M3X#m30s-vh20">M30S VH20</a></li>
<li><a href="../whatsminer/M3X#m30s-vh30">M30S VH30</a></li>
<li><a href="../whatsminer/M3X#m30s-vh40">M30S VH40</a></li>
<li><a href="../whatsminer/M3X#m30s-vh50">M30S VH50</a></li>
<li><a href="../whatsminer/M3X#m30s-vh60">M30S VH60</a></li>
<li><a href="../whatsminer/M3X#m30s-vi20">M30S VI20</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v10">M30S+ V10</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v20">M30S+ V20</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v30">M30S+ V30</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v40">M30S+ V40</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v50">M30S+ V50</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v60">M30S+ V60</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v70">M30S+ V70</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v80">M30S+ V80</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v90">M30S+ V90</a></li>
<li><a href="../whatsminer/M3X#m30s_1-v100">M30S+ V100</a></li>
<li><a href="../whatsminer/M3X#m30s_1-ve30">M30S+ VE30</a></li>
<li><a href="../whatsminer/M3X#m30s_1-ve40">M30S+ VE40</a></li>
<li><a href="../whatsminer/M3X#m30s_1-ve50">M30S+ VE50</a></li>
<li><a href="../whatsminer/M3X#m30s_1-ve60">M30S+ VE60</a></li>
<li><a href="../whatsminer/M3X#m30s_1-ve70">M30S+ VE70</a></li>
<li><a href="../whatsminer/M3X#m30s_1-ve80">M30S+ VE80</a></li>
<li><a href="../whatsminer/M3X#m30s_1-ve90">M30S+ VE90</a></li>
<li><a href="../whatsminer/M3X#m30s_1-ve100">M30S+ VE100</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vf20">M30S+ VF20</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vf30">M30S+ VF30</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vg20">M30S+ VG20</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vg30">M30S+ VG30</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vg40">M30S+ VG40</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vg50">M30S+ VG50</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vg60">M30S+ VG60</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vh10">M30S+ VH10</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vh20">M30S+ VH20</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vh30">M30S+ VH30</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vh40">M30S+ VH40</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vh50">M30S+ VH50</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vh60">M30S+ VH60</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-v10">M30S++ V10</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-v20">M30S++ V20</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-ve30">M30S++ VE30</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-ve40">M30S++ VE40</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-ve50">M30S++ VE50</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vf40">M30S++ VF40</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vg30">M30S++ VG30</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vg40">M30S++ VG40</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vg50">M30S++ VG50</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh10">M30S++ VH10</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh20">M30S++ VH20</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh30">M30S++ VH30</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh40">M30S++ VH40</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh50">M30S++ VH50</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh60">M30S++ VH60</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh70">M30S++ VH70</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh80">M30S++ VH80</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh90">M30S++ VH90</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vh100">M30S++ VH100</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vj20">M30S++ VJ20</a></li>
<li><a href="../whatsminer/M3X#m30s_1_1-vj30">M30S++ VJ30</a></li>
<li><a href="../whatsminer/M3X#m31-v10">M31 V10</a></li>
<li><a href="../whatsminer/M3X#m31-v20">M31 V20</a></li>
<li><a href="../whatsminer/M3X#m31h-v10">M31H V10</a></li>
<li><a href="../whatsminer/M3X#m31h-v40">M31H V40</a></li>
<li><a href="../whatsminer/M3X#m30l-v10">M30L V10</a></li>
<li><a href="../whatsminer/M3X#m31s-v10">M31S V10</a></li>
<li><a href="../whatsminer/M3X#m31s-v20">M31S V20</a></li>
<li><a href="../whatsminer/M3X#m31s-v30">M31S V30</a></li>
<li><a href="../whatsminer/M3X#m31s-v40">M31S V40</a></li>
<li><a href="../whatsminer/M3X#m31s-v50">M31S V50</a></li>
<li><a href="../whatsminer/M3X#m31s-v60">M31S V60</a></li>
<li><a href="../whatsminer/M3X#m31s-v70">M31S V70</a></li>
<li><a href="../whatsminer/M3X#m31s-v80">M31S V80</a></li>
<li><a href="../whatsminer/M3X#m31s-v90">M31S V90</a></li>
<li><a href="../whatsminer/M3X#m31s-ve10">M31S VE10</a></li>
<li><a href="../whatsminer/M3X#m31s-ve20">M31S VE20</a></li>
<li><a href="../whatsminer/M3X#m31s-ve30">M31S VE30</a></li>
<li><a href="../whatsminer/M3X#m31se-v10">M31SE V10</a></li>
<li><a href="../whatsminer/M3X#m31se-v20">M31SE V20</a></li>
<li><a href="../whatsminer/M3X#m31se-v30">M31SE V30</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v10">M31S+ V10</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v20">M31S+ V20</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v30">M31S+ V30</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v40">M31S+ V40</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v50">M31S+ V50</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v60">M31S+ V60</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v80">M31S+ V80</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v90">M31S+ V90</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v100">M31S+ V100</a></li>
<li><a href="../whatsminer/M3X#m31s_1-ve10">M31S+ VE10</a></li>
<li><a href="../whatsminer/M3X#m31s_1-ve20">M31S+ VE20</a></li>
<li><a href="../whatsminer/M3X#m31s_1-ve30">M31S+ VE30</a></li>
<li><a href="../whatsminer/M3X#m31s_1-ve40">M31S+ VE40</a></li>
<li><a href="../whatsminer/M3X#m31s_1-ve50">M31S+ VE50</a></li>
<li><a href="../whatsminer/M3X#m31s_1-ve60">M31S+ VE60</a></li>
<li><a href="../whatsminer/M3X#m31s_1-ve80">M31S+ VE80</a></li>
<li><a href="../whatsminer/M3X#m31s_1-vf20">M31S+ VF20</a></li>
<li><a href="../whatsminer/M3X#m31s_1-vf30">M31S+ VF30</a></li>
<li><a href="../whatsminer/M3X#m31s_1-vg20">M31S+ VG20</a></li>
<li><a href="../whatsminer/M3X#m31s_1-vg30">M31S+ VG30</a></li>
<li><a href="../whatsminer/M3X#m32-v10">M32 V10</a></li>
<li><a href="../whatsminer/M3X#m32-v20">M32 V20</a></li>
<li><a href="../whatsminer/M3X#m33-v10">M33 V10</a></li>
<li><a href="../whatsminer/M3X#m33-v20">M33 V20</a></li>
<li><a href="../whatsminer/M3X#m33-v30">M33 V30</a></li>
<li><a href="../whatsminer/M3X#m33s-vg30">M33S VG30</a></li>
<li><a href="../whatsminer/M3X#m33s_1-vg20">M33S+ VG20</a></li>
<li><a href="../whatsminer/M3X#m33s_1-vh20">M33S+ VH20</a></li>
<li><a href="../whatsminer/M3X#m33s_1-vh30">M33S+ VH30</a></li>
<li><a href="../whatsminer/M3X#m33s_1_1-vh20">M33S++ VH20</a></li>
<li><a href="../whatsminer/M3X#m33s_1_1-vh30">M33S++ VH30</a></li>
<li><a href="../whatsminer/M3X#m33s_1_1-vg40">M33S++ VG40</a></li>
<li><a href="../whatsminer/M3X#m34s_1-ve10">M34S+ VE10</a></li>
<li><a href="../whatsminer/M3X#m36s-ve10">M36S VE10</a></li>
<li><a href="../whatsminer/M3X#m36s_1-vg30">M36S+ VG30</a></li>
<li><a href="../whatsminer/M3X#m36s_1_1-vh30">M36S++ VH30</a></li>
<li><a href="../whatsminer/M3X#m39-v10">M39 V10</a></li>
<li><a href="../whatsminer/M3X#m39-v20">M39 V20</a></li>
<li><a href="../whatsminer/M3X#m39-v30">M39 V30</a></li>
</ul>
</details>
<details>
<summary>M5X Series:</summary>
<ul>
<details>
<summary><a href='../whatsminer/M5X/#M50'>M50</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M50VG30'>M50VG30</a></li>
<li><a href='../whatsminer/M5X/#M50VH10'>M50VH10</a></li>
<li><a href='../whatsminer/M5X/#M50VH20'>M50VH20</a></li>
<li><a href='../whatsminer/M5X/#M50VH30'>M50VH30</a></li>
<li><a href='../whatsminer/M5X/#M50VH40'>M50VH40</a></li>
<li><a href='../whatsminer/M5X/#M50VH50'>M50VH50</a></li>
<li><a href='../whatsminer/M5X/#M50VH60'>M50VH60</a></li>
<li><a href='../whatsminer/M5X/#M50VH70'>M50VH70</a></li>
<li><a href='../whatsminer/M5X/#M50VH80'>M50VH80</a></li>
<li><a href='../whatsminer/M5X/#M50VJ10'>M50VJ10</a></li>
<li><a href='../whatsminer/M5X/#M50VJ20'>M50VJ20</a></li>
<li><a href='../whatsminer/M5X/#M50VJ30'>M50VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M50S'>M50S</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M50SVJ10'>M50SVJ10</a></li>
<li><a href='../whatsminer/M5X/#M50SVJ20'>M50SVJ20</a></li>
<li><a href='../whatsminer/M5X/#M50SVJ30'>M50SVJ30</a></li>
<li><a href='../whatsminer/M5X/#M50SVH10'>M50SVH10</a></li>
<li><a href='../whatsminer/M5X/#M50SVH20'>M50SVH20</a></li>
<li><a href='../whatsminer/M5X/#M50SVH30'>M50SVH30</a></li>
<li><a href='../whatsminer/M5X/#M50SVH40'>M50SVH40</a></li>
<li><a href='../whatsminer/M5X/#M50SVH50'>M50SVH50</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M50S_1'>M50S+</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M50S_1VH30'>M50S+VH30</a></li>
<li><a href='../whatsminer/M5X/#M50S_1VH40'>M50S+VH40</a></li>
<li><a href='../whatsminer/M5X/#M50S_1VJ30'>M50S+VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M53'>M53</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M53VH30'>M53VH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M53S'>M53S</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M53SVH30'>M53SVH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M53S_1'>M53S+</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M53S_1VJ30'>M53S+VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M56'>M56</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M56VH30'>M56VH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M56S'>M56S</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M56SVH30'>M56SVH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M56S_1'>M56S+</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M56S_1VJ30'>M56S+VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M59'>M59</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M59VH30'>M59VH30</a></li>
</ul>
</details>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Antminers:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19/#s19">S19</a></li>
<li><a href="../antminer/X19/#s19l">S19L</a></li>
<li><a href="../antminer/X19/#s19-pro">S19 Pro</a></li>
<li><a href="../antminer/X19/#s19a">S19a</a></li>
<li><a href="../antminer/X19/#s19j">S19j</a></li>
<li><a href="../antminer/X19/#s19j-pro">S19j Pro</a></li>
<li><a href="../antminer/X19/#s19-xp">S19 XP</a></li>
<li><a href="../antminer/X19/#t19">T19</a></li>
</ul>
</details>
<details>
<summary>X17 Series:</summary>
<ul>
<li><a href="../antminer/X17/#s17">S17</a></li>
<li><a href="../antminer/X17/#s17_1">S17+</a></li>
<li><a href="../antminer/X17/#s17-pro">S17 Pro</a></li>
<li><a href="../antminer/X17/#s17e">S17e</a></li>
<li><a href="../antminer/X17/#t17">T17</a></li>
<li><a href="../antminer/X17/#t17_1">T17+</a></li>
<li><a href="../antminer/X17/#t17e">T17e</a></li>
</ul>
</details>
<details>
<summary>X15 Series:</summary>
<ul>
<li><a href="../antminer/X15/#z15">Z15</a></li>
</ul>
</details>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9/#s9">S9</a></li>
<li><a href="../antminer/X9/#s9i">S9i</a></li>
<li><a href="../antminer/X9/#t9">T9</a></li>
<li><a href="../antminer/X9/#e9-pro">E9 Pro</a></li>
</ul>
</details>
<details>
<summary>X7 Series:</summary>
<ul>
<li><a href="../antminer/X7/#l7">L7</a></li>
</ul>
</details>
<details>
<summary>X5 Series:</summary>
<ul>
<li><a href="../antminer/X5/#dr5">DR5</a></li>
</ul>
</details>
<details>
<summary>X3 Series:</summary>
<ul>
<li><a href="../antminer/X3/#hs3">HS3</a></li>
</ul>
<ul>
<li><a href="../whatsminer/M5X#m50-ve30">M50 VE30</a></li>
<li><a href="../whatsminer/M5X#m50-vg30">M50 VG30</a></li>
<li><a href="../whatsminer/M5X#m50-vh10">M50 VH10</a></li>
<li><a href="../whatsminer/M5X#m50-vh20">M50 VH20</a></li>
<li><a href="../whatsminer/M5X#m50-vh30">M50 VH30</a></li>
<li><a href="../whatsminer/M5X#m50-vh40">M50 VH40</a></li>
<li><a href="../whatsminer/M5X#m50-vh50">M50 VH50</a></li>
<li><a href="../whatsminer/M5X#m50-vh60">M50 VH60</a></li>
<li><a href="../whatsminer/M5X#m50-vh70">M50 VH70</a></li>
<li><a href="../whatsminer/M5X#m50-vh80">M50 VH80</a></li>
<li><a href="../whatsminer/M5X#m50-vj10">M50 VJ10</a></li>
<li><a href="../whatsminer/M5X#m50-vj20">M50 VJ20</a></li>
<li><a href="../whatsminer/M5X#m50-vj30">M50 VJ30</a></li>
<li><a href="../whatsminer/M5X#m50s-vj10">M50S VJ10</a></li>
<li><a href="../whatsminer/M5X#m50s-vj20">M50S VJ20</a></li>
<li><a href="../whatsminer/M5X#m50s-vj30">M50S VJ30</a></li>
<li><a href="../whatsminer/M5X#m50s-vh10">M50S VH10</a></li>
<li><a href="../whatsminer/M5X#m50s-vh20">M50S VH20</a></li>
<li><a href="../whatsminer/M5X#m50s-vh30">M50S VH30</a></li>
<li><a href="../whatsminer/M5X#m50s-vh40">M50S VH40</a></li>
<li><a href="../whatsminer/M5X#m50s-vh50">M50S VH50</a></li>
<li><a href="../whatsminer/M5X#m50s_1-vh30">M50S+ VH30</a></li>
<li><a href="../whatsminer/M5X#m50s_1-vh40">M50S+ VH40</a></li>
<li><a href="../whatsminer/M5X#m50s_1-vj30">M50S+ VJ30</a></li>
<li><a href="../whatsminer/M5X#m50s_1-vk20">M50S+ VK20</a></li>
<li><a href="../whatsminer/M5X#m50s_1_1-vk10">M50S++ VK10</a></li>
<li><a href="../whatsminer/M5X#m50s_1_1-vk20">M50S++ VK20</a></li>
<li><a href="../whatsminer/M5X#m50s_1_1-vk30">M50S++ VK30</a></li>
<li><a href="../whatsminer/M5X#m53-vh30">M53 VH30</a></li>
<li><a href="../whatsminer/M5X#m53s-vh30">M53S VH30</a></li>
<li><a href="../whatsminer/M5X#m53s_1-vj30">M53S+ VJ30</a></li>
<li><a href="../whatsminer/M5X#m56-vh30">M56 VH30</a></li>
<li><a href="../whatsminer/M5X#m56s-vh30">M56S VH30</a></li>
<li><a href="../whatsminer/M5X#m56s_1-vj30">M56S+ VJ30</a></li>
<li><a href="../whatsminer/M5X#m59-vh30">M59 VH30</a></li>
</ul>
</details>
</ul>
</details>
@@ -492,75 +299,185 @@ details {
<summary>Stock Firmware Avalonminers:</summary>
<ul>
<details>
<summary>A7X Series:</summary>
<ul>
<li><a href="../avalonminer/A7X/#a721">A721</a></li>
<li><a href="../avalonminer/A7X/#a741">A741</a></li>
<li><a href="../avalonminer/A7X/#a761">A761</a></li>
</ul>
<summary>A7X Series:</summary>
<ul>
<li><a href="../avalonminer/A7X#avalon-721">Avalon 721</a></li>
<li><a href="../avalonminer/A7X#avalon-741">Avalon 741</a></li>
<li><a href="../avalonminer/A7X#avalon-761">Avalon 761</a></li>
</ul>
</details>
<details>
<summary>A8X Series:</summary>
<ul>
<li><a href="../avalonminer/A8X/#a821">A821</a></li>
<li><a href="../avalonminer/A8X/#a841">A841</a></li>
<li><a href="../avalonminer/A8X/#a851">A851</a></li>
</ul>
<summary>A8X Series:</summary>
<ul>
<li><a href="../avalonminer/A8X#avalon-821">Avalon 821</a></li>
<li><a href="../avalonminer/A8X#avalon-841">Avalon 841</a></li>
<li><a href="../avalonminer/A8X#avalon-851">Avalon 851</a></li>
</ul>
</details>
<details>
<summary>A9X Series:</summary>
<ul>
<li><a href="../avalonminer/A9X/#a921">A921</a></li>
</ul>
<summary>A9X Series:</summary>
<ul>
<li><a href="../avalonminer/A9X#avalon-921">Avalon 921</a></li>
</ul>
</details>
<details>
<summary>A10X Series:</summary>
<ul>
<li><a href="../avalonminer/A10X/#a1026">A1026</a></li>
<li><a href="../avalonminer/A10X/#a1047">A1047</a></li>
<li><a href="../avalonminer/A10X/#a1066">A1066</a></li>
</ul>
<summary>A10X Series:</summary>
<ul>
<li><a href="../avalonminer/A10X#avalon-1026">Avalon 1026</a></li>
<li><a href="../avalonminer/A10X#avalon-1047">Avalon 1047</a></li>
<li><a href="../avalonminer/A10X#avalon-1066">Avalon 1066</a></li>
</ul>
</details>
<details>
<summary>A11X Series:</summary>
<ul>
<li><a href="../avalonminer/A11X#avalon-1166-pro">Avalon 1166 Pro</a></li>
</ul>
</details>
<details>
<summary>A12X Series:</summary>
<ul>
<li><a href="../avalonminer/A12X#avalon-1246">Avalon 1246</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Innosilicon Miners:</summary>
<summary>Stock Firmware Innosilicons:</summary>
<ul>
<details>
<summary>T3X Series:</summary>
<ul>
<li><a href="../innosilicon/T3X/#t3h">T3H+</a></li>
</ul>
<summary>T3X Series:</summary>
<ul>
<li><a href="../innosilicon/T3X#t3h_1">T3H+</a></li>
</ul>
</details>
<details>
<summary>A10X Series:</summary>
<ul>
<li><a href="../innosilicon/A10X/#a10x">A10X</a></li>
</ul>
<summary>A10X Series:</summary>
<ul>
<li><a href="../innosilicon/A10X#a10x">A10X</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware BFGMinerGoldshell Miners:</summary>
<summary>Stock Firmware Goldshells:</summary>
<ul>
<details>
<summary>CKX Series:</summary>
<ul>
<li><a href="../goldshell/CKX/#ck5">CK5</a></li>
</ul>
<summary>X5 Series:</summary>
<ul>
<li><a href="../goldshell/X5#ck5">CK5</a></li>
<li><a href="../goldshell/X5#hs5">HS5</a></li>
<li><a href="../goldshell/X5#kd5">KD5</a></li>
</ul>
</details>
<details>
<summary>HSX Series:</summary>
<ul>
<li><a href="../goldshell/HSX/#hs5">HS5</a></li>
</ul>
</details>
<details>
<summary>KDX Series:</summary>
<ul>
<li><a href="../goldshell/KDX/#kd5">KD5</a></li>
<li><a href="../goldshell/KDX/#kd-max">KD Max</a></li>
</ul>
<summary>XMax Series:</summary>
<ul>
<li><a href="../goldshell/XMax#kd-max">KD Max</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>BOS+ Firmware Miners:</summary>
<ul>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9#s9-bos">S9 (BOS)</a></li>
</ul>
</details>
<details>
<summary>X17 Series:</summary>
<ul>
<li><a href="../antminer/X17#s17-bos">S17 (BOS)</a></li>
<li><a href="../antminer/X17#s17_1-bos">S17+ (BOS)</a></li>
<li><a href="../antminer/X17#s17-pro-bos">S17 Pro (BOS)</a></li>
<li><a href="../antminer/X17#s17e-bos">S17e (BOS)</a></li>
<li><a href="../antminer/X17#t17-bos">T17 (BOS)</a></li>
<li><a href="../antminer/X17#t17_1-bos">T17+ (BOS)</a></li>
<li><a href="../antminer/X17#t17e-bos">T17e (BOS)</a></li>
</ul>
</details>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-bos">S19 (BOS)</a></li>
<li><a href="../antminer/X19#s19-pro-bos">S19 Pro (BOS)</a></li>
<li><a href="../antminer/X19#s19j-bos">S19j (BOS)</a></li>
<li><a href="../antminer/X19#s19j-no-pic-bos">S19j No PIC (BOS)</a></li>
<li><a href="../antminer/X19#s19j-pro-bos">S19j Pro (BOS)</a></li>
<li><a href="../antminer/X19#s19j-pro-bos">S19j Pro (BOS)</a></li>
<li><a href="../antminer/X19#t19-bos">T19 (BOS)</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>Vnish Firmware Miners:</summary>
<ul>
<details>
<summary>X3 Series:</summary>
<ul>
<li><a href="../antminer/X3#l3_1-vnish">L3+ (VNish)</a></li>
</ul>
</details>
<details>
<summary>X17 Series:</summary>
<ul>
<li><a href="../antminer/X17#s17_1-vnish">S17+ (VNish)</a></li>
<li><a href="../antminer/X17#s17-pro-vnish">S17 Pro (VNish)</a></li>
</ul>
</details>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-vnish">S19 (VNish)</a></li>
<li><a href="../antminer/X19#s19-no-pic-vnish">S19 No PIC (VNish)</a></li>
<li><a href="../antminer/X19#s19-pro-vnish">S19 Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19j-vnish">S19j (VNish)</a></li>
<li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19a-vnish">S19a (VNish)</a></li>
<li><a href="../antminer/X19#s19a-pro-vnish">S19a Pro (VNish)</a></li>
<li><a href="../antminer/X19#t19-vnish">T19 (VNish)</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>ePIC Firmware Miners:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-epic">S19 (ePIC)</a></li>
<li><a href="../antminer/X19#s19-pro-epic">S19 Pro (ePIC)</a></li>
<li><a href="../antminer/X19#s19j-epic">S19j (ePIC)</a></li>
<li><a href="../antminer/X19#s19j-pro-epic">S19j Pro (ePIC)</a></li>
<li><a href="../antminer/X19#s19-xp-epic">S19 XP (ePIC)</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>HiveOS Firmware Miners:</summary>
<ul>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9#t9-hiveon">T9 (Hiveon)</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>LuxOS Firmware Miners:</summary>
<ul>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9#s9-luxos">S9 (LuxOS)</a></li>
</ul>
</details>
</ul>
</details>

View File

@@ -1,90 +1,94 @@
# pyasic
## M2X Models
## M20V10
::: pyasic.miners.btc.whatsminer.btminer.M2X.M20.BTMinerM20V10
## M20 V10
::: pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20V10
handler: python
options:
show_root_heading: false
heading_level: 4
## M20SV10
::: pyasic.miners.btc.whatsminer.btminer.M2X.M20S.BTMinerM20SV10
## M20S V10
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M20SV20
::: pyasic.miners.btc.whatsminer.btminer.M2X.M20S.BTMinerM20SV20
## M20S V20
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV20
handler: python
options:
show_root_heading: false
heading_level: 4
## M20SV30
::: pyasic.miners.btc.whatsminer.btminer.M2X.M20S.BTMinerM20SV30
## M20S V30
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV30
handler: python
options:
show_root_heading: false
heading_level: 4
## M20S+V30
::: pyasic.miners.btc.whatsminer.btminer.M2X.M20S_Plus.BTMinerM20SPlusV30
## M20P V10
::: pyasic.miners.whatsminer.btminer.M2X.M20P.BTMinerM20PV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M21V10
::: pyasic.miners.btc.whatsminer.btminer.M2X.M21.BTMinerM21V10
## M20P V30
::: pyasic.miners.whatsminer.btminer.M2X.M20P.BTMinerM20PV30
handler: python
options:
show_root_heading: false
heading_level: 4
## M21SV20
::: pyasic.miners.btc.whatsminer.btminer.M2X.M21S.BTMinerM21SV20
## M20S+ V30
::: pyasic.miners.whatsminer.btminer.M2X.M20S_Plus.BTMinerM20SPlusV30
handler: python
options:
show_root_heading: false
heading_level: 4
## M21SV60
::: pyasic.miners.btc.whatsminer.btminer.M2X.M21S.BTMinerM21SV60
## M21 V10
::: pyasic.miners.whatsminer.btminer.M2X.M21.BTMinerM21V10
handler: python
options:
show_root_heading: false
heading_level: 4
## M21SV70
::: pyasic.miners.btc.whatsminer.btminer.M2X.M21S.BTMinerM21SV70
## M21S V20
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV20
handler: python
options:
show_root_heading: false
heading_level: 4
## M21S+V20
::: pyasic.miners.btc.whatsminer.btminer.M2X.M21S_Plus.BTMinerM21SPlusV20
## M21S V60
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV60
handler: python
options:
show_root_heading: false
heading_level: 4
## M29V10
::: pyasic.miners.btc.whatsminer.btminer.M2X.M29.BTMinerM29V10
## M21S V70
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV70
handler: python
options:
show_root_heading: false
heading_level: 4
## M21S+ V20
::: pyasic.miners.whatsminer.btminer.M2X.M21S_Plus.BTMinerM21SPlusV20
handler: python
options:
show_root_heading: false
heading_level: 4
## M29 V10
::: pyasic.miners.whatsminer.btminer.M2X.M29.BTMinerM29V10
handler: python
options:
show_root_heading: false
heading_level: 4

File diff suppressed because it is too large Load Diff

View File

@@ -1,242 +1,248 @@
# pyasic
## M5X Models
## M50VG30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VG30
## M50 VE30
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VE30
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VH10
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VH10
## M50 VG30
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VG30
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VH20
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VH20
## M50 VH10
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH10
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VH30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VH30
## M50 VH20
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH20
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VH40
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VH40
## M50 VH30
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH30
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VH50
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VH50
## M50 VH40
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH40
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VH60
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VH60
## M50 VH50
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH50
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VH70
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VH70
## M50 VH60
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH60
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VH80
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VH80
## M50 VH70
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH70
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VJ10
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VJ10
## M50 VH80
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH80
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VJ20
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VJ20
## M50 VJ10
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ10
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VJ30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50.BTMinerM50VJ30
## M50 VJ20
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ20
handler: python
options:
show_root_heading: false
heading_level: 4
## M50SVJ10
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ10
## M50 VJ30
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ30
handler: python
options:
show_root_heading: false
heading_level: 4
## M50SVJ20
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ20
## M50S VJ10
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ10
handler: python
options:
show_root_heading: false
heading_level: 4
## M50SVJ30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ30
## M50S VJ20
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ20
handler: python
options:
show_root_heading: false
heading_level: 4
## M50SVH10
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S.BTMinerM50SVH10
## M50S VJ30
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ30
handler: python
options:
show_root_heading: false
heading_level: 4
## M50SVH20
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S.BTMinerM50SVH20
## M50S VH10
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH10
handler: python
options:
show_root_heading: false
heading_level: 4
## M50SVH30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S.BTMinerM50SVH30
## M50S VH20
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH20
handler: python
options:
show_root_heading: false
heading_level: 4
## M50SVH40
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S.BTMinerM50SVH40
## M50S VH30
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH30
handler: python
options:
show_root_heading: false
heading_level: 4
## M50SVH50
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S.BTMinerM50SVH50
## M50S VH40
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH40
handler: python
options:
show_root_heading: false
heading_level: 4
## M50S+VH30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH30
## M50S VH50
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH50
handler: python
options:
show_root_heading: false
heading_level: 4
## M50S+VH40
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH40
## M50S+ VH30
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH30
handler: python
options:
show_root_heading: false
heading_level: 4
## M50S+VJ30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVJ30
## M50S+ VH40
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH40
handler: python
options:
show_root_heading: false
heading_level: 4
## M53VH30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M53.BTMinerM53VH30
## M50S+ VJ30
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVJ30
handler: python
options:
show_root_heading: false
heading_level: 4
## M53SVH30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M53S.BTMinerM53SVH30
## M50S+ VK20
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVK20
handler: python
options:
show_root_heading: false
heading_level: 4
## M53S+VJ30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVJ30
## M50S++ VK10
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK10
handler: python
options:
show_root_heading: false
heading_level: 4
## M56VH30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M56.BTMinerM56VH30
## M50S++ VK20
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK20
handler: python
options:
show_root_heading: false
heading_level: 4
## M56SVH30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M56S.BTMinerM56SVH30
## M50S++ VK30
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK30
handler: python
options:
show_root_heading: false
heading_level: 4
## M56S+VJ30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVJ30
## M53 VH30
::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VH30
handler: python
options:
show_root_heading: false
heading_level: 4
## M59VH30
::: pyasic.miners.btc.whatsminer.btminer.M5X.M59.BTMinerM59VH30
## M53S VH30
::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVH30
handler: python
options:
show_root_heading: false
heading_level: 4
## M53S+ VJ30
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVJ30
handler: python
options:
show_root_heading: false
heading_level: 4
## M56 VH30
::: pyasic.miners.whatsminer.btminer.M5X.M56.BTMinerM56VH30
handler: python
options:
show_root_heading: false
heading_level: 4
## M56S VH30
::: pyasic.miners.whatsminer.btminer.M5X.M56S.BTMinerM56SVH30
handler: python
options:
show_root_heading: false
heading_level: 4
## M56S+ VJ30
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVJ30
handler: python
options:
show_root_heading: false
heading_level: 4
## M59 VH30
::: pyasic.miners.whatsminer.btminer.M5X.M59.BTMinerM59VH30
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,12 +0,0 @@
# pyasic
## Miner Network Range
[`MinerNetworkRange`][pyasic.network.net_range.MinerNetworkRange] is a class used by [`MinerNetwork`][pyasic.network.MinerNetwork] to handle any constructor stings.
The goal is to emulate what is produced by `ipaddress.ip_network` by allowing [`MinerNetwork`][pyasic.network.MinerNetwork] to get a list of hosts.
This allows this class to be the [`MinerNetwork.network`][pyasic.network.MinerNetwork] and hence be used for scanning.
::: pyasic.network.net_range.MinerNetworkRange
handler: python
options:
show_root_heading: false
heading_level: 4

35
docs/settings/settings.md Normal file
View File

@@ -0,0 +1,35 @@
# pyasic
## settings
All settings here are global settings for all of pyasic. Set these settings with `update(key, value)`.
Settings options:
- `network_ping_retries`
- `network_ping_timeout`
- `network_scan_threads`
- `factory_get_retries`
- `factory_get_timeout`
- `get_data_retries`
- `api_function_timeout`
- `default_whatsminer_password`
- `default_innosilicon_password`
- `default_antminer_password`
- `default_bosminer_password`
- `default_vnish_password`
- `default_goldshell_password`
- `socket_linger_time`
### get
::: pyasic.settings.get
handler: python
options:
show_root_heading: false
heading_level: 4
### update
::: pyasic.settings.update
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -4,10 +4,10 @@ nav:
- Introduction: "index.md"
- Miners:
- Supported Miners: "miners/supported_types.md"
- Standard Functionality: "miners/functions.md"
- Miner Factory: "miners/miner_factory.md"
- Network:
- Miner Network: "network/miner_network.md"
- Miner Network Range: "network/miner_network_range.md"
- Dataclasses:
- Miner Data: "data/miner_data.md"
- Error Codes: "data/error_codes.md"
@@ -20,6 +20,7 @@ nav:
- BOSMiner: "API/bosminer.md"
- BTMiner: "API/btminer.md"
- CGMiner: "API/cgminer.md"
- LUXMiner: "API/luxminer.md"
- Unknown: "API/unknown.md"
- Backends:
- BMMiner: "miners/backends/bmminer.md"
@@ -27,6 +28,9 @@ nav:
- BFGMiner: "miners/backends/bfgminer.md"
- BTMiner: "miners/backends/btminer.md"
- CGMiner: "miners/backends/cgminer.md"
- LUXMiner: "miners/backends/luxminer.md"
- VNish: "miners/backends/vnish.md"
- ePIC: "miners/backends/epic.md"
- Hiveon: "miners/backends/hiveon.md"
- Classes:
- Antminer X3: "miners/antminer/X3.md"
@@ -40,16 +44,18 @@ nav:
- Avalon 8X: "miners/avalonminer/A8X.md"
- Avalon 9X: "miners/avalonminer/A9X.md"
- Avalon 10X: "miners/avalonminer/A10X.md"
- Avalon 11X: "miners/avalonminer/A11X.md"
- Avalon 12X: "miners/avalonminer/A12X.md"
- Whatsminer M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md"
- Whatsminer M5X: "miners/whatsminer/M5X.md"
- Innosilicon T3X: "miners/innosilicon/T3X.md"
- Innosilicon A10X: "miners/innosilicon/A10X.md"
- Goldshell CKX: "miners/goldshell/CKX.md"
- Goldshell HSX: "miners/goldshell/HSX.md"
- Goldshell KDX: "miners/goldshell/KDX.md"
- Goldshell X5: "miners/goldshell/X5.md"
- Goldshell XMax: "miners/goldshell/XMax.md"
- Base Miner: "miners/base_miner.md"
- Settings:
- Settings: "settings/settings.md"
plugins:
- mkdocstrings

View File

@@ -32,6 +32,8 @@ class BaseMinerAPI:
# ip address of the miner
self.ip = ipaddress.ip_address(ip)
self.pwd = "admin"
def __new__(cls, *args, **kwargs):
if cls is BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
@@ -73,18 +75,23 @@ class BaseMinerAPI:
# send the command
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
if data == b"Socket connect failed: Connection refused\n":
if not ignore_errors:
raise APIError(data.decode("utf-8"))
return {}
data = self._load_api_data(data)
# check for if the user wants to allow errors to return
if not ignore_errors:
# validate the command succeeded
validation = self._validate_command_output(data)
if not validation[0]:
if allow_warning:
logging.warning(
f"{self.ip}: API Command Error: {command}: {validation[1]}"
)
validation = self._validate_command_output(data)
if not validation[0]:
if not ignore_errors:
# validate the command succeeded
raise APIError(validation[1])
if allow_warning:
logging.warning(
f"{self.ip}: API Command Error: {command}: {validation[1]}"
)
logging.debug(f"{self} - (Send Command) - Received data.")
return data
@@ -111,16 +118,29 @@ class BaseMinerAPI:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError as e:
# try to identify the error
if ":" in e.message:
err_command = e.message.split(":")[0]
if err_command in commands:
commands.remove(err_command)
continue
if e.message is not None:
if ":" in e.message:
err_command = e.message.split(":")[0]
if err_command in commands:
commands.remove(err_command)
continue
return {command: [{}] for command in commands}
logging.debug(f"{self} - (Multicommand) - Received data")
data["multicommand"] = True
return data
async def _handle_multicommand(self, command: str, allow_warning: bool = True):
try:
data = await self.send_command(command, allow_warning=allow_warning)
if not "+" in command:
return {command: [data]}
return data
except APIError:
if "+" in command:
return {command: [{}] for command in command.split("+")}
return {command: [{}]}
@property
def commands(self) -> list:
return self.get_commands()
@@ -164,7 +184,11 @@ If you are sure you want to use this command please use API.send_command("{comma
)
return return_commands
async def _send_bytes(self, data: bytes, timeout: int = 100) -> bytes:
async def _send_bytes(
self,
data: bytes,
timeout: int = 100,
) -> bytes:
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
try:
# get reader and writer streams
@@ -183,16 +207,18 @@ If you are sure you want to use this command please use API.send_command("{comma
logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
await writer.drain()
try:
ret_data = await asyncio.wait_for(reader.read(4096), timeout=timeout)
except ConnectionAbortedError:
return b"{}"
try:
# TO address a situation where a whatsminer has an unknown PW -AND-
# Fix for stupid whatsminer bug, reboot/restart seem to not load properly in the loop
# have to receive, save the data, check if there is more data by reading with a short timeout
# append that data if there is more, and then onto the main loop.
ret_data += await asyncio.wait_for(reader.read(1), timeout=1)
# the password timeout might need to be longer than 1, but it seems to work for now.
ret_data = await asyncio.wait_for(reader.read(1), timeout=1)
except asyncio.TimeoutError:
return ret_data
return b"{}"
try:
ret_data += await asyncio.wait_for(reader.read(4096), timeout=timeout)
except ConnectionAbortedError:
return b"{}"
# loop to receive all the data
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
@@ -231,13 +257,22 @@ If you are sure you want to use this command please use API.send_command("{comma
# this is an error
return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"]
elif "id" not in data.keys():
if isinstance(data["STATUS"], list):
if data["STATUS"][0].get("STATUS", None) in ["S", "I"]:
return True, None
else:
return False, data["STATUS"][0]["Msg"]
if data["STATUS"] not in ["S", "I"]:
return False, data["Msg"]
else:
# make sure the command succeeded
if type(data["STATUS"]) == str:
if isinstance(data["STATUS"], str):
if data["STATUS"] in ["RESTART"]:
return True, None
elif isinstance(data["STATUS"], dict):
if data["STATUS"].get("STATUS") in ["S", "I"]:
return True, None
elif data["STATUS"][0]["STATUS"] not in ("S", "I"):
# this is an error
if data["STATUS"][0]["STATUS"] not in ("S", "I"):

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
import logging
from pyasic.API import APIError, BaseMinerAPI
@@ -56,19 +56,19 @@ class BFGMinerAPI(BaseMinerAPI):
return data
async def _x19_multicommand(self, *commands) -> dict:
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
tasks = []
# send all commands individually
for cmd in commands:
tasks.append(
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
)
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data
async def version(self) -> dict:

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
import logging
from pyasic.API import APIError, BaseMinerAPI
@@ -57,21 +58,19 @@ class BMMinerAPI(BaseMinerAPI):
return data
async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict:
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(
await self.send_command(cmd, allow_warning=allow_warning)
)
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
tasks = []
# send all commands individually
for cmd in commands:
tasks.append(
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
)
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data
async def version(self) -> dict:

View File

@@ -22,15 +22,15 @@ import hashlib
import json
import logging
import re
from typing import Union
from typing import Literal, Union
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from passlib.handlers.md5_crypt import md5_crypt
from pyasic import settings
from pyasic.API import BaseMinerAPI
from pyasic.errors import APIError
from pyasic.misc import api_min_version
from pyasic.settings import PyasicSettings
### IMPORTANT ###
# you need to change the password of the miners using the Whatsminer
@@ -40,6 +40,12 @@ from pyasic.settings import PyasicSettings
# you change the password, you can pass that to this class as pwd,
# or add it as the Whatsminer_pwd in the settings.toml file.
PrePowerOnMessage = Union[
Literal["wait for adjust temp"],
Literal["adjust complete"],
Literal["adjust continue"],
]
def _crypt(word: str, salt: str) -> str:
"""Encrypts a word with a salt, using a standard salt format.
@@ -186,7 +192,7 @@ class BTMinerAPI(BaseMinerAPI):
ip: str,
api_ver: str = "0.0.0",
port: int = 4028,
pwd: str = PyasicSettings().global_whatsminer_password,
pwd: str = settings.get("default_whatsminer_password", "admin"),
):
super().__init__(ip, port)
self.pwd = pwd
@@ -203,27 +209,35 @@ class BTMinerAPI(BaseMinerAPI):
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# commands starting with "get_" aren't supported, but we can fake that
get_commands_data = {}
# commands starting with "get_" and the "status" command aren't supported, but we can fake that
tasks = []
for command in list(commands):
if command.startswith("get_"):
if command.startswith("get_") or command == "status":
commands.remove(command)
# send seperately and append later
try:
get_commands_data[command] = [
await self.send_command(command, allow_warning=allow_warning)
]
except APIError:
get_commands_data[command] = [{}]
tasks.append(
asyncio.create_task(
self._handle_multicommand(command, allow_warning=allow_warning)
)
)
command = "+".join(commands)
try:
main_data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
main_data = {command: [{}] for command in commands}
tasks.append(
asyncio.create_task(
self._handle_multicommand(command, allow_warning=allow_warning)
)
)
all_data = await asyncio.gather(*tasks)
logging.debug(f"{self} - (Multicommand) - Received data")
data = dict(**main_data, **get_commands_data)
data = {}
for item in all_data:
data.update(item)
data["multicommand"] = True
return data
@@ -473,6 +487,34 @@ class BTMinerAPI(BaseMinerAPI):
"""
return await self.send_privileged_command("set_low_power")
async def set_high_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>
"""
return await self.send_privileged_command("set_high_power")
async def set_normal_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>
"""
return await self.send_privileged_command("set_normal_power")
async def update_firmware(self): # noqa - static
"""Not implemented."""
# to be determined if this will be added later
@@ -685,7 +727,7 @@ class BTMinerAPI(BaseMinerAPI):
)
return await self.send_privileged_command("set_power_pct", percent=str(percent))
async def pre_power_on(self, complete: bool, msg: str) -> dict:
async def pre_power_on(self, complete: bool, msg: PrePowerOnMessage) -> dict:
"""Configure or check status of pre power on.
<details>
@@ -705,7 +747,7 @@ class BTMinerAPI(BaseMinerAPI):
</details>
"""
if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue":
if msg not in ("wait for adjust temp", "adjust complete", "adjust continue"):
raise APIError(
"Message is incorrect, please choose one of "
'["wait for adjust temp", '
@@ -721,6 +763,34 @@ class BTMinerAPI(BaseMinerAPI):
)
### ADDED IN V2.0.5 Whatsminer API ###
@api_min_version("2.0.5")
async def set_power_pct_v2(self, percent: int) -> dict:
"""Set the power percentage of the miner based on current power. Used for temporary adjustment. Added in API v2.0.5.
<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"
)
return await self.send_privileged_command(
"set_power_pct_v2", percent=str(percent)
)
@api_min_version("2.0.5")
async def set_temp_offset(self, temp_offset: int) -> dict:
"""Set the offset of miner hash board target temperature.

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
import logging
from pyasic.API import APIError, BaseMinerAPI
@@ -56,19 +56,19 @@ class CGMinerAPI(BaseMinerAPI):
return data
async def _x19_multicommand(self, *commands) -> dict:
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
tasks = []
# send all commands individually
for cmd in commands:
tasks.append(
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
)
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data
async def version(self) -> dict:

759
pyasic/API/luxminer.py Normal file
View File

@@ -0,0 +1,759 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from typing import Literal
from pyasic.API import BaseMinerAPI
class LUXMinerAPI(BaseMinerAPI):
"""An abstraction of the LUXMiner API.
Each method corresponds to an API command in LUXMiner.
[LUXMiner API documentation](https://docs.firmware.luxor.tech/API/intro)
This class abstracts use of the LUXMiner 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, api_ver: str = "0.0.0", port: int = 4028) -> None:
super().__init__(ip, port=port)
self.api_ver = api_ver
async def addgroup(self, name: str, quota: int) -> dict:
"""Add a pool group.
<details>
<summary>Expand</summary>
Parameters:
name: The group name.
quota: The group quota.
Returns:
Confirmation of adding a pool group.
</details>
"""
return await self.send_command("addgroup", parameters=f"{name},{quota}")
async def addpool(
self, url: str, user: str, pwd: str = "", group_id: str = None
) -> dict:
"""Add a pool.
<details>
<summary>Expand</summary>
Parameters:
url: The pool url.
user: The pool username.
pwd: The pool password.
group_id: The group ID to use.
Returns:
Confirmation of adding a pool.
</details>
"""
pool_data = [url, user, pwd]
if group_id is not None:
pool_data.append(group_id)
return await self.send_command("addpool", parameters=",".join(pool_data))
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 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 check(self, command: str) -> dict:
"""Check if the command `command` exists in LUXMiner.
<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 config(self) -> dict:
"""Get some basic configuration info.
<details>
<summary>Expand</summary>
Returns:
Miner configuration information.
</details>
"""
return await self.send_command("config")
async def curtail(self, session_id: str) -> dict:
"""Put the miner into sleep mode. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
Returns:
A confirmation of putting the miner to sleep.
</details>
"""
return await self.send_command("curtail", parameters=session_id)
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 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 edevs(self) -> dict:
"""Alias for devs"""
return await self.devs()
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 estats(self) -> dict:
"""Alias for stats"""
return await self.stats()
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 fanset(self, session_id: str, speed: int, min_fans: int = None) -> dict:
"""Set fan control. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
speed: The fan speed to set. Use -1 to set automatically.
min_fans: The minimum number of fans to use. Optional.
Returns:
A confirmation of setting fan control values.
</details>
"""
fanset_data = [str(session_id), str(speed)]
if min_fans is not None:
fanset_data.append(str(min_fans))
return await self.send_command("fanset", parameters=",".join(fanset_data))
async def frequencyget(self, board_n: int, chip_n: int = None) -> dict:
"""Get frequency data for a board and chips.
<details>
<summary>Expand</summary>
Parameters:
board_n: The board number to get frequency info from.
chip_n: The chip number to get frequency info from. Optional.
Returns:
Board and/or chip frequency values.
</details>
"""
frequencyget_data = [str(board_n)]
if chip_n is not None:
frequencyget_data.append(str(chip_n))
return await self.send_command(
"frequencyget", parameters=",".join(frequencyget_data)
)
async def frequencyset(self, session_id: str, board_n: int, freq: int) -> dict:
"""Set frequency. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
board_n: The board number to set frequency on.
freq: The frequency to set.
Returns:
A confirmation of setting frequency values.
</details>
"""
return await self.send_command(
"frequencyset", parameters=f"{session_id},{board_n},{freq}"
)
async def frequencystop(self, session_id: str, board_n: int) -> dict:
"""Stop set frequency. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
board_n: The board number to set frequency on.
Returns:
A confirmation of stopping frequencyset value.
</details>
"""
return await self.send_command(
"frequencystop", parameters=f"{session_id},{board_n}"
)
async def groupquota(self, group_n: int, quota: int) -> dict:
"""Set a group's quota.
<details>
<summary>Expand</summary>
Parameters:
group_n: The group number to set quota on.
quota: The quota to use.
Returns:
A confirmation of setting quota value.
</details>
"""
return await self.send_command("groupquota", parameters=f"{group_n},{quota}")
async def groups(self) -> dict:
"""Get pool group data.
<details>
<summary>Expand</summary>
Returns:
Data on the pool groups on the miner.
</details>
"""
return await self.send_command("groups")
async def healthchipget(self, board_n: int, chip_n: int = None) -> dict:
"""Get chip health.
<details>
<summary>Expand</summary>
Parameters:
board_n: The board number to get chip health of.
chip_n: The chip number to get chip health of. Optional.
Returns:
Chip health data.
</details>
"""
healthchipget_data = [str(board_n)]
if chip_n is not None:
healthchipget_data.append(str(chip_n))
return await self.send_command(
"healthchipget", parameters=",".join(healthchipget_data)
)
async def healthchipset(
self, session_id: str, board_n: int, chip_n: int = None
) -> dict:
"""Select the next chip to have its health checked. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
board_n: The board number to next get chip health of.
chip_n: The chip number to next get chip health of. Optional.
Returns:
Confirmation of selecting the next health check chip.
</details>
"""
healthchipset_data = [session_id, str(board_n)]
if chip_n is not None:
healthchipset_data.append(str(chip_n))
return await self.send_command(
"healthchipset", parameters=",".join(healthchipset_data)
)
async def healthctrl(self) -> dict:
"""Get health check config.
<details>
<summary>Expand</summary>
Returns:
Health check config.
</details>
"""
return await self.send_command("healthctrl")
async def healthctrlset(
self, session_id: str, num_readings: int, amplified_factor: float
) -> dict:
"""Set health control config. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
num_readings: The minimum number of readings for evaluation.
amplified_factor: Performance factor of the evaluation.
Returns:
A confirmation of setting health control config.
</details>
"""
return await self.send_command(
"healthctrlset",
parameters=f"{session_id},{num_readings},{amplified_factor}",
)
async def kill(self) -> dict:
"""Forced session kill. Use logoff instead.
<details>
<summary>Expand</summary>
Returns:
A confirmation of killing the active session.
</details>
"""
return await self.send_command("kill")
async def lcd(self) -> dict:
"""Get a general all-in-one status summary of the miner. Always zeros on LUXMiner.
<details>
<summary>Expand</summary>
Returns:
An all-in-one status summary of the miner.
</details>
"""
return await self.send_command("lcd")
async def ledset(
self,
session_id: str,
color: Literal["red"],
state: Literal["on", "off", "blink"],
) -> dict:
"""Set led. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
color: The color LED to set. Can be "red".
state: The state to set the LED to. Can be "on", "off", or "blink".
Returns:
A confirmation of setting LED.
</details>
"""
return await self.send_command(
"ledset", parameters=f"{session_id},{color},{state}"
)
async def limits(self) -> dict:
"""Get max and min values of config parameters.
<details>
<summary>Expand</summary>
Returns:
Data on max and min values of config parameters.
</details>
"""
return await self.send_command("limits")
async def logoff(self, session_id: str) -> dict:
"""Log off of a session. Requires a session id from an active session.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
Returns:
Confirmation of logging off a session.
</details>
"""
return await self.send_command("logoff", parameters=session_id)
async def logon(self) -> dict:
"""Get or create a session.
<details>
<summary>Expand</summary>
Returns:
The Session ID to be used.
</details>
"""
return await self.send_command("logon")
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 power(self) -> dict:
"""Get the estimated power usage in watts.
<details>
<summary>Expand</summary>
Returns:
Estimated power usage in watts.
</details>
"""
return await self.send_command("power")
async def profiles(self) -> dict:
"""Get the available profiles.
<details>
<summary>Expand</summary>
Returns:
Data on available profiles.
</details>
"""
return await self.send_command("profiles")
async def profileset(self, session_id: str, board_n: int, profile: str) -> dict:
"""Set active profile for a board. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
board_n: The board to set the profile on.
profile: The profile name to use.
Returns:
A confirmation of setting the profile on board_n.
</details>
"""
return await self.send_command(
"profileset", parameters=f"{session_id},{board_n},{profile}"
)
async def reboot(self, session_id: str, board_n: int, delay_s: int = None) -> dict:
"""Reboot a board. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
board_n: The board to reboot.
delay_s: The number of seconds to delay until startup. If it is 0, the board will just stop. Optional.
Returns:
A confirmation of rebooting board_n.
</details>
"""
reboot_data = [session_id, str(board_n)]
if delay_s is not None:
reboot_data.append(str(delay_s))
return await self.send_command("reboot", parameters=",".join(reboot_data))
async def rebootdevice(self, session_id: str) -> dict:
"""Reboot the miner. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
Returns:
A confirmation of rebooting the miner.
</details>
"""
return await self.send_command("rebootdevice", parameters=session_id)
async def removegroup(self, group_id: str) -> dict:
"""Remove a pool group.
<details>
<summary>Expand</summary>
Parameters:
group_id: Group id to remove.
Returns:
A confirmation of removing the pool group.
</details>
"""
return await self.send_command("removegroup", parameters=group_id)
async def resetminer(self, session_id: str) -> dict:
"""Restart the mining process. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
Returns:
A confirmation of restarting the mining process.
</details>
"""
return await self.send_command("resetminer", parameters=session_id)
async def removepool(self, pool_id: int) -> dict:
"""Remove a pool.
<details>
<summary>Expand</summary>
Parameters:
pool_id: Pool to remove.
Returns:
A confirmation of removing the pool.
</details>
"""
return await self.send_command("removepool", parameters=str(pool_id))
async def session(self) -> dict:
"""Get the current session.
<details>
<summary>Expand</summary>
Returns:
Data on the current session.
</details>
"""
return await self.send_command("session")
async def tempctrlset(self, target: int, hot: int, dangerous: int) -> dict:
"""Set temp control values.
<details>
<summary>Expand</summary>
Parameters:
target: Target temp.
hot: Hot temp.
dangerous: Dangerous temp.
Returns:
A confirmation of setting the temp control config.
</details>
"""
return await self.send_command(
"tempctrlset", parameters=f"{target},{hot},{dangerous}"
)
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 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 switchpool(self, pool_id: int) -> dict:
"""Switch to a pool.
<details>
<summary>Expand</summary>
Parameters:
pool_id: Pool to switch to.
Returns:
A confirmation of switching to the pool.
</details>
"""
return await self.send_command("switchpool", parameters=str(pool_id))
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 version(self) -> dict:
"""Get miner version info.
<details>
<summary>Expand</summary>
Returns:
Miner version information.
</details>
"""
return await self.send_command("version")
async def voltageget(self, board_n: int) -> dict:
"""Get voltage data for a board.
<details>
<summary>Expand</summary>
Parameters:
board_n: The board number to get voltage info from.
Returns:
Board voltage values.
</details>
"""
return await self.send_command("frequencyget", parameters=str(board_n))
async def voltageset(self, session_id: str, board_n: int, voltage: float) -> dict:
"""Set voltage values.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
board_n: The board to set the voltage on.
voltage: The voltage to use.
Returns:
A confirmation of setting the voltage.
</details>
"""
return await self.send_command(
"voltageset", parameters=f"{session_id},{board_n},{voltage}"
)
async def wakeup(self, session_id: str) -> dict:
"""Take the miner out of sleep mode. Requires a session_id from logon.
<details>
<summary>Expand</summary>
Parameters:
session_id: Session id from the logon command.
Returns:
A confirmation of resuming mining.
</details>
"""
return await self.send_command("wakeup", parameters=session_id)

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic import settings
from pyasic.API.bmminer import BMMinerAPI
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API.btminer import BTMinerAPI
@@ -29,10 +30,9 @@ from pyasic.data import (
from pyasic.errors import APIError, APIWarning
from pyasic.miners import get_miner
from pyasic.miners.base import AnyMiner
from pyasic.miners.miner_factory import MinerFactory
from pyasic.miners.miner_factory import MinerFactory, miner_factory
from pyasic.miners.miner_listener import MinerListener
from pyasic.network import MinerNetwork
from pyasic.settings import PyasicSettings
__all__ = [
"BMMinerAPI",
@@ -51,7 +51,8 @@ __all__ = [
"get_miner",
"AnyMiner",
"MinerFactory",
"miner_factory",
"MinerListener",
"MinerNetwork",
"PyasicSettings",
"settings",
]

View File

@@ -13,667 +13,150 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from copy import deepcopy
from dataclasses import asdict, dataclass
import logging
import random
import string
import time
from dataclasses import asdict, dataclass, fields
from enum import IntEnum
from typing import List, Literal
import toml
import yaml
class X19PowerMode(IntEnum):
Normal = 0
Sleep = 1
LPM = 3
@dataclass
class _Pool:
"""A dataclass for pool information.
Attributes:
url: URL of the pool.
username: Username on the pool.
password: Worker password on the pool.
"""
url: str = ""
username: str = ""
password: str = ""
@classmethod
def fields(cls):
return fields(cls)
def from_dict(self, data: dict):
"""Convert raw pool data as a dict to usable data and save it to this class.
Parameters:
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_wm(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an Whatsminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_x19(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an X19 device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_x17(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an X5 device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_goldshell(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by a goldshell device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {
f"Pool": self.url,
f"UserName": username,
f"Password": self.password,
}
return pool
def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a string usable by an Avalonminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
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) -> dict:
"""Convert the data in this class to a dict usable by an BOSMiner device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "password": self.password}
return pool
@dataclass
class _PoolGroup:
"""A dataclass for pool group information.
Attributes:
quota: The group quota.
group_name: The name of the pool group.
pools: A list of pools in this group.
"""
quota: int = 1
group_name: str = None
pools: List[_Pool] = None
@classmethod
def fields(cls):
return fields(cls)
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.
Parameters:
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) -> List[dict]:
"""Convert the data in this class to a list usable by an X19 device.
Parameters:
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_x17(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by an X17 device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = {
"_ant_pool1url": "",
"_ant_pool1user": "",
"_ant_pool1pw": "",
"_ant_pool2url": "",
"_ant_pool2user": "",
"_ant_pool2pw": "",
"_ant_pool3url": "",
"_ant_pool3user": "",
"_ant_pool3pw": "",
}
for idx, pool in enumerate(self.pools[:3]):
pools[f"_ant_pool{idx+1}url"] = pool.as_x17(user_suffix=user_suffix)["url"]
pools[f"_ant_pool{idx+1}user"] = pool.as_x17(user_suffix=user_suffix)[
"user"
]
pools[f"_ant_pool{idx+1}pw"] = pool.as_x17(user_suffix=user_suffix)["pass"]
return pools
def as_goldshell(self, user_suffix: str = None) -> list:
"""Convert the data in this class to a list usable by a goldshell device.
Parameters:
user_suffix: The suffix to append to username.
"""
return [pool.as_goldshell(user_suffix=user_suffix) for pool in self.pools[:3]]
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = {
"Pool1": None,
"UserName1": None,
"Password1": None,
"Pool2": None,
"UserName2": None,
"Password2": None,
"Pool3": None,
"UserName3": None,
"Password3": None,
}
for idx, pool in enumerate(self.pools[:3]):
pool_data = pool.as_inno(user_suffix=user_suffix)
for key in pool_data:
pools[f"{key}{idx+1}"] = pool_data[key]
return pools
def as_wm(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by a Whatsminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = {}
for i in range(1, 4):
if i <= len(self.pools):
pool_wm = self.pools[i - 1].as_wm(user_suffix)
pools[f"pool_{i}"] = pool_wm["url"]
pools[f"worker_{i}"] = pool_wm["user"]
pools[f"passwd_{i}"] = pool_wm["pass"]
else:
pools[f"pool_{i}"] = ""
pools[f"worker_{i}"] = ""
pools[f"passwd_{i}"] = ""
return pools
def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a dict usable by an Avalonminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
pool = self.pools[0].as_avalon(user_suffix=user_suffix)
return pool
def as_bos(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an BOSMiner device.
Parameters:
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
from pyasic.config.fans import FanModeConfig
from pyasic.config.mining import MiningModeConfig
from pyasic.config.pools import PoolConfig
from pyasic.config.power_scaling import PowerScalingConfig, PowerScalingShutdown
from pyasic.config.temperature import TemperatureConfig
@dataclass
class MinerConfig:
"""A dataclass for miner configuration information.
Attributes:
pool_groups: A list of pool groups in this config.
temp_mode: The temperature control mode.
temp_target: The target temp.
temp_hot: The hot temp (100% fans).
temp_dangerous: The dangerous temp (shutdown).
minimum_fans: The minimum numbers of fans to run the miner.
fan_speed: Manual fan speed to run the fan at (only if temp_mode == "manual").
asicboost: Whether or not to enable asicboost.
autotuning_enabled: Whether or not to enable autotuning.
autotuning_mode: Autotuning mode, either "wattage" or "hashrate".
autotuning_wattage: The wattage to use when autotuning.
autotuning_hashrate: The hashrate to use when autotuning.
dps_enabled: Whether or not to enable dynamic power scaling.
dps_power_step: The amount of power to reduce autotuning by when the miner reaches dangerous temp.
dps_min_power: The minimum power to reduce autotuning to.
dps_shutdown_enabled: Whether or not to shutdown the miner when `dps_min_power` is reached.
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 = 100.0
minimum_fans: int = None
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
asicboost: bool = None
miner_mode: IntEnum = X19PowerMode.Normal
autotuning_enabled: bool = True
autotuning_mode: Literal["power", "hashrate"] = None
autotuning_wattage: int = None
autotuning_hashrate: int = None
dps_enabled: bool = None
dps_power_step: int = None
dps_min_power: int = None
dps_shutdown_enabled: bool = None
dps_shutdown_duration: float = None
@classmethod
def fields(cls):
return fields(cls)
pools: PoolConfig = PoolConfig.default()
fan_mode: FanModeConfig = FanModeConfig.default()
temperature: TemperatureConfig = TemperatureConfig.default()
mining_mode: MiningModeConfig = MiningModeConfig.default()
power_scaling: PowerScalingConfig = PowerScalingConfig.default()
def as_dict(self) -> dict:
"""Convert the data in this class to a dict."""
logging.debug(f"MinerConfig - (To Dict) - Dumping Dict config")
data_dict = asdict(self)
for key in asdict(self).keys():
if isinstance(data_dict[key], IntEnum):
data_dict[key] = data_dict[key].value
if data_dict[key] is None:
del data_dict[key]
return data_dict
return asdict(self)
def as_toml(self) -> str:
"""Convert the data in this class to toml."""
logging.debug(f"MinerConfig - (To TOML) - Dumping TOML config")
return toml.dumps(self.as_dict())
def as_yaml(self) -> str:
"""Convert the data in this class to yaml."""
logging.debug(f"MinerConfig - (To YAML) - Dumping YAML config")
return yaml.dump(self.as_dict(), sort_keys=False)
def from_raw(self, data: dict):
"""Convert raw config data as a dict to usable data and save it to this class.
This should be able to handle any raw config file from any miner supported by pyasic.
Parameters:
data: The raw config data to convert.
"""
logging.debug(f"MinerConfig - (From Raw) - Loading raw config")
pool_groups = []
if isinstance(data, list):
# goldshell config list
data = {"pools": data}
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 == "bitmain-work-mode":
if data[key]:
self.miner_mode = X19PowerMode(int(data[key]))
elif key == "fan_control":
for _key in data[key]:
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]:
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]:
if _key == "enabled":
self.autotuning_enabled = data[key][_key]
elif _key == "psu_power_limit":
self.autotuning_wattage = data[key][_key]
elif _key == "power_target":
self.autotuning_wattage = data[key][_key]
elif _key == "hashrate_target":
self.autotuning_hashrate = data[key][_key]
elif _key == "mode":
self.autotuning_mode = data[key][_key].replace("_target", "")
if key in ["power_scaling", "performance_scaling"]:
for _key in data[key]:
if _key == "enabled":
self.dps_enabled = data[key][_key]
elif _key == "power_step":
self.dps_power_step = data[key][_key]
elif _key in ["min_psu_power_limit", "min_power_target"]:
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_api(self, pools: list):
"""Convert list output from the `AnyMiner.api.pools()` command into a usable data and save it to this class.
Parameters:
pools: The list of pool data to convert.
"""
logging.debug(f"MinerConfig - (From API) - Loading API config")
_pools = []
for pool in pools:
url = pool.get("URL")
user = pool.get("User")
_pools.append({"url": url, "user": user, "pass": "123"})
self.pool_groups = [_PoolGroup().from_dict({"pools": _pools})]
return self
def from_dict(self, data: dict):
"""Convert an output dict of this class back into usable data and save it to this class.
Parameters:
data: The dict config data to convert.
"""
logging.debug(f"MinerConfig - (From Dict) - Loading Dict config")
pool_groups = []
for group in data["pool_groups"]:
pool_groups.append(_PoolGroup().from_dict(group))
for key in data:
if (
hasattr(self, key)
and not key == "pool_groups"
and not key == "miner_mode"
):
setattr(self, key, data[key])
if key == "miner_mode":
self.miner_mode = X19PowerMode(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.
Parameters:
data: The toml config data to convert.
"""
logging.debug(f"MinerConfig - (From TOML) - Loading TOML config")
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.
Parameters:
data: The yaml config data to convert.
"""
logging.debug(f"MinerConfig - (From YAML) - Loading YAML config")
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
def as_am_modern(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_am_modern(),
"freq-level": "100",
**self.mining_mode.as_am_modern(),
**self.pools.as_am_modern(user_suffix=user_suffix),
**self.temperature.as_am_modern(),
**self.power_scaling.as_am_modern(),
}
def as_wm(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by a Whatsminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Whatsminer) - Generating Whatsminer config")
return {
"pools": self.pool_groups[0].as_wm(user_suffix=user_suffix),
"wattage": self.autotuning_wattage,
**self.fan_mode.as_wm(),
**self.mining_mode.as_wm(),
**self.pools.as_wm(user_suffix=user_suffix),
**self.temperature.as_wm(),
**self.power_scaling.as_wm(),
}
def as_am_old(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_am_old(),
**self.mining_mode.as_am_old(),
**self.pools.as_am_old(user_suffix=user_suffix),
**self.temperature.as_am_old(),
**self.power_scaling.as_am_old(),
}
def as_goldshell(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_goldshell(),
**self.mining_mode.as_goldshell(),
**self.pools.as_goldshell(user_suffix=user_suffix),
**self.temperature.as_goldshell(),
**self.power_scaling.as_goldshell(),
}
def as_avalon(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_avalon(),
**self.mining_mode.as_avalon(),
**self.pools.as_avalon(user_suffix=user_suffix),
**self.temperature.as_avalon(),
**self.power_scaling.as_avalon(),
}
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Inno) - Generating Innosilicon config")
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
def as_x19(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by an X19 device.
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As X19) - Generating X19 config")
cfg = {
"bitmain-fan-ctrl": False,
"bitmain-fan-pwn": "100",
"freq-level": "100",
"miner-mode": str(self.miner_mode.value),
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
return {
**self.fan_mode.as_inno(),
**self.mining_mode.as_inno(),
**self.pools.as_inno(user_suffix=user_suffix),
**self.temperature.as_inno(),
**self.power_scaling.as_inno(),
}
if not self.temp_mode == "auto":
cfg["bitmain-fan-ctrl"] = True
if self.fan_speed:
cfg["bitmain-fan-pwn"] = str(self.fan_speed)
if self.miner_mode == X19PowerMode.Sleep:
cfg["freq-level"] = "0"
return cfg
def as_x17(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by an X5 device.
Parameters:
user_suffix: The suffix to append to username.
"""
cfg = self.pool_groups[0].as_x17(user_suffix=user_suffix)
return cfg
def as_goldshell(self, user_suffix: str = None) -> list:
"""Convert the data in this class to a config usable by a goldshell device.
Parameters:
user_suffix: The suffix to append to username.
"""
cfg = self.pool_groups[0].as_goldshell(user_suffix=user_suffix)
return cfg
def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an Avalonminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Avalon) - Generating AvalonMiner config")
cfg = self.pool_groups[0].as_avalon(user_suffix=user_suffix)
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.
Parameters:
model: The model of the miner to be used in the format portion of the config.
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As BOS) - Generating BOSMiner config")
cfg = {
"format": {
"version": "1.2+",
"model": f"Antminer {model.replace('j', 'J')}",
"generator": "pyasic",
"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,
},
def as_bosminer(self, user_suffix: str = None) -> dict:
return {
**merge(self.fan_mode.as_bosminer(), self.temperature.as_bosminer()),
**self.mining_mode.as_bosminer(),
**self.pools.as_bosminer(user_suffix=user_suffix),
**self.power_scaling.as_bosminer(),
}
if self.autotuning_enabled or self.autotuning_wattage:
cfg["autotuning"] = {}
if self.autotuning_enabled:
cfg["autotuning"]["enabled"] = True
else:
cfg["autotuning"]["enabled"] = False
if self.autotuning_mode:
cfg["format"]["version"] = "2.0"
cfg["autotuning"]["mode"] = self.autotuning_mode + "_target"
if self.autotuning_wattage:
cfg["autotuning"]["power_target"] = self.autotuning_wattage
elif self.autotuning_hashrate:
cfg["autotuning"]["hashrate_target"] = self.autotuning_hashrate
else:
if self.autotuning_wattage:
cfg["autotuning"]["psu_power_limit"] = self.autotuning_wattage
def as_bos_grpc(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_bos_grpc(),
**self.temperature.as_bos_grpc(),
**self.mining_mode.as_bos_grpc(),
**self.pools.as_bos_grpc(user_suffix=user_suffix),
**self.power_scaling.as_bos_grpc(),
}
if self.asicboost:
cfg["hash_chain_global"] = {}
cfg["hash_chain_global"]["asic_boost"] = self.asicboost
@classmethod
def from_dict(cls, dict_conf: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_dict(dict_conf.get("pools")),
mining_mode=MiningModeConfig.from_dict(dict_conf.get("mining_mode")),
fan_mode=FanModeConfig.from_dict(dict_conf.get("fan_mode")),
temperature=TemperatureConfig.from_dict(dict_conf.get("temperature")),
power_scaling=PowerScalingConfig.from_dict(dict_conf.get("power_scaling")),
)
if self.minimum_fans is not None or self.fan_speed is not None:
cfg["fan_control"] = {}
if self.minimum_fans is not None:
cfg["fan_control"]["min_fans"] = self.minimum_fans
if self.fan_speed is not None:
cfg["fan_control"]["speed"] = self.fan_speed
@classmethod
def from_api(cls, api_pools: dict) -> "MinerConfig":
return cls(pools=PoolConfig.from_api(api_pools))
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:
if cfg["format"]["version"] == "2.0":
cfg["power_scaling"]["min_power_target"] = self.dps_min_power
else:
cfg["power_scaling"]["min_psu_power_limit"] = self.dps_min_power
if self.dps_shutdown_enabled:
cfg["power_scaling"]["shutdown_enabled"] = self.dps_shutdown_enabled
if self.dps_shutdown_duration:
cfg["power_scaling"]["shutdown_duration"] = self.dps_shutdown_duration
@classmethod
def from_am_modern(cls, web_conf: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_am_modern(web_conf),
mining_mode=MiningModeConfig.from_am_modern(web_conf),
fan_mode=FanModeConfig.from_am_modern(web_conf),
)
return toml.dumps(cfg)
@classmethod
def from_am_old(cls, web_conf: dict) -> "MinerConfig":
return cls.from_am_modern(web_conf)
@classmethod
def from_goldshell(cls, web_conf: dict) -> "MinerConfig":
return cls(pools=PoolConfig.from_am_modern(web_conf))
@classmethod
def from_inno(cls, web_pools: list) -> "MinerConfig":
return cls(pools=PoolConfig.from_inno(web_pools))
@classmethod
def from_bosminer(cls, toml_conf: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_bosminer(toml_conf),
mining_mode=MiningModeConfig.from_bosminer(toml_conf),
fan_mode=FanModeConfig.from_bosminer(toml_conf),
temperature=TemperatureConfig.from_bosminer(toml_conf),
power_scaling=PowerScalingConfig.from_bosminer(toml_conf),
)
def merge(a: dict, b: dict) -> dict:
result = deepcopy(a)
for b_key, b_val in b.items():
a_val = result.get(b_key)
if isinstance(a_val, dict) and isinstance(b_val, dict):
result[b_key] = merge(a_val, b_val)
else:
result[b_key] = deepcopy(b_val)
return result

89
pyasic/config/base.py Normal file
View File

@@ -0,0 +1,89 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from dataclasses import asdict, dataclass
from enum import Enum
from typing import Union
class MinerConfigOption(Enum):
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]):
return cls.default()
def as_am_modern(self) -> dict:
return self.value.as_am_modern()
def as_am_old(self) -> dict:
return self.value.as_am_old()
def as_wm(self) -> dict:
return self.value.as_wm()
def as_inno(self) -> dict:
return self.value.as_inno()
def as_goldshell(self) -> dict:
return self.value.as_goldshell()
def as_avalon(self) -> dict:
return self.value.as_avalon()
def as_bosminer(self) -> dict:
return self.value.as_bosminer()
def as_bos_grpc(self) -> dict:
return self.value.as_bos_grpc()
def __call__(self, *args, **kwargs):
return self.value(*args, **kwargs)
@classmethod
def default(cls):
pass
@dataclass
class MinerConfigValue:
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]):
return cls()
def as_dict(self):
return asdict(self)
def as_am_modern(self) -> dict:
return {}
def as_am_old(self) -> dict:
return {}
def as_wm(self) -> dict:
return {}
def as_inno(self) -> dict:
return {}
def as_goldshell(self) -> dict:
return {}
def as_avalon(self) -> dict:
return {}
def as_bosminer(self) -> dict:
return {}
def as_bos_grpc(self) -> dict:
return {}

134
pyasic/config/fans.py Normal file
View File

@@ -0,0 +1,134 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from dataclasses import dataclass, field
from typing import Union
from pyasic.config.base import MinerConfigOption, MinerConfigValue
@dataclass
class FanModeNormal(MinerConfigValue):
mode: str = field(init=False, default="normal")
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "FanModeNormal":
return cls()
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": False, "bitmain-fan-pwn": "100"}
def as_bosminer(self) -> dict:
return {"temp_control": {"mode": "auto"}}
@dataclass
class FanModeManual(MinerConfigValue):
mode: str = field(init=False, default="manual")
minimum_fans: int = 1
speed: int = 100
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "FanModeManual":
cls_conf = {}
if dict_conf.get("min_fans") is not None:
cls_conf["minimum_fans"] = dict_conf["minimum_fans"]
if dict_conf.get("speed") is not None:
cls_conf["speed"] = dict_conf["speed"]
return cls(**cls_conf)
@classmethod
def from_bosminer(cls, toml_fan_conf: dict) -> "FanModeManual":
cls_conf = {}
if toml_fan_conf.get("min_fans") is not None:
cls_conf["minimum_fans"] = toml_fan_conf["min_fans"]
if toml_fan_conf.get("speed") is not None:
cls_conf["speed"] = toml_fan_conf["speed"]
return cls(**cls_conf)
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwn": str(self.speed)}
def as_bosminer(self) -> dict:
return {
"temp_control": {"mode": "manual"},
"fan_control": {"min_fans": self.minimum_fans, "speed": self.speed},
}
@dataclass
class FanModeImmersion(MinerConfigValue):
mode: str = field(init=False, default="immersion")
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "FanModeImmersion":
return cls()
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwn": "0"}
def as_bosminer(self) -> dict:
return {"temp_control": {"mode": "disabled"}}
class FanModeConfig(MinerConfigOption):
normal = FanModeNormal
manual = FanModeManual
immersion = FanModeImmersion
@classmethod
def default(cls):
return cls.normal()
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]):
if dict_conf is None:
return cls.default()
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
clsattr = getattr(cls, mode)
if clsattr is not None:
return clsattr().from_dict(dict_conf)
@classmethod
def from_am_modern(cls, web_conf: dict):
if web_conf.get("bitmain-fan-ctrl") is not None:
fan_manual = web_conf["bitmain-fan-ctrl"]
if fan_manual:
return cls.manual(speed=web_conf["bitmain-fan-pwm"])
else:
return cls.normal()
else:
return cls.default()
@classmethod
def from_bosminer(cls, toml_conf: dict):
if toml_conf.get("temp_control") is None:
return cls.default()
if toml_conf["temp_control"].get("mode") is None:
return cls.default()
mode = toml_conf["temp_control"]["mode"]
if mode == "auto":
return cls.normal()
elif mode == "manual":
if toml_conf.get("fan_control"):
return cls.manual().from_bosminer(toml_conf["fan_control"])
return cls.manual()
elif mode == "disabled":
return cls.immersion()

213
pyasic/config/mining.py Normal file
View File

@@ -0,0 +1,213 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from dataclasses import dataclass, field
from typing import Union
from pyasic.config.base import MinerConfigOption, MinerConfigValue
@dataclass
class MiningModeNormal(MinerConfigValue):
mode: str = field(init=False, default="normal")
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "MiningModeNormal":
return cls()
def as_am_modern(self) -> dict:
return {"miner-mode": "0"}
def as_wm(self) -> dict:
return {"mode": self.mode}
@dataclass
class MiningModeSleep(MinerConfigValue):
mode: str = field(init=False, default="sleep")
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "MiningModeSleep":
return cls()
def as_am_modern(self) -> dict:
return {"miner-mode": "1"}
def as_wm(self) -> dict:
return {"mode": self.mode}
@dataclass
class MiningModeLPM(MinerConfigValue):
mode: str = field(init=False, default="low")
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "MiningModeLPM":
return cls()
def as_am_modern(self) -> dict:
return {"miner-mode": "3"}
def as_wm(self) -> dict:
return {"mode": self.mode}
@dataclass
class MiningModeHPM(MinerConfigValue):
mode: str = field(init=False, default="high")
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "MiningModeHPM":
return cls()
def as_am_modern(self):
return {"miner-mode": "0"}
def as_wm(self) -> dict:
return {"mode": self.mode}
@dataclass
class MiningModePowerTune(MinerConfigValue):
mode: str = field(init=False, default="power_tuning")
power: int = None
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "MiningModePowerTune":
return cls(dict_conf.get("power"))
def as_am_modern(self) -> dict:
return {"miner-mode": "0"}
def as_wm(self) -> dict:
if self.power is not None:
return {"mode": self.mode, self.mode: {"wattage": self.power}}
return {}
def as_bosminer(self) -> dict:
return {"autotuning": {"enabled": True, "psu_power_limit": self.power}}
@dataclass
class MiningModeHashrateTune(MinerConfigValue):
mode: str = field(init=False, default="hashrate_tuning")
hashrate: int = None
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "MiningModeHashrateTune":
return cls(dict_conf.get("hashrate"))
def as_am_modern(self) -> dict:
return {"miner-mode": "0"}
@dataclass
class ManualBoardSettings(MinerConfigValue):
freq: float
volt: float
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "ManualBoardSettings":
return cls(freq=dict_conf["freq"], volt=dict_conf["volt"])
def as_am_modern(self) -> dict:
return {"miner-mode": "0"}
@dataclass
class MiningModeManual(MinerConfigValue):
mode: str = field(init=False, default="manual")
global_freq: float
global_volt: float
boards: dict[int, ManualBoardSettings] = field(default_factory=dict)
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "MiningModeManual":
return cls(
global_freq=dict_conf["global_freq"],
global_volt=dict_conf["global_volt"],
boards={i: ManualBoardSettings.from_dict(dict_conf[i]) for i in dict_conf},
)
def as_am_modern(self) -> dict:
return {"miner-mode": "0"}
class MiningModeConfig(MinerConfigOption):
normal = MiningModeNormal
low = MiningModeLPM
high = MiningModeHPM
sleep = MiningModeSleep
power_tuning = MiningModePowerTune
hashrate_tuning = MiningModeHashrateTune
manual = MiningModeManual
@classmethod
def default(cls):
return cls.normal()
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]):
if dict_conf is None:
return cls.default()
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
clsattr = getattr(cls, mode)
if clsattr is not None:
return clsattr().from_dict(dict_conf)
@classmethod
def from_am_modern(cls, web_conf: dict):
if web_conf.get("bitmain-work-mode") is not None:
work_mode = web_conf["bitmain-work-mode"]
if work_mode == "":
return cls.default()
if int(work_mode) == 0:
return cls.normal()
elif int(work_mode) == 1:
return cls.sleep()
elif int(work_mode) == 3:
return cls.low()
return cls.default()
@classmethod
def from_bosminer(cls, toml_conf: dict):
if toml_conf.get("autotuning") is None:
return cls.default()
autotuning_conf = toml_conf["autotuning"]
if autotuning_conf.get("enabled") is None:
return cls.default()
if not autotuning_conf["enabled"]:
return cls.default()
if autotuning_conf.get("psu_power_limit") is not None:
# old autotuning conf
return cls.power_tuning(autotuning_conf["psu_power_limit"])
if autotuning_conf.get("mode") is not None:
# new autotuning conf
mode = autotuning_conf["mode"]
if mode == "power_target":
if autotuning_conf.get("power_target") is not None:
return cls.power_tuning(autotuning_conf["power_target"])
return cls.power_tuning()
if mode == "hashrate_target":
if autotuning_conf.get("hashrate_target") is not None:
return cls.hashrate_tuning(autotuning_conf["hashrate_target"])
return cls.hashrate_tuning()

356
pyasic/config/pools.py Normal file
View File

@@ -0,0 +1,356 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import random
import string
from dataclasses import dataclass, field
from typing import Union
from pyasic.config.base import MinerConfigValue
@dataclass
class Pool(MinerConfigValue):
url: str
user: str
password: str
def as_am_modern(self, user_suffix: str = None):
if user_suffix is not None:
return {
"url": self.url,
"user": f"{self.user}{user_suffix}",
"pass": self.password,
}
return {"url": self.url, "user": self.user, "pass": self.password}
def as_wm(self, idx: int, user_suffix: str = None):
if user_suffix is not None:
return {
f"pool_{idx}": self.url,
f"worker_{idx}": f"{self.user}{user_suffix}",
f"passwd_{idx}": self.password,
}
return {
f"pool_{idx}": self.url,
f"worker_{idx}": self.user,
f"passwd_{idx}": self.password,
}
def as_am_old(self, idx: int, user_suffix: str = None):
if user_suffix is not None:
return {
f"_ant_pool{idx}url": self.url,
f"_ant_pool{idx}user": f"{self.user}{user_suffix}",
f"_ant_pool{idx}pw": self.password,
}
return {
f"_ant_pool{idx}url": self.url,
f"_ant_pool{idx}user": self.user,
f"_ant_pool{idx}pw": self.password,
}
def as_goldshell(self, user_suffix: str = None):
if user_suffix is not None:
return {
"url": self.url,
"user": f"{self.user}{user_suffix}",
"pass": self.password,
}
return {"url": self.url, "user": self.user, "pass": self.password}
def as_avalon(self, user_suffix: str = None):
if user_suffix is not None:
return ",".join([self.url, f"{self.user}{user_suffix}", self.password])
return ",".join([self.url, self.user, self.password])
def as_inno(self, idx: int, user_suffix: str = None):
if user_suffix is not None:
return {
f"Pool{idx}": self.url,
f"UserName{idx}": f"{self.user}{user_suffix}",
f"Password{idx}": self.password,
}
return {
f"Pool{idx}": self.url,
f"UserName{idx}": self.user,
f"Password{idx}": self.password,
}
def as_bosminer(self, user_suffix: str = None):
if user_suffix is not None:
return {
"url": self.url,
"user": f"{self.user}{user_suffix}",
"password": self.password,
}
return {"url": self.url, "user": self.user, "password": self.password}
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "Pool":
return cls(
url=dict_conf["url"], user=dict_conf["user"], password=dict_conf["password"]
)
@classmethod
def from_api(cls, api_pool: dict) -> "Pool":
return cls(url=api_pool["URL"], user=api_pool["User"], password="x")
@classmethod
def from_am_modern(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
# TODO: check if this is accurate, user/username, pass/password
@classmethod
def from_goldshell(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_inno(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_bosminer(cls, toml_pool_conf: dict) -> "Pool":
return cls(
url=toml_pool_conf["url"],
user=toml_pool_conf["user"],
password=toml_pool_conf["password"],
)
@dataclass
class PoolGroup(MinerConfigValue):
pools: list[Pool] = field(default_factory=list)
quota: int = 1
name: str = None
def __post_init__(self):
if self.name is None:
self.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 as_am_modern(self, user_suffix: str = None) -> list:
pools = []
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.append(self.pools[idx].as_am_modern(user_suffix=user_suffix))
else:
pools.append(Pool("", "", "").as_am_modern())
idx += 1
return pools
def as_wm(self, user_suffix: str = None) -> dict:
pools = {}
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.update(
**self.pools[idx].as_wm(idx=idx + 1, user_suffix=user_suffix)
)
else:
pools.update(**Pool("", "", "").as_wm(idx=idx + 1))
idx += 1
return pools
def as_am_old(self, user_suffix: str = None) -> dict:
pools = {}
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.update(
**self.pools[idx].as_am_old(idx=idx + 1, user_suffix=user_suffix)
)
else:
pools.update(**Pool("", "", "").as_am_old(idx=idx + 1))
idx += 1
return pools
def as_goldshell(self, user_suffix: str = None) -> list:
return [pool.as_goldshell(user_suffix) for pool in self.pools]
def as_avalon(self, user_suffix: str = None) -> dict:
if len(self.pools) > 0:
return self.pools[0].as_avalon(user_suffix=user_suffix)
return Pool("", "", "").as_avalon()
def as_inno(self, user_suffix: str = None) -> dict:
pools = {}
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.update(
**self.pools[idx].as_inno(idx=idx + 1, user_suffix=user_suffix)
)
else:
pools.update(**Pool("", "", "").as_inno(idx=idx + 1))
idx += 1
return pools
def as_bosminer(self, user_suffix: str = None) -> dict:
if len(self.pools) > 0:
conf = {
"name": self.name,
"pool": [
pool.as_bosminer(user_suffix=user_suffix) for pool in self.pools
],
}
if self.quota is not None:
conf["quota"] = self.quota
return conf
return {"name": "Group", "pool": []}
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "PoolGroup":
cls_conf = {}
if dict_conf.get("quota") is not None:
cls_conf["quota"] = dict_conf["quota"]
if dict_conf.get("name") is not None:
cls_conf["name"] = dict_conf["name"]
cls_conf["pools"] = [Pool.from_dict(p) for p in dict_conf["pools"]]
return cls(**cls_conf)
@classmethod
def from_api(cls, api_pool_list: list) -> "PoolGroup":
pools = []
for pool in api_pool_list:
pools.append(Pool.from_api(pool))
return cls(pools=pools)
@classmethod
def from_am_modern(cls, web_pool_list: list) -> "PoolGroup":
pools = []
for pool in web_pool_list:
pools.append(Pool.from_am_modern(pool))
return cls(pools=pools)
@classmethod
def from_goldshell(cls, web_pools: list) -> "PoolGroup":
return cls([Pool.from_goldshell(p) for p in web_pools])
@classmethod
def from_inno(cls, web_pools: list) -> "PoolGroup":
return cls([Pool.from_inno(p) for p in web_pools])
@classmethod
def from_bosminer(cls, toml_group_conf: dict) -> "PoolGroup":
if toml_group_conf.get("pool") is not None:
return cls(
name=toml_group_conf["name"],
quota=toml_group_conf.get("quota"),
pools=[Pool.from_bosminer(p) for p in toml_group_conf["pool"]],
)
return cls()
@dataclass
class PoolConfig(MinerConfigValue):
groups: list[PoolGroup] = field(default_factory=list)
@classmethod
def default(cls) -> "PoolConfig":
return cls()
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "PoolConfig":
if dict_conf is None:
return cls.default()
return cls(groups=[PoolGroup.from_dict(g) for g in dict_conf["groups"]])
@classmethod
def simple(cls, pools: list[Union[Pool, dict[str, str]]]) -> "PoolConfig":
group_pools = []
for pool in pools:
if isinstance(pool, dict):
pool = Pool(**pool)
group_pools.append(pool)
return cls(groups=[PoolGroup(pools=group_pools)])
def as_am_modern(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_am_modern(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_am_modern()}
def as_wm(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_wm()}
def as_am_old(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return self.groups[0].as_am_old(user_suffix=user_suffix)
return PoolGroup().as_am_old()
def as_goldshell(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_goldshell(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_goldshell()}
def as_avalon(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_avalon(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_avalon()}
def as_inno(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return self.groups[0].as_inno(user_suffix=user_suffix)
return PoolGroup().as_inno()
def as_bosminer(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {
"group": [g.as_bosminer(user_suffix=user_suffix) for g in self.groups]
}
return {"group": [PoolGroup().as_bosminer()]}
def as_bos_grpc(self, user_suffix: str = None) -> dict:
return {}
@classmethod
def from_api(cls, api_pools: dict) -> "PoolConfig":
pool_data = api_pools["POOLS"]
pool_data = sorted(pool_data, key=lambda x: int(x["POOL"]))
return cls([PoolGroup.from_api(pool_data)])
@classmethod
def from_am_modern(cls, web_conf: dict) -> "PoolConfig":
pool_data = web_conf["pools"]
return cls([PoolGroup.from_am_modern(pool_data)])
@classmethod
def from_goldshell(cls, web_pools: list) -> "PoolConfig":
return cls([PoolGroup.from_goldshell(web_pools)])
@classmethod
def from_inno(cls, web_pools: list) -> "PoolConfig":
return cls([PoolGroup.from_inno(web_pools)])
@classmethod
def from_bosminer(cls, toml_conf: dict) -> "PoolConfig":
if toml_conf.get("group") is None:
return cls()
return cls([PoolGroup.from_bosminer(g) for g in toml_conf["group"]])

View File

@@ -0,0 +1,189 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from dataclasses import dataclass, field
from typing import Union
from pyasic.config.base import MinerConfigOption, MinerConfigValue
from pyasic.web.bosminer.proto.braiins.bos.v1 import DpsPowerTarget, DpsTarget, Hours
@dataclass
class PowerScalingShutdownEnabled(MinerConfigValue):
mode: str = field(init=False, default="enabled")
duration: int = None
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "PowerScalingShutdownEnabled":
return cls(duration=dict_conf.get("duration"))
def as_bosminer(self) -> dict:
cfg = {"shutdown_enabled": True}
if self.duration is not None:
cfg["shutdown_duration"] = self.duration
return cfg
def as_bos_grpc(self) -> dict:
cfg = {"enable_shutdown ": True}
if self.duration is not None:
cfg["shutdown_duration"] = Hours(self.duration)
return cfg
@dataclass
class PowerScalingShutdownDisabled(MinerConfigValue):
mode: str = field(init=False, default="disabled")
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "PowerScalingShutdownDisabled":
return cls()
def as_bosminer(self) -> dict:
return {"shutdown_enabled": False}
def as_bos_grpc(self) -> dict:
return {"enable_shutdown ": False}
class PowerScalingShutdown(MinerConfigOption):
enabled = PowerScalingShutdownEnabled
disabled = PowerScalingShutdownDisabled
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]):
if dict_conf is None:
return cls.default()
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
clsattr = getattr(cls, mode)
if clsattr is not None:
return clsattr().from_dict(dict_conf)
@classmethod
def from_bosminer(cls, power_scaling_conf: dict):
sd_enabled = power_scaling_conf.get("shutdown_enabled")
if sd_enabled is not None:
if sd_enabled:
return cls.enabled(power_scaling_conf.get("shutdown_duration"))
else:
return cls.disabled()
return None
@dataclass
class PowerScalingEnabled(MinerConfigValue):
mode: str = field(init=False, default="enabled")
power_step: int = None
minimum_power: int = None
shutdown_enabled: Union[
PowerScalingShutdownEnabled, PowerScalingShutdownDisabled
] = None
@classmethod
def from_bosminer(cls, power_scaling_conf: dict) -> "PowerScalingEnabled":
power_step = power_scaling_conf.get("power_step")
min_power = power_scaling_conf.get("min_psu_power_limit")
sd_mode = PowerScalingShutdown.from_bosminer(power_scaling_conf)
return cls(
power_step=power_step, minimum_power=min_power, shutdown_enabled=sd_mode
)
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "PowerScalingEnabled":
cls_conf = {
"power_step": dict_conf.get("power_step"),
"minimum_power": dict_conf.get("minimum_power"),
}
shutdown_enabled = dict_conf.get("shutdown_enabled")
if shutdown_enabled is not None:
cls_conf["shutdown_enabled"] = PowerScalingShutdown.from_dict(
shutdown_enabled
)
return cls(**cls_conf)
def as_bosminer(self) -> dict:
cfg = {"enabled": True}
if self.power_step is not None:
cfg["power_step"] = self.power_step
if self.minimum_power is not None:
cfg["min_psu_power_limit"] = self.minimum_power
if self.shutdown_enabled is not None:
cfg = {**cfg, **self.shutdown_enabled.as_bosminer()}
return {"power_scaling": cfg}
def as_bos_grpc(self) -> dict:
cfg = {"enable": True}
target_conf = {}
if self.power_step is not None:
target_conf["power_step"] = self.power_step
if self.minimum_power is not None:
target_conf["min_power_target"] = self.minimum_power
cfg["target"] = DpsTarget(power_target=DpsPowerTarget(**target_conf))
if self.shutdown_enabled is not None:
cfg = {**cfg, **self.shutdown_enabled.as_bos_grpc()}
return {"dps": cfg}
@dataclass
class PowerScalingDisabled(MinerConfigValue):
mode: str = field(init=False, default="disabled")
class PowerScalingConfig(MinerConfigOption):
enabled = PowerScalingEnabled
disabled = PowerScalingDisabled
@classmethod
def default(cls):
return cls.disabled()
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]):
if dict_conf is None:
return cls.default()
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
clsattr = getattr(cls, mode)
if clsattr is not None:
return clsattr().from_dict(dict_conf)
@classmethod
def from_bosminer(cls, toml_conf: dict):
power_scaling = toml_conf.get("power_scaling")
if power_scaling is not None:
enabled = power_scaling.get("enabled")
if enabled is not None:
if enabled:
return cls.enabled().from_bosminer(power_scaling)
else:
return cls.disabled()
return cls.default()

View File

@@ -0,0 +1,58 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from dataclasses import dataclass
from typing import Union
from pyasic.config.base import MinerConfigValue
@dataclass
class TemperatureConfig(MinerConfigValue):
target: int = None
hot: int = None
danger: int = None
@classmethod
def default(cls):
return cls()
def as_bosminer(self) -> dict:
temp_cfg = {}
if self.target is not None:
temp_cfg["target_temp"] = self.target
if self.hot is not None:
temp_cfg["hot_temp"] = self.hot
if self.danger is not None:
temp_cfg["dangerous_temp"] = self.danger
return {"temp_control": temp_cfg}
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "TemperatureConfig":
return cls(
target=dict_conf.get("target"),
hot=dict_conf.get("hot"),
danger=dict_conf.get("danger"),
)
@classmethod
def from_bosminer(cls, toml_conf: dict) -> "TemperatureConfig":
temp_control = toml_conf.get("temp_control")
if temp_control is not None:
return cls(
target=temp_control.get("target_temp"),
hot=temp_control.get("hot_temp"),
danger=temp_control.get("dangerous_temp"),
)

View File

@@ -20,7 +20,7 @@ import logging
import time
from dataclasses import asdict, dataclass, field, fields
from datetime import datetime, timezone
from typing import List, Union
from typing import Any, List, Union
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
@@ -40,13 +40,28 @@ class HashBoard:
"""
slot: int = 0
hashrate: float = 0.0
temp: int = -1
chip_temp: int = -1
chips: int = 0
expected_chips: int = 0
hashrate: float = None
temp: int = None
chip_temp: int = None
chips: int = None
expected_chips: int = None
missing: bool = True
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"{item}")
@dataclass
class Fan:
@@ -56,7 +71,22 @@ class Fan:
speed: The speed of the fan.
"""
speed: int = -1
speed: int = None
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"{item}")
@dataclass
@@ -66,34 +96,23 @@ class MinerData:
Attributes:
ip: The IP of the miner as a str.
datetime: The time and date this data was generated.
uptime: The uptime of the miner in seconds.
mac: The MAC address of the miner as a str.
model: The model of the miner as a str.
make: The make of the miner as a str.
api_ver: The current api version on the miner as a str.
fw_ver: The current firmware version on the miner as a str.
hostname: The network hostname of the miner as a str.
hashrate: The hashrate of the miner in TH/s as a float.
hashrate: The hashrate of the miner in TH/s as a float. Calculated automatically.
_hashrate: Backup for hashrate found via API instead of hashboards.
nominal_hashrate: The factory nominal hashrate of the miner in TH/s as a float.
left_board_hashrate: The hashrate of the left board of the miner in TH/s as a float.
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.
hashboards: A list of hashboards on the miner with their statistics.
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.
fans: A list of fans on the miner with their speeds.
fan_psu: The speed of the PSU on the fan if the miner collects it.
left_chips: The number of chips online in the left board as an int.
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.
percent_ideal_chips: The percent of total chips out of the ideal count. Calculated automatically.
@@ -106,51 +125,37 @@ class MinerData:
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.
fault_light: Whether or not the fault light is on as a boolean.
fault_light: Whether the fault light is on as a boolean.
efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically.
is_mining: Whether the miner is mining.
"""
ip: str
datetime: datetime = None
mac: str = "00:00:00:00:00:00"
model: str = "Unknown"
make: str = "Unknown"
api_ver: str = "Unknown"
fw_ver: str = "Unknown"
hostname: str = "Unknown"
uptime: int = None
mac: str = None
model: str = None
make: str = None
api_ver: str = None
fw_ver: str = None
hostname: str = None
hashrate: float = field(init=False)
_hashrate: float = 0
nominal_hashrate: float = 0
_hashrate: float = None
nominal_hashrate: float = None
hashboards: List[HashBoard] = field(default_factory=list)
ideal_hashboards: int = 1
left_board_hashrate: float = field(init=False)
center_board_hashrate: float = field(init=False)
right_board_hashrate: float = field(init=False)
ideal_hashboards: int = None
temperature_avg: int = field(init=False)
env_temp: float = -1.0
left_board_temp: int = field(init=False)
left_board_chip_temp: int = field(init=False)
center_board_temp: int = field(init=False)
center_board_chip_temp: int = field(init=False)
right_board_temp: int = field(init=False)
right_board_chip_temp: int = field(init=False)
wattage: int = -1
wattage_limit: int = -1
env_temp: float = None
wattage: int = None
wattage_limit: int = None
fans: List[Fan] = field(default_factory=list)
fan_1: int = field(init=False)
fan_2: int = field(init=False)
fan_3: int = field(init=False)
fan_4: int = field(init=False)
fan_psu: int = -1
left_chips: int = field(init=False)
center_chips: int = field(init=False)
right_chips: int = field(init=False)
fan_psu: int = None
total_chips: int = field(init=False)
ideal_chips: int = 1
ideal_chips: int = None
percent_ideal_chips: float = field(init=False)
percent_ideal_hashrate: float = field(init=False)
percent_ideal_wattage: float = field(init=False)
nominal: int = field(init=False)
nominal: bool = field(init=False)
pool_split: str = "0"
pool_1_url: str = "Unknown"
pool_1_user: str = "Unknown"
@@ -161,6 +166,7 @@ class MinerData:
] = field(default_factory=list)
fault_light: Union[bool, None] = None
efficiency: int = field(init=False)
is_mining: bool = True
@classmethod
def fields(cls):
@@ -169,7 +175,16 @@ class MinerData:
def __post_init__(self):
self.datetime = datetime.now(timezone.utc).astimezone()
def __getitem__(self, item):
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try:
return getattr(self, item)
except AttributeError:
@@ -221,204 +236,37 @@ class MinerData:
@property
def hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) > 0:
return round(sum(map(lambda x: x.hashrate, self.hashboards)), 2)
hr_data = []
for item in self.hashboards:
if item.hashrate is not None:
hr_data.append(item.hashrate)
if len(hr_data) > 0:
return round(sum(hr_data), 2)
return self._hashrate
@hashrate.setter
def hashrate(self, val):
self._hashrate = val
@property
def fan_1(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 0:
return self.fans[0].speed
@fan_1.setter
def fan_1(self, val):
pass
@property
def fan_2(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 1:
return self.fans[1].speed
@fan_2.setter
def fan_2(self, val):
pass
@property
def fan_3(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 2:
return self.fans[2].speed
@fan_3.setter
def fan_3(self, val):
pass
@property
def fan_4(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 3:
return self.fans[3].speed
@fan_4.setter
def fan_4(self, val):
pass
@property
def total_chips(self): # noqa - Skip PyCharm inspection
return sum([hb.chips for hb in self.hashboards])
if len(self.hashboards) > 0:
chip_data = []
for item in self.hashboards:
if item.chips is not None:
chip_data.append(item.chips)
if len(chip_data) > 0:
return sum(chip_data)
return None
@total_chips.setter
def total_chips(self, val):
pass
@property
def left_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[0].chips
return 0
@left_chips.setter
def left_chips(self, val):
pass
@property
def center_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].chips
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[1].chips
return 0
@center_chips.setter
def center_chips(self, val):
pass
@property
def right_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].chips
if len(self.hashboards) == 3:
return self.hashboards[2].chips
if len(self.hashboards) > 3:
return self.hashboards[-1:][0].chips
return 0
@right_chips.setter
def right_chips(self, val):
pass
@property
def left_board_hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[0].hashrate
return 0
@left_board_hashrate.setter
def left_board_hashrate(self, val):
pass
@property
def center_board_hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].hashrate
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[1].hashrate
return 0
@center_board_hashrate.setter
def center_board_hashrate(self, val):
pass
@property
def right_board_hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].hashrate
if len(self.hashboards) == 3:
return self.hashboards[2].hashrate
if len(self.hashboards) > 3:
return self.hashboards[-1:][0].hashrate
return 0
@right_board_hashrate.setter
def right_board_hashrate(self, val):
pass
@property
def left_board_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[0].temp
return 0
@left_board_temp.setter
def left_board_temp(self, val):
pass
@property
def center_board_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].temp
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[1].temp
return 0
@center_board_temp.setter
def center_board_temp(self, val):
pass
@property
def right_board_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].temp
if len(self.hashboards) == 3:
return self.hashboards[2].temp
if len(self.hashboards) > 3:
return self.hashboards[-1:][0].temp
return 0
@right_board_temp.setter
def right_board_temp(self, val):
pass
@property
def left_board_chip_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[0].chip_temp
return 0
@left_board_chip_temp.setter
def left_board_chip_temp(self, val):
pass
@property
def center_board_chip_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].chip_temp
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[1].chip_temp
return 0
@center_board_chip_temp.setter
def center_board_chip_temp(self, val):
pass
@property
def right_board_chip_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].chip_temp
if len(self.hashboards) == 3:
return self.hashboards[2].chip_temp
if len(self.hashboards) > 3:
return self.hashboards[-1:][0].chip_temp
return 0
@right_board_chip_temp.setter
def right_board_chip_temp(self, val):
pass
@property
def nominal(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.ideal_chips is None:
return None
return self.ideal_chips == self.total_chips
@nominal.setter
@@ -427,6 +275,8 @@ class MinerData:
@property
def percent_ideal_chips(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.ideal_chips is None:
return None
if self.total_chips == 0 or self.ideal_chips == 0:
return 0
return round((self.total_chips / self.ideal_chips) * 100)
@@ -437,6 +287,8 @@ class MinerData:
@property
def percent_ideal_hashrate(self): # noqa - Skip PyCharm inspection
if self.hashrate is None or self.nominal_hashrate is None:
return None
if self.hashrate == 0 or self.nominal_hashrate == 0:
return 0
return round((self.hashrate / self.nominal_hashrate) * 100)
@@ -447,6 +299,8 @@ class MinerData:
@property
def percent_ideal_wattage(self): # noqa - Skip PyCharm inspection
if self.wattage_limit is None or self.wattage is None:
return None
if self.wattage_limit == 0 or self.wattage == 0:
return 0
return round((self.wattage / self.wattage_limit) * 100)
@@ -460,11 +314,11 @@ class MinerData:
total_temp = 0
temp_count = 0
for hb in self.hashboards:
if hb.temp and not hb.temp == -1:
if hb.temp is not None:
total_temp += hb.temp
temp_count += 1
if not temp_count > 0:
return 0
return None
return round(total_temp / temp_count)
@temperature_avg.setter
@@ -473,7 +327,9 @@ class MinerData:
@property
def efficiency(self): # noqa - Skip PyCharm inspection
if self.hashrate == 0 or self.wattage == -1:
if self.hashrate is None or self.wattage is None:
return None
if self.hashrate == 0 or self.wattage == 0:
return 0
return round(self.wattage / self.hashrate)
@@ -482,13 +338,16 @@ class MinerData:
pass
def asdict(self) -> dict:
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
return asdict(self)
def as_dict(self) -> dict:
"""Get this dataclass as a dictionary.
Returns:
A dictionary version of this class.
"""
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
return asdict(self)
return self.asdict()
def as_json(self) -> str:
"""Get this dataclass as JSON.
@@ -533,27 +392,45 @@ class MinerData:
tags = ["ip", "mac", "model", "hostname"]
for attribute in self:
if attribute in tags:
escaped_data = self[attribute].replace(" ", "\\ ")
escaped_data = self.get(attribute, "Unknown").replace(" ", "\\ ")
tag_data.append(f"{attribute}={escaped_data}")
continue
if isinstance(self[attribute], str):
elif str(attribute).startswith("_"):
continue
elif isinstance(self[attribute], str):
field_data.append(f'{attribute}="{self[attribute]}"')
continue
if isinstance(self[attribute], bool):
elif isinstance(self[attribute], bool):
field_data.append(f"{attribute}={str(self[attribute]).lower()}")
continue
if isinstance(self[attribute], int):
elif isinstance(self[attribute], int):
field_data.append(f"{attribute}={self[attribute]}")
continue
if isinstance(self[attribute], float):
elif isinstance(self[attribute], float):
field_data.append(f"{attribute}={self[attribute]}")
continue
if attribute == "fault_light" and not self[attribute]:
field_data.append(f"{attribute}=false")
continue
if attribute == "errors":
elif attribute == "errors":
for idx, item in enumerate(self[attribute]):
field_data.append(f'error_{idx+1}="{item.error_message}"')
elif attribute == "hashboards":
for idx, item in enumerate(self[attribute]):
field_data.append(
f"hashboard_{idx+1}_hashrate={item.get('hashrate', 0.0)}"
)
field_data.append(
f"hashboard_{idx+1}_temperature={item.get('temp', 0)}"
)
field_data.append(
f"hashboard_{idx+1}_chip_temperature={item.get('chip_temp', 0)}"
)
field_data.append(f"hashboard_{idx+1}_chips={item.get('chips', 0)}")
field_data.append(
f"hashboard_{idx+1}_expected_chips={item.get('expected_chips', 0)}"
)
elif attribute == "fans":
for idx, item in enumerate(self[attribute]):
if item.speed is not None:
field_data.append(f"fan_{idx+1}={item.speed}")
tags_str = ",".join(tag_data)
field_str = ",".join(field_data)

View File

@@ -16,8 +16,6 @@
from dataclasses import asdict, dataclass, field, fields
C_N_CODES = ["52", "53", "54", "55", "56"]
@dataclass
class WhatsminerError:
@@ -37,10 +35,8 @@ class WhatsminerError:
@property
def error_message(self): # noqa - Skip PyCharm inspection
if len(str(self.error_code)) > 3 and str(self.error_code)[:2] in C_N_CODES:
# 55 error code base has chip numbers, so the format is
# 55 -> board num len 1 -> chip num len 3
err_type = 55
if len(str(self.error_code)) == 6 and not str(self.error_code)[:1] == "1":
err_type = int(str(self.error_code)[:2])
err_subtype = int(str(self.error_code)[2:3])
err_value = int(str(self.error_code)[3:])
else:
@@ -88,7 +84,9 @@ class WhatsminerError:
ERROR_CODES = {
1: { # Fan error
0: {0: "Fan unknown."},
0: {
0: "Fan unknown.",
},
1: { # Fan speed error of 1000+
0: "Intake fan speed error.",
1: "Exhaust fan speed error.",
@@ -101,7 +99,9 @@ ERROR_CODES = {
0: "Intake fan speed error. Fan speed deviates by more than 3000.",
1: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
},
4: {0: "Fan speed too high."}, # High speed
4: {
0: "Fan speed too high.",
}, # High speed
},
2: { # Power error
0: {
@@ -126,6 +126,7 @@ ERROR_CODES = {
6: "Power remained unchanged for a long time.",
7: "Power set enable error.",
8: "Power input voltage is lower than 230V for high power mode.",
9: "Power input current is incorrect.",
},
3: {
3: "Power output high temperature protection error.",
@@ -159,6 +160,8 @@ ERROR_CODES = {
6: {
3: "Power communication warning.",
4: "Power communication error.",
5: "Power unknown error.",
6: "Power unknown error.",
7: "Power watchdog protection.",
8: "Power output high current protection.",
9: "Power input high current protection.",
@@ -170,57 +173,134 @@ ERROR_CODES = {
3: "Power input too high warning.",
4: "Power fan warning.",
5: "Power high temperature warning.",
6: "Power unknown error.",
7: "Power unknown error.",
8: "Power unknown error.",
9: "Power unknown error.",
},
8: {
0: "Power unknown error.",
1: "Power vendor status 1 bit 0 error.",
2: "Power vendor status 1 bit 1 error.",
3: "Power vendor status 1 bit 2 error.",
4: "Power vendor status 1 bit 3 error.",
5: "Power vendor status 1 bit 4 error.",
6: "Power vendor status 1 bit 5 error.",
7: "Power vendor status 1 bit 6 error.",
8: "Power vendor status 1 bit 7 error.",
9: "Power vendor status 2 bit 0 error.",
},
9: {
0: "Power vendor status 2 bit 1 error.",
1: "Power vendor status 2 bit 2 error.",
2: "Power vendor status 2 bit 3 error.",
3: "Power vendor status 2 bit 4 error.",
4: "Power vendor status 2 bit 5 error.",
5: "Power vendor status 2 bit 6 error.",
6: "Power vendor status 2 bit 7 error.",
},
},
3: { # temperature error
0: { # sensor detection error
"n": "Slot {n} temperature sensor detection error."
"n": "Slot {n} temperature sensor detection error.",
},
2: { # temperature reading error
"n": "Slot {n} temperature reading error.",
9: "Control board temperature sensor communication error.",
},
5: {"n": "Slot {n} temperature protecting."}, # temperature protection
6: {0: "Hashboard high temperature error."}, # high temp
5: {
"n": "Slot {n} temperature protecting.",
}, # temperature protection
6: {
0: "Hashboard high temperature error.",
1: "Hashboard high temperature error.",
2: "Hashboard high temperature error.",
3: "Hashboard high temperature error.",
}, # high temp
7: {
0: "The environment temperature fluctuates too much.",
}, # env temp
8: {
0: "Humidity sensor not found.",
1: "Humidity sensor read error.",
2: "Humidity sensor read error.",
3: "Humidity sensor protecting.",
},
}, # humidity
},
4: { # EEPROM error
0: {0: "Eeprom unknown error."},
1: {"n": "Slot {n} eeprom detection error."}, # EEPROM detection error
2: {"n": "Slot {n} eeprom parsing error."}, # EEPROM parsing error
3: {"n": "Slot {n} chip bin type error."}, # chip bin error
4: {"n": "Slot {n} eeprom chip number X error."}, # EEPROM chip number error
5: {"n": "Slot {n} eeprom xfer error."}, # EEPROM xfer error
0: {
0: "Eeprom unknown error.",
},
1: {
"n": "Slot {n} eeprom detection error.",
}, # EEPROM detection error
2: {
"n": "Slot {n} eeprom parsing error.",
}, # EEPROM parsing error
3: {
"n": "Slot {n} chip bin type error.",
}, # chip bin error
4: {
"n": "Slot {n} eeprom chip number X error.",
}, # EEPROM chip number error
5: {
"n": "Slot {n} eeprom xfer error.",
}, # EEPROM xfer error
},
5: { # hashboard error
0: {0: "Board unknown error."},
1: {"n": "Slot {n} miner type error."}, # board miner type error
2: {"n": "Slot {n} bin type error."}, # chip bin type error
3: {"n": "Slot {n} not found."}, # board not found error
4: {"n": "Slot {n} error reading chip id."}, # reading chip id error
5: {"n": "Slot {n} has bad chips."}, # board has bad chips error
6: {"n": "Slot {n} loss of balance error."}, # loss of balance error
7: {"n": "Slot {n} xfer error chip."}, # xfer error
8: {"n": "Slot {n} reset error."}, # reset error
9: {"n": "Slot {n} frequency too low."}, # freq error
0: {
0: "Board unknown error.",
},
1: {
"n": "Slot {n} miner type error.",
}, # board miner type error
2: {
"n": "Slot {n} bin type error.",
}, # chip bin type error
3: {
"n": "Slot {n} not found.",
}, # board not found error
4: {
"n": "Slot {n} error reading chip id.",
}, # reading chip id error
5: {
"n": "Slot {n} has bad chips.",
}, # board has bad chips error
6: {
"n": "Slot {n} loss of balance error.",
}, # loss of balance error
7: {
"n": "Slot {n} xfer error chip.",
}, # xfer error
8: {
"n": "Slot {n} reset error.",
}, # reset error
9: {
"n": "Slot {n} frequency too low.",
}, # freq error
},
6: { # env temp error
0: {0: "Environment temperature is too high."}, # normal env temp error
0: {
0: "Environment temperature is too high.",
}, # normal env temp error
1: { # high power env temp error
0: "Environment temperature is too high for high performance mode."
0: "Environment temperature is too high for high performance mode.",
},
},
7: { # control board error
0: {0: "MAC address invalid", 1: "Control board no support chip."},
0: {
0: "MAC address invalid",
1: "Control board no support chip.",
},
1: {
0: "Control board rebooted as an exception.",
1: "Control board rebooted as exception and cpufreq reduced, please upgrade the firmware",
2: "Control board rebooted as an exception.",
3: "The network is unstable, change time.",
4: "Unknown error.",
},
2: {
"n": "Control board slot {n} frame error.",
},
},
8: { # checksum error
@@ -228,63 +308,152 @@ ERROR_CODES = {
0: "CGMiner checksum error.",
1: "System monitor checksum error.",
2: "Remote daemon checksum error.",
}
},
1: {0: "Air to liquid PCB serial # does not match."},
},
9: {0: {1: "Power rate error."}}, # power rate error
9: {
0: {0: "Unknown error.", 1: "Power rate error.", 2: "Unknown error."}
}, # power rate error
20: { # pool error
1: {0: "All pools are disabled."}, # all disabled error
2: {"n": "Pool {n} connection failed."}, # pool connection failed error
3: {0: "High rejection rate on pool."}, # rejection rate error
0: {
0: "No pool information configured.",
},
1: {
0: "All pools are disabled.",
}, # all disabled error
2: {
"n": "Pool {n} connection failed.",
}, # pool connection failed error
3: {
0: "High rejection rate on pool.",
}, # rejection rate error
4: { # asicboost not supported error
0: "The pool does not support asicboost mode."
0: "The pool does not support asicboost mode.",
},
},
21: {1: {"n": "Slot {n} factory test step failed."}},
21: {
1: {
"n": "Slot {n} factory test step failed.",
}
},
23: { # hashrate error
1: {0: "Hashrate is too low."},
2: {0: "Hashrate is too low."},
3: {0: "Hashrate loss is too high."},
4: {0: "Hashrate loss is too high."},
5: {0: "Hashrate loss."},
1: {
0: "Hashrate is too low.",
},
2: {
0: "Hashrate is too low.",
},
3: {
0: "Hashrate loss is too high.",
},
4: {
0: "Hashrate loss is too high.",
},
5: {
0: "Hashrate loss.",
},
},
50: { # water velocity error/voltage error
1: {"n": "Slot {n} chip voltage too low."},
2: {"n": "Slot {n} chip voltage changed."},
3: {"n": "Slot {n} chip temperature difference is too large."},
4: {"n": "Slot {n} chip hottest temperature difference is too large."},
7: {"n": "Slot {n} water velocity is abnormal."}, # abnormal water velocity
8: {0: "Chip temp calibration failed, please restore factory settings."},
9: {"n": "Slot {n} chip temp calibration check no balance."},
1: {
"n": "Slot {n} chip voltage too low.",
},
2: {
"n": "Slot {n} chip voltage changed.",
},
3: {
"n": "Slot {n} chip temperature difference is too large.",
},
4: {
"n": "Slot {n} chip hottest temperature difference is too large.",
},
5: {"n": "Slot {n} stopped hashing, chips temperature protecting."},
7: {
"n": "Slot {n} water velocity is abnormal.",
}, # abnormal water velocity
8: {
0: "Chip temp calibration failed, please restore factory settings.",
},
9: {
"n": "Slot {n} chip temp calibration check no balance.",
},
},
51: { # frequency error
1: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
7: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
1: {
"n": "Slot {n} frequency up timeout.",
}, # frequency up timeout
2: {"n": "Slot {n} too many CRC errors."},
3: {"n": "Slot {n} unstable."},
7: {
"n": "Slot {n} frequency up timeout.",
}, # frequency up timeout
},
52: {
"n": {
"c": "Slot {n} chip {c} error nonce.",
},
},
53: {
"n": {
"c": "Slot {n} chip {c} too few nonce.",
},
},
54: {
"n": {
"c": "Slot {n} chip {c} temp protected.",
},
},
55: {
"n": {
"c": "Slot {n} chip {c} has been reset.",
},
},
56: {
"n": {
"c": "Slot {n} chip {c} zero nonce.",
},
},
52: {"n": {"c": "Slot {n} chip {c} error nonce."}},
53: {"n": {"c": "Slot {n} chip {c} too few nonce."}},
54: {"n": {"c": "Slot {n} chip {c} temp protected."}},
55: {"n": {"c": "Slot {n} chip {c} has been reset."}},
56: {"n": {"c": "Slot {n} chip {c} does not return to the nonce."}},
80: {
0: {0: "The tool version is too low, please update."},
1: {0: "Low freq."},
2: {0: "Low hashrate."},
3: {5: "High env temp."},
0: {
0: "The tool version is too low, please update.",
},
1: {
0: "Low freq.",
},
2: {
0: "Low hashrate.",
},
3: {
5: "High env temp.",
},
},
81: {
0: {0: "Chip data error."},
0: {
0: "Chip data error.",
},
},
82: {
0: {0: "Power version error."},
1: {0: "Miner type error."},
2: {0: "Version info error."},
0: {
0: "Power version error.",
},
1: {
0: "Miner type error.",
},
2: {
0: "Version info error.",
},
},
83: {
0: {0: "Empty level error."},
0: {
0: "Empty level error.",
},
},
84: {
0: {0: "Old firmware."},
1: {0: "Software version error."},
0: {
0: "Old firmware.",
},
1: {
0: "Software version error.",
},
},
85: {
"n": {
@@ -296,8 +465,12 @@ ERROR_CODES = {
},
},
86: {
0: {0: "Missing product serial #."},
1: {0: "Missing product type."},
0: {
0: "Missing product serial #.",
},
1: {
0: "Missing product type.",
},
2: {
0: "Missing miner serial #.",
1: "Wrong miner serial # length.",
@@ -314,12 +487,34 @@ ERROR_CODES = {
3: "Wrong power model rate.",
4: "Wrong power model format.",
},
5: {0: "Wrong hash board struct."},
6: {0: "Wrong miner cooling type."},
7: {0: "Missing PCB serial #."},
5: {
0: "Wrong hash board struct.",
},
6: {
0: "Wrong miner cooling type.",
},
7: {
0: "Missing PCB serial #.",
},
},
87: {
0: {
0: "Miner power mismatch.",
},
},
90: {
0: {
0: "Process error, exited with signal: 3.",
},
1: {
0: "Process error, exited with signal: 3.",
},
},
99: {
9: {
9: "Miner unknown error.",
},
},
87: {0: {0: "Miner power mismatch."}},
99: {9: {9: "Miner unknown error."}},
1000: {
0: {
0: "Security library error, please upgrade firmware",
@@ -328,7 +523,11 @@ ERROR_CODES = {
3: "/antiv/dig/pf_partial.dig illegal.",
},
},
1001: {0: {0: "Security BTMiner removed, please upgrade firmware."}},
1001: {
0: {
0: "Security BTMiner removed, please upgrade firmware.",
},
},
1100: {
0: {
0: "Security illegal file, please upgrade firmware.",

View File

@@ -149,10 +149,10 @@ class _MinerPhaseBalancer:
not self.miners[data_point.ip]["shutdown"]
):
# cant do anything with it so need to find a semi-accurate power limit
if not data_point.wattage_limit == -1:
if not data_point.wattage_limit == None:
self.miners[data_point.ip]["max"] = int(data_point.wattage_limit)
self.miners[data_point.ip]["min"] = int(data_point.wattage_limit)
elif not data_point.wattage == -1:
elif not data_point.wattage == None:
self.miners[data_point.ip]["max"] = int(data_point.wattage)
self.miners[data_point.ip]["min"] = int(data_point.wattage)

View File

@@ -16,31 +16,29 @@
import logging
from pyasic.settings import PyasicSettings
def init_logger():
if PyasicSettings().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",
)
# if PyasicSettings().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 PyasicSettings().debug:
_logger.setLevel(logging.DEBUG)
logging.getLogger("asyncssh").setLevel(logging.DEBUG)
else:
_logger.setLevel(logging.WARNING)
logging.getLogger("asyncssh").setLevel(logging.WARNING)
# if PyasicSettings().debug:
# _logger.setLevel(logging.DEBUG)
# logging.getLogger("asyncssh").setLevel(logging.DEBUG)
# else:
_logger.setLevel(logging.WARNING)
logging.getLogger("asyncssh").setLevel(logging.WARNING)
return _logger

View File

@@ -18,4 +18,6 @@ from .bmminer import *
from .bosminer import *
from .cgminer import *
from .hiveon import *
from .luxos import *
from .vnish import *
from .epic import *

View File

@@ -21,9 +21,11 @@ from pyasic.miners.types import (
S19XP,
S19a,
S19aPro,
S19i,
S19j,
S19jNoPIC,
S19jPro,
S19Plus,
S19Pro,
S19ProPlus,
)
@@ -33,6 +35,14 @@ class BMMinerS19(AntminerModern, S19):
pass
class BMMinerS19Plus(AntminerModern, S19Plus):
pass
class BMMinerS19i(AntminerModern, S19i):
pass
class BMMinerS19Pro(AntminerModern, S19Pro):
pass

View File

@@ -18,10 +18,12 @@ from .S19 import (
BMMinerS19,
BMMinerS19a,
BMMinerS19aPro,
BMMinerS19i,
BMMinerS19j,
BMMinerS19jNoPIC,
BMMinerS19jPro,
BMMinerS19L,
BMMinerS19Plus,
BMMinerS19Pro,
BMMinerS19ProPlus,
BMMinerS19XP,

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------
from pyasic.miners.backends import BOSMiner
from pyasic.miners.types import S19, S19j, S19jNoPIC, S19jPro, S19Pro
from pyasic.miners.types import S19, S19j, S19jNoPIC, S19jPro, S19kProNoPIC, S19Pro
class BOSMinerS19(BOSMiner, S19):
@@ -36,3 +36,7 @@ class BOSMinerS19jNoPIC(BOSMiner, S19jNoPIC):
class BOSMinerS19jPro(BOSMiner, S19jPro):
pass
class BOSMinerS19kProNoPIC(BOSMiner, S19kProNoPIC):
pass

View File

@@ -19,6 +19,7 @@ from .S19 import (
BOSMinerS19j,
BOSMinerS19jNoPIC,
BOSMinerS19jPro,
BOSMinerS19kProNoPIC,
BOSMinerS19Pro,
)
from .T19 import BOSMinerT19

View File

@@ -0,0 +1,46 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners.backends import ePIC
from pyasic.miners.types import S19, S19XP, S19j, S19jPro, S19jProPlus, S19kPro, S19Pro
class ePICS19(ePIC, S19):
pass
class ePICS19Pro(ePIC, S19Pro):
pass
class ePICS19j(ePIC, S19j):
pass
class ePICS19jPro(ePIC, S19jPro):
pass
class ePICS19jProPlus(ePIC, S19jProPlus):
pass
class ePICS19kPro(ePIC, S19kPro):
pass
class ePICS19XP(ePIC, S19XP):
pass

View File

@@ -0,0 +1,25 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .S19 import (
ePICS19,
ePICS19Pro,
ePICS19j,
ePICS19jPro,
ePICS19jProPlus,
ePICS19kPro,
ePICS19XP,
)

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .X19 import *

View File

@@ -58,7 +58,7 @@ class HiveonT9(Hiveon, T9):
hashrate = 0
chips = 0
for chipset in board_map[board]:
if hashboard.chip_temp == -1:
if hashboard.chip_temp == None:
try:
hashboard.board_temp = api_stats["STATS"][1][f"temp{chipset}"]
hashboard.chip_temp = api_stats["STATS"][1][f"temp2_{chipset}"]

View File

@@ -0,0 +1,22 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners.backends import LUXMiner
from pyasic.miners.types import S9
class LUXMinerS9(LUXMiner, S9):
pass

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .S9 import LUXMinerS9

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .X9 import *

View File

@@ -0,0 +1,25 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners.backends import VNish
from pyasic.miners.types import S17Plus, S17Pro
class VNishS17Plus(VNish, S17Plus):
pass
class VNishS17Pro(VNish, S17Pro):
pass

View File

@@ -0,0 +1,16 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .S17 import VNishS17Plus, VNishS17Pro

View File

@@ -15,13 +15,26 @@
# ------------------------------------------------------------------------------
from pyasic.miners.backends import VNish
from pyasic.miners.types import S19, S19XP, S19a, S19aPro, S19j, S19jPro, S19Pro
from pyasic.miners.types import (
S19,
S19XP,
S19a,
S19aPro,
S19j,
S19jPro,
S19NoPIC,
S19Pro,
)
class VNishS19(VNish, S19):
pass
class VNishS19NoPIC(VNish, S19NoPIC):
pass
class VNishS19Pro(VNish, S19Pro):
pass

View File

@@ -20,6 +20,7 @@ from .S19 import (
VNishS19aPro,
VNishS19j,
VNishS19jPro,
VNishS19NoPIC,
VNishS19Pro,
VNishS19XP,
)

View File

@@ -15,4 +15,5 @@
# ------------------------------------------------------------------------------
from .X3 import *
from .X17 import *
from .X19 import *

View File

@@ -22,5 +22,7 @@ from .btminer import BTMiner
from .cgminer import CGMiner
from .cgminer_avalon import CGMinerAvalon
from .hiveon import Hiveon
from .luxminer import LUXMiner
from .vnish import VNish
from .whatsminer import M2X, M3X, M5X
from .epic import ePIC
from .whatsminer import M2X, M3X, M5X, M6X

View File

@@ -18,7 +18,7 @@ import asyncio
from typing import List, Optional, Union
from pyasic.API import APIError
from pyasic.config import MinerConfig, X19PowerMode
from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData, X19Error
from pyasic.miners.backends.bmminer import BMMiner
@@ -26,11 +26,17 @@ from pyasic.miners.backends.cgminer import CGMiner
from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
ANTMINER_MODERN_DATA_LOC = {
"mac": {"cmd": "get_mac", "kwargs": {}},
"mac": {
"cmd": "get_mac",
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
},
"model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}},
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {}},
"hostname": {
"cmd": "get_hostname",
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
@@ -42,9 +48,20 @@ ANTMINER_MODERN_DATA_LOC = {
"wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}},
"fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}},
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
"errors": {"cmd": "get_errors", "kwargs": {}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
"errors": {"cmd": "get_errors", "kwargs": {"web_summary": {"web": "summary"}}},
"fault_light": {
"cmd": "get_fault_light",
"kwargs": {"web_get_blink_status": {"web": "get_blink_status"}},
},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {
"cmd": "is_mining",
"kwargs": {"web_get_conf": {"web": "get_miner_conf"}},
},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_stats": {"api": "stats"}},
},
}
@@ -63,23 +80,21 @@ class AntminerModern(BMMiner):
async def get_config(self) -> MinerConfig:
data = await self.web.get_miner_conf()
if data:
self.config = MinerConfig().from_raw(data)
self.config = MinerConfig.from_am_modern(data)
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
self.config = config
conf = config.as_x19(user_suffix=user_suffix)
data = await self.web.set_miner_conf(conf)
if data:
if data.get("code") == "M000":
return
for i in range(7):
data = await self.get_config()
if data.as_x19() == conf:
break
await asyncio.sleep(1)
await self.web.set_miner_conf(config.as_am_modern(user_suffix=user_suffix))
# if data:
# if data.get("code") == "M000":
# return
#
# for i in range(7):
# data = await self.get_config()
# if data == self.config:
# break
# await asyncio.sleep(1)
async def fault_light_on(self) -> bool:
data = await self.web.blink(blink=True)
@@ -92,7 +107,7 @@ class AntminerModern(BMMiner):
data = await self.web.blink(blink=False)
if data:
if data.get("code") == "B100":
self.light = True
self.light = False
return self.light
async def reboot(self) -> bool:
@@ -103,31 +118,41 @@ class AntminerModern(BMMiner):
async def stop_mining(self) -> bool:
cfg = await self.get_config()
cfg.miner_mode = X19PowerMode.Sleep
cfg.miner_mode = MiningModeConfig.sleep
await self.send_config(cfg)
return True
async def resume_mining(self) -> bool:
cfg = await self.get_config()
cfg.miner_mode = X19PowerMode.Normal
cfg.miner_mode = MiningModeConfig.normal
await self.send_config(cfg)
return True
async def get_hostname(self) -> Union[str, None]:
try:
data = await self.web.get_system_info()
if data:
return data["hostname"]
except KeyError:
pass
async def get_hostname(self, web_get_system_info: dict = None) -> Union[str, None]:
if not web_get_system_info:
try:
web_get_system_info = await self.web.get_system_info()
except APIError:
pass
async def get_mac(self) -> Union[str, None]:
try:
data = await self.web.get_system_info()
if data:
return data["macaddr"]
except KeyError:
pass
if web_get_system_info:
try:
return web_get_system_info["hostname"]
except KeyError:
pass
async def get_mac(self, web_get_system_info: dict = None) -> Union[str, None]:
if not web_get_system_info:
try:
web_get_system_info = await self.web.get_system_info()
except APIError:
pass
if web_get_system_info:
try:
return web_get_system_info["macaddr"]
except KeyError:
pass
try:
data = await self.web.get_network_info()
@@ -136,12 +161,17 @@ class AntminerModern(BMMiner):
except KeyError:
pass
async def get_errors(self) -> List[MinerErrorData]:
errors = []
data = await self.web.summary()
if data:
async def get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
if not web_summary:
try:
for item in data["SUMMARY"][0]["status"]:
web_summary = await self.web.summary()
except APIError:
pass
errors = []
if web_summary:
try:
for item in web_summary["SUMMARY"][0]["status"]:
try:
if not item["status"] == "s":
errors.append(X19Error(item["msg"]))
@@ -151,15 +181,21 @@ class AntminerModern(BMMiner):
pass
return errors
async def get_fault_light(self) -> bool:
async def get_fault_light(self, web_get_blink_status: dict = None) -> bool:
if self.light:
return self.light
try:
data = await self.web.get_blink_status()
if data:
self.light = data["blink"]
except KeyError:
pass
if not web_get_blink_status:
try:
web_get_blink_status = await self.web.get_blink_status()
except APIError:
pass
if web_get_blink_status:
try:
self.light = web_get_blink_status["blink"]
except KeyError:
pass
return self.light
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
@@ -227,6 +263,36 @@ class AntminerModern(BMMiner):
protocol=protocol,
)
async def is_mining(self, web_get_conf: dict = None) -> Optional[bool]:
if not web_get_conf:
try:
web_get_conf = await self.web.get_miner_conf()
except APIError:
pass
if web_get_conf:
try:
if web_get_conf["bitmain-work-mode"].isdigit():
return (
False if int(web_get_conf["bitmain-work-mode"]) == 1 else True
)
return False
except LookupError:
pass
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass
ANTMINER_OLD_DATA_LOC = {
"mac": {"cmd": "get_mac", "kwargs": {}},
@@ -257,6 +323,14 @@ ANTMINER_OLD_DATA_LOC = {
"kwargs": {"web_get_blink_status": {"web": "get_blink_status"}},
},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {
"cmd": "is_mining",
"kwargs": {"web_get_conf": {"web": "get_miner_conf"}},
},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_stats": {"api": "stats"}},
},
}
@@ -273,11 +347,12 @@ class AntminerOld(CGMiner):
async def get_config(self) -> MinerConfig:
data = await self.web.get_miner_conf()
if data:
self.config = MinerConfig().from_raw(data)
self.config = MinerConfig.from_am_old(data)
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
await self.web.set_miner_conf(config.as_x17(user_suffix=user_suffix))
self.config = config
await self.web.set_miner_conf(config.as_am_old(user_suffix=user_suffix))
async def get_mac(self) -> Union[str, None]:
try:
@@ -353,7 +428,7 @@ class AntminerOld(CGMiner):
except APIError:
pass
fans_data = [Fan(), Fan(), Fan(), Fan()]
fans_data = [Fan() for _ in range(self.fan_count)]
if api_stats:
try:
fan_offset = -1
@@ -367,8 +442,8 @@ class AntminerOld(CGMiner):
fan_offset = 3
for fan in range(self.fan_count):
fans_data[fan] = Fan(
api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
fans_data[fan].speed = api_stats["STATS"][1].get(
f"fan{fan_offset+fan}", 0
)
except (KeyError, IndexError):
pass
@@ -425,3 +500,41 @@ class AntminerOld(CGMiner):
pass
return hashboards
async def is_mining(self, web_get_conf: dict = None) -> Optional[bool]:
if not web_get_conf:
try:
web_get_conf = await self.web.get_miner_conf()
except APIError:
pass
if web_get_conf:
try:
return False if int(web_get_conf["bitmain-work-mode"]) == 1 else True
except LookupError:
pass
api_summary = None
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary is not None:
if not api_summary == {}:
return True
else:
return False
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass

View File

@@ -44,6 +44,8 @@ BFGMINER_DATA_LOC = {
"errors": {"cmd": "get_errors", "kwargs": {}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {}},
}
@@ -70,7 +72,7 @@ class BFGMiner(BaseMiner):
except APIError:
return self.config
self.config = MinerConfig().from_api(pools["POOLS"])
self.config = MinerConfig.from_api(pools)
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
@@ -247,14 +249,16 @@ class BFGMiner(BaseMiner):
for fan_num in range(0, 8, 4):
for _f_num in range(4):
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}")
if f and not f == 0 and fan_offset == -1:
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}", 0)
if not f == 0 and fan_offset == -1:
fan_offset = fan_num
if fan_offset == -1:
fan_offset = 1
for fan in range(self.fan_count):
fans_data[fan] = api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
fans_data[fan] = api_stats["STATS"][1].get(
f"fan{fan_offset+fan}", 0
)
except (KeyError, IndexError):
pass
fans = [Fan(speed=d) if d else Fan() for d in fans_data]
@@ -316,3 +320,9 @@ class BFGMiner(BaseMiner):
return round(ideal_rate, 2)
except (KeyError, IndexError):
pass
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
return None
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
return None

View File

@@ -13,11 +13,12 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from typing import List
from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data import HashBoard
from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.miners.backends import BFGMiner
from pyasic.web.goldshell import GoldshellWebAPI
@@ -47,6 +48,8 @@ GOLDSHELL_DATA_LOC = {
"errors": {"cmd": "get_errors", "kwargs": {}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {}},
}
@@ -61,7 +64,14 @@ class BFGMinerGoldshell(BFGMiner):
self.data_locations = GOLDSHELL_DATA_LOC
async def get_config(self) -> MinerConfig:
return MinerConfig().from_raw(await self.web.pools())
# get pool data
try:
pools = await self.web.pools()
except APIError:
return self.config
self.config = MinerConfig.from_goldshell(pools)
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
pools_data = await self.web.pools()
@@ -77,7 +87,7 @@ class BFGMinerGoldshell(BFGMiner):
self.config = config
# send them back 1 at a time
for pool in config.as_goldshell(user_suffix=user_suffix):
for pool in config.as_goldshell(user_suffix=user_suffix)["pools"]:
await self.web.newpool(
url=pool["url"], user=pool["user"], password=pool["pass"]
)
@@ -136,7 +146,7 @@ class BFGMinerGoldshell(BFGMiner):
except KeyError:
pass
else:
print(self, api_devs)
logger.error(self, api_devs)
if not api_devdetails:
try:
@@ -154,6 +164,12 @@ class BFGMinerGoldshell(BFGMiner):
except KeyError:
pass
else:
print(self, api_devdetails)
logger.error(self, api_devdetails)
return hashboards
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
return None
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
return None

View File

@@ -45,6 +45,11 @@ BMMINER_DATA_LOC = {
"errors": {"cmd": "get_errors", "kwargs": {}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_stats": {"api": "stats"}},
},
}
@@ -99,7 +104,7 @@ class BMMiner(BaseMiner):
except APIError:
return self.config
self.config = MinerConfig().from_api(pools["POOLS"])
self.config = MinerConfig.from_api(pools)
return self.config
async def reboot(self) -> bool:
@@ -230,7 +235,22 @@ class BMMiner(BaseMiner):
if board_offset == -1:
board_offset = 1
for i in range(board_offset, board_offset + self.ideal_hashboards):
real_slots = []
for i in range(board_offset, board_offset + 4):
try:
key = f"chain_acs{i}"
if boards[1].get(key, "") != "":
real_slots.append(i)
except LookupError:
pass
if len(real_slots) < 3:
real_slots = list(
range(board_offset, board_offset + self.ideal_hashboards)
)
for i in real_slots:
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
)
@@ -254,7 +274,7 @@ class BMMiner(BaseMiner):
if (not chips) or (not chips > 0):
hashboard.missing = True
hashboards.append(hashboard)
except (IndexError, KeyError, ValueError, TypeError):
except (LookupError, ValueError, TypeError):
pass
return hashboards
@@ -275,24 +295,25 @@ class BMMiner(BaseMiner):
except APIError:
pass
fans_data = [None, None, None, None]
fans = [Fan() for _ in range(self.fan_count)]
if api_stats:
try:
fan_offset = -1
for fan_num in range(1, 8, 4):
for _f_num in range(4):
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}")
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}", 0)
if f and not f == 0 and fan_offset == -1:
fan_offset = fan_num
if fan_offset == -1:
fan_offset = 1
for fan in range(self.fan_count):
fans_data[fan] = api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
fans[fan].speed = api_stats["STATS"][1].get(
f"fan{fan_offset+fan}", 0
)
except (KeyError, IndexError):
pass
fans = [Fan(speed=d) if d else Fan() for d in fans_data]
return fans
@@ -351,3 +372,19 @@ class BMMiner(BaseMiner):
return round(ideal_rate, 2)
except (KeyError, IndexError):
pass
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
return None
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
if not api_stats:
try:
api_stats = await self.web.get_miner_conf()
except APIError:
pass
if api_stats:
try:
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass

View File

@@ -13,8 +13,9 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
import logging
import time
from collections import namedtuple
from typing import List, Optional, Tuple, Union
@@ -22,6 +23,7 @@ import toml
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
from pyasic.errors import APIError
@@ -29,7 +31,12 @@ from pyasic.miners.base import BaseMiner
from pyasic.web.bosminer import BOSMinerWebAPI
BOSMINER_DATA_LOC = {
"mac": {"cmd": "get_mac", "kwargs": {}},
"mac": {
"cmd": "get_mac",
"kwargs": {
"web_net_conf": {"web": "/cgi-bin/luci/admin/network/iface_status/lan"}
},
},
"model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {
"cmd": "get_api_ver",
@@ -155,7 +162,7 @@ BOSMINER_DATA_LOC = {
"config": {
"... on BosminerConfig": {
"groups": {
"pools": {"urluser": None},
"pools": {"url": None, "user": None},
"strategy": {
"... on QuotaStrategy": {"quota": None}
},
@@ -167,15 +174,23 @@ BOSMINER_DATA_LOC = {
},
},
},
"is_mining": {
"cmd": "is_mining",
"kwargs": {"api_devdetails": {"api": "devdetails"}},
},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_summary": {"api": "summary"}},
},
}
class BOSMiner(BaseMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
def __init__(self, ip: str, api_ver: str = "0.0.0", boser: bool = None) -> None:
super().__init__(ip)
# interfaces
self.api = BOSMinerAPI(ip, api_ver)
self.web = BOSMinerWebAPI(ip)
self.web = BOSMinerWebAPI(ip, boser=boser)
# static data
self.api_type = "BOSMiner"
@@ -192,8 +207,8 @@ class BOSMiner(BaseMiner):
result = None
try:
conn = await self._get_ssh_connection()
except ConnectionError:
conn = await asyncio.wait_for(self._get_ssh_connection(), timeout=10)
except (ConnectionError, asyncio.TimeoutError):
return None
# open an ssh connection
@@ -284,23 +299,13 @@ class BOSMiner(BaseMiner):
return False
async def get_config(self) -> MinerConfig:
"""Gets the config for the miner and sets it as `self.config`.
Returns:
The config from `self.config`.
"""
logging.debug(f"{self}: Getting config.")
conn = None
try:
conn = await self._get_ssh_connection()
except ConnectionError:
try:
pools = await self.api.pools()
except APIError:
return self.config
if pools:
self.config = MinerConfig().from_api(pools["POOLS"])
return self.config
conn = None
if conn:
async with conn:
# good ol' BBB compatibility :/
@@ -308,21 +313,42 @@ class BOSMiner(BaseMiner):
(await conn.run("cat /etc/bosminer.toml")).stdout
)
logging.debug(f"{self}: Converting config file.")
cfg = MinerConfig().from_raw(toml_data)
cfg = MinerConfig.from_bosminer(toml_data)
self.config = cfg
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
logging.debug(f"{self}: Sending config.")
self.config = config
toml_conf = config.as_bos(
model=self.model.replace(" (BOS)", ""), user_suffix=user_suffix
if self.web.grpc is not None:
try:
await self._send_config_grpc(config, user_suffix)
return
except:
pass
await self._send_config_bosminer(config, user_suffix)
async def _send_config_grpc(self, config: MinerConfig, user_suffix: str = None):
raise NotImplementedError
mining_mode = config.mining_mode
async def _send_config_bosminer(self, config: MinerConfig, user_suffix: str = None):
toml_conf = toml.dumps(
{
"format": {
"version": "1.2+",
"generator": "pyasic",
"model": f"{self.make.replace('Miner', 'miner')} {self.model.replace(' (BOS)', '').replace('j', 'J')}",
"timestamp": int(time.time()),
},
**config.as_bosminer(user_suffix=user_suffix),
}
)
try:
conn = await self._get_ssh_connection()
except ConnectionError:
return None
except ConnectionError as e:
raise APIError("SSH connection failed when sending config.") from e
async with conn:
# BBB check because bitmain suxx
bbb_check = await conn.run(
@@ -352,7 +378,9 @@ class BOSMiner(BaseMiner):
async def set_power_limit(self, wattage: int) -> bool:
try:
cfg = await self.get_config()
cfg.autotuning_wattage = wattage
if cfg is None:
return False
cfg.mining_mode = MiningModePowerTune(wattage)
await self.send_config(cfg)
except Exception as e:
logging.warning(f"{self} set_power_limit: {e}")
@@ -360,17 +388,85 @@ class BOSMiner(BaseMiner):
else:
return True
async def set_static_ip(
self,
ip: str,
dns: str,
gateway: str,
subnet_mask: str = "255.255.255.0",
):
cfg_data_lan = (
"config interface 'lan'\n\toption type 'bridge'\n\toption ifname 'eth0'\n\toption proto 'static'\n\toption ipaddr '"
+ ip
+ "'\n\toption netmask '"
+ subnet_mask
+ "'\n\toption gateway '"
+ gateway
+ "'\n\toption dns '"
+ dns
+ "'"
)
data = await self.send_ssh_command("cat /etc/config/network")
split_data = data.split("\n\n")
for idx in range(len(split_data)):
if "config interface 'lan'" in split_data[idx]:
split_data[idx] = cfg_data_lan
config = "\n\n".join(split_data)
conn = await self._get_ssh_connection()
async with conn:
await conn.run("echo '" + config + "' > /etc/config/network")
async def set_dhcp(self):
cfg_data_lan = "config interface 'lan'\n\toption type 'bridge'\n\toption ifname 'eth0'\n\toption proto 'dhcp'"
data = await self.send_ssh_command("cat /etc/config/network")
split_data = data.split("\n\n")
for idx in range(len(split_data)):
if "config interface 'lan'" in split_data[idx]:
split_data[idx] = cfg_data_lan
config = "\n\n".join(split_data)
conn = await self._get_ssh_connection()
async with conn:
await conn.run("echo '" + config + "' > /etc/config/network")
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_mac(self) -> Optional[str]:
result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
if result:
return result.upper().strip()
async def get_mac(self, web_net_conf: Union[dict, list] = None) -> Optional[str]:
if not web_net_conf:
try:
web_net_conf = await self.web.send_command(
"/cgi-bin/luci/admin/network/iface_status/lan"
)
except APIError:
pass
if isinstance(web_net_conf, dict):
if "/cgi-bin/luci/admin/network/iface_status/lan" in web_net_conf.keys():
web_net_conf = web_net_conf[
"/cgi-bin/luci/admin/network/iface_status/lan"
]
if web_net_conf:
try:
return web_net_conf[0]["macaddr"]
except LookupError:
pass
# could use ssh, but its slow and buggy
# result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
# if result:
# return result.upper().strip()
async def get_model(self) -> Optional[str]:
return self.model + " (BOS)"
if self.model is not None:
return self.model + " (BOS)"
return "? (BOS)"
async def get_version(
self, api_version: dict = None, graphql_version: dict = None
@@ -412,8 +508,8 @@ class BOSMiner(BaseMiner):
if graphql_version:
try:
fw_ver = graphql_version["bos"]["info"]["version"]["full"]
except KeyError:
fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"]
except (KeyError, TypeError):
pass
if not fw_ver:
@@ -440,9 +536,9 @@ class BOSMiner(BaseMiner):
if graphql_hostname:
try:
hostname = graphql_hostname["bos"]["hostname"]
hostname = graphql_hostname["data"]["bos"]["hostname"]
return hostname
except KeyError:
except (TypeError, KeyError):
pass
try:
@@ -461,7 +557,6 @@ class BOSMiner(BaseMiner):
async def get_hashrate(
self, api_summary: dict = None, graphql_hashrate: dict = None
) -> Optional[float]:
# get hr from graphql
if not graphql_hashrate:
try:
@@ -475,14 +570,14 @@ class BOSMiner(BaseMiner):
try:
return round(
float(
graphql_hashrate["bosminer"]["info"]["workSolver"][
graphql_hashrate["data"]["bosminer"]["info"]["workSolver"][
"realHashrate"
]["mhs1M"]
/ 1000000
),
2,
)
except (KeyError, IndexError, ValueError):
except (LookupError, ValueError, TypeError):
pass
# get hr from API
@@ -533,14 +628,19 @@ class BOSMiner(BaseMiner):
if graphql_boards:
try:
boards = graphql_boards["bosminer"]["info"]["workSolver"][
boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][
"childSolvers"
]
except (KeyError, IndexError):
except (TypeError, LookupError):
boards = None
if boards:
offset = 6 if int(boards[0]["name"]) in [6, 7, 8] else 0
b_names = [int(b["name"]) for b in boards]
offset = 0
if 3 in b_names:
offset = 1
elif 6 in b_names or 7 in b_names or 8 in b_names:
offset = 6
for hb in boards:
_id = int(hb["name"]) - offset
board = hashboards[_id]
@@ -641,13 +741,12 @@ class BOSMiner(BaseMiner):
)
except APIError:
pass
if graphql_wattage:
if graphql_wattage is not None:
try:
return graphql_wattage["bosminer"]["info"]["workSolver"]["power"][
"approxConsumptionW"
]
except KeyError:
return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][
"power"
]["approxConsumptionW"]
except (LookupError, TypeError):
pass
if not api_tunerstatus:
@@ -677,10 +776,10 @@ class BOSMiner(BaseMiner):
if graphql_wattage_limit:
try:
return graphql_wattage_limit["bosminer"]["info"]["workSolver"]["power"][
"limitW"
]
except KeyError:
return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][
"power"
]["limitW"]
except (LookupError, TypeError):
pass
if not api_tunerstatus:
@@ -705,17 +804,20 @@ class BOSMiner(BaseMiner):
)
except APIError:
pass
if graphql_fans:
fans = {"fan_1": Fan(), "fan_2": Fan(), "fan_3": Fan(), "fan_4": Fan()}
if graphql_fans.get("data"):
fans = []
for n in range(self.fan_count):
try:
fans[f"fan_{n + 1}"].speed = graphql_fans["bosminer"]["info"][
"fans"
][n]["rpm"]
except KeyError:
fans.append(
Fan(
speed=graphql_fans["data"]["bosminer"]["info"]["fans"][n][
"rpm"
]
)
)
except (LookupError, TypeError):
pass
return [fans["fan_1"], fans["fan_2"], fans["fan_3"], fans["fan_4"]]
return fans
if not api_fans:
try:
@@ -724,14 +826,14 @@ class BOSMiner(BaseMiner):
pass
if api_fans:
fans = {"fan_1": Fan(), "fan_2": Fan(), "fan_3": Fan(), "fan_4": Fan()}
fans = []
for n in range(self.fan_count):
try:
fans[f"fan_{n + 1}"].speed = api_fans["FANS"][n]["RPM"]
fans.append(Fan(api_fans["FANS"][n]["RPM"]))
except (IndexError, KeyError):
pass
return [fans["fan_1"], fans["fan_2"], fans["fan_3"], fans["fan_4"]]
return [Fan(), Fan(), Fan(), Fan()]
return fans
return [Fan() for _ in range(self.fan_count)]
async def get_fan_psu(self) -> Optional[int]:
return None
@@ -761,7 +863,7 @@ class BOSMiner(BaseMiner):
if graphql_pools:
groups = []
try:
g = graphql_pools["bosminer"]["config"]["groups"]
g = graphql_pools["data"]["bosminer"]["config"]["groups"]
for group in g:
pools = {"quota": group["strategy"]["quota"]}
for i, pool in enumerate(group["pools"]):
@@ -773,7 +875,7 @@ class BOSMiner(BaseMiner):
pools[f"pool_{i + 1}_user"] = pool["user"]
groups.append(pools)
return groups
except KeyError:
except (KeyError, TypeError):
pass
if not api_pools:
@@ -850,10 +952,10 @@ class BOSMiner(BaseMiner):
if graphql_errors:
errors = []
try:
boards = graphql_errors["bosminer"]["info"]["workSolver"][
boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][
"childSolvers"
]
except (KeyError, IndexError):
except (LookupError, TypeError):
boards = None
if boards:
@@ -944,19 +1046,22 @@ class BOSMiner(BaseMiner):
# get light through GraphQL
if graphql_fault_light:
try:
self.light = graphql_fault_light["bos"]["faultLight"]
self.light = graphql_fault_light["data"]["bos"]["faultLight"]
return self.light
except (TypeError, KeyError, ValueError, IndexError):
except (TypeError, ValueError, LookupError):
pass
# get light via ssh if that fails (10x slower)
data = (
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
).strip()
self.light = False
if data == "50":
self.light = True
return self.light
try:
data = (
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
).strip()
self.light = False
if data == "50":
self.light = True
return self.light
except (TypeError, AttributeError):
return self.light
async def get_nominal_hashrate(self, api_devs: dict = None) -> Optional[float]:
if not api_devs:
@@ -983,3 +1088,31 @@ class BOSMiner(BaseMiner):
)
except (IndexError, KeyError):
pass
async def is_mining(self, api_devdetails: dict = None) -> Optional[bool]:
if not api_devdetails:
try:
api_devdetails = await self.api.send_command(
"devdetails", ignore_errors=True, allow_warning=False
)
except APIError:
pass
if api_devdetails:
try:
return not api_devdetails["STATUS"][0]["Msg"] == "Unavailable"
except LookupError:
pass
async def get_uptime(self, api_summary: dict = None) -> Optional[int]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return int(api_summary["SUMMARY"][0]["Elapsed"])
except LookupError:
pass

View File

@@ -150,3 +150,9 @@ class BOSMinerOld(BOSMiner):
async def get_data(self, allow_warning: bool = False, **kwargs) -> MinerData:
return MinerData(ip=str(self.ip))
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
return None
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
return None

View File

@@ -15,12 +15,11 @@
# ------------------------------------------------------------------------------
import logging
import warnings
from collections import namedtuple
from typing import List, Optional, Tuple
from pyasic.API.btminer import BTMinerAPI
from pyasic.config import MinerConfig
from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData, WhatsminerError
from pyasic.errors import APIError
@@ -88,6 +87,11 @@ BTMINER_DATA_LOC = {
"kwargs": {"api_get_miner_info": {"api": "get_miner_info"}},
},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {"api_status": {"api": "status"}}},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_summary": {"api": "summary"}},
},
}
@@ -193,44 +197,68 @@ class BTMiner(BaseMiner):
try:
await self.api.update_pools(**pools_conf)
if conf["mode"] == "normal":
await self.api.set_normal_power()
elif conf["mode"] == "high":
await self.api.set_high_power()
elif conf["mode"] == "low":
await self.api.set_low_power()
elif conf["mode"] == "power_tuning":
await self.api.adjust_power_limit(conf["power_tuning"]["wattage"])
except APIError:
pass
try:
await self.api.adjust_power_limit(conf["wattage"])
except APIError:
# cannot set wattage
# cannot update, no API access usually
pass
async def get_config(self) -> MinerConfig:
pools = None
summary = None
cfg = MinerConfig()
status = None
try:
data = await self.api.multicommand("pools", "summary")
data = await self.api.multicommand("pools", "summary", "status")
pools = data["pools"][0]
summary = data["summary"][0]
status = data["status"][0]
except APIError as e:
logging.warning(e)
except LookupError:
pass
if pools:
if "POOLS" in pools:
cfg = cfg.from_api(pools["POOLS"])
if pools is not None:
cfg = MinerConfig.from_api(pools)
else:
# somethings wrong with the miner
warnings.warn(
f"Failed to gather pool config for miner: {self}, miner did not return pool information."
)
if summary:
if "SUMMARY" in summary:
if wattage := summary["SUMMARY"][0].get("Power Limit"):
cfg.autotuning_wattage = wattage
cfg = MinerConfig()
self.config = cfg
is_mining = await self.is_mining(status)
if not is_mining:
cfg.mining_mode = MiningModeConfig.sleep()
return cfg
return self.config
if summary is not None:
mining_mode = None
try:
mining_mode = summary["SUMMARY"][0]["Power Mode"]
except LookupError:
pass
if mining_mode == "High":
cfg.mining_mode = MiningModeConfig.high()
return cfg
elif mining_mode == "Low":
cfg.mining_mode = MiningModeConfig.low()
return cfg
try:
power_lim = summary["SUMMARY"][0]["Power Limit"]
except LookupError:
power_lim = None
if power_lim is None:
cfg.mining_mode = MiningModeConfig.normal()
return cfg
cfg.mining_mode = MiningModeConfig.power_tuning(power_lim)
self.config = cfg
return self.config
async def set_power_limit(self, wattage: int) -> bool:
try:
@@ -381,7 +409,6 @@ class BTMiner(BaseMiner):
pass
async def get_hashboards(self, api_devs: dict = None) -> List[HashBoard]:
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
@@ -437,7 +464,8 @@ class BTMiner(BaseMiner):
if api_summary:
try:
return api_summary["SUMMARY"][0]["Power"]
wattage = api_summary["SUMMARY"][0]["Power"]
return wattage if not wattage == -1 else None
except (KeyError, IndexError):
pass
@@ -463,15 +491,13 @@ class BTMiner(BaseMiner):
except APIError:
pass
fans = [Fan(), Fan(), Fan(), Fan()]
fans = [Fan() for _ in range(self.fan_count)]
if api_summary:
try:
if self.fan_count > 0:
fans = [
Fan(api_summary["SUMMARY"][0]["Fan Speed In"]),
Fan(api_summary["SUMMARY"][0]["Fan Speed Out"]),
Fan(),
Fan(),
Fan(api_summary["SUMMARY"][0].get("Fan Speed In", 0)),
Fan(api_summary["SUMMARY"][0].get("Fan Speed Out", 0)),
]
except (KeyError, IndexError):
pass
@@ -618,3 +644,35 @@ class BTMiner(BaseMiner):
async def set_hostname(self, hostname: str):
await self.api.set_hostname(hostname)
async def is_mining(self, api_status: dict = None) -> Optional[bool]:
if not api_status:
try:
api_status = await self.api.status()
except APIError:
pass
if api_status:
try:
if api_status["Msg"].get("btmineroff"):
try:
await self.api.devdetails()
except APIError:
return False
return True
return True if api_status["Msg"]["mineroff"] == "false" else False
except LookupError:
pass
async def get_uptime(self, api_summary: dict = None) -> Optional[int]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return int(api_summary["SUMMARY"][0]["Elapsed"])
except LookupError:
pass

View File

@@ -45,6 +45,11 @@ CGMINER_DATA_LOC = {
"errors": {"cmd": "get_errors", "kwargs": {}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_stats": {"api": "stats"}},
},
}
@@ -138,10 +143,13 @@ class CGMiner(BaseMiner):
return True
async def get_config(self) -> MinerConfig:
api_pools = await self.api.pools()
# get pool data
try:
pools = await self.api.pools()
except APIError:
return self.config
if api_pools:
self.config = MinerConfig().from_api(api_pools["POOLS"])
self.config = MinerConfig.from_api(pools)
return self.config
async def fault_light_off(self) -> bool:
@@ -296,7 +304,7 @@ class CGMiner(BaseMiner):
except APIError:
pass
fans_data = [Fan(), Fan(), Fan(), Fan()]
fans = [Fan() for _ in range(self.fan_count)]
if api_stats:
try:
fan_offset = -1
@@ -310,12 +318,12 @@ class CGMiner(BaseMiner):
fan_offset = 1
for fan in range(self.fan_count):
fans_data[fan] = Fan(
api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
fans[fan].speed = api_stats["STATS"][1].get(
f"fan{fan_offset+fan}", 0
)
except (KeyError, IndexError):
pass
return fans_data
return fans
async def get_fan_psu(self) -> Optional[int]:
return None
@@ -375,3 +383,19 @@ class CGMiner(BaseMiner):
return round(ideal_rate, 2)
except (KeyError, IndexError):
pass
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
return None
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass

View File

@@ -29,21 +29,29 @@ AVALON_DATA_LOC = {
"model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}},
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {"mac": {"web": "mac"}}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {"mac": {"api": "version"}}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_devs": {"api": "devs"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"kwargs": {"api_stats": {"api": "stats"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"api_stats": {"api": "stats"}}},
"env_temp": {"cmd": "get_env_temp", "kwargs": {}},
"env_temp": {"cmd": "get_env_temp", "kwargs": {"api_stats": {"api": "stats"}}},
"wattage": {"cmd": "get_wattage", "kwargs": {}},
"wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}},
"wattage_limit": {
"cmd": "get_wattage_limit",
"kwargs": {"api_stats": {"api": "stats"}},
},
"fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}},
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
"errors": {"cmd": "get_errors", "kwargs": {}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
"fault_light": {
"cmd": "get_fault_light",
"kwargs": {"api_stats": {"api": "stats"}},
},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {}},
}
@@ -92,17 +100,17 @@ class CGMinerAvalon(CGMiner):
return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
self.config = config
return None
logging.debug(f"{self}: Sending config.") # noqa - This doesnt work...
conf = config.as_avalon(user_suffix=user_suffix)
try:
data = await self.api.ascset( # noqa
0, "setpool", f"root,root,{conf}"
) # this should work but doesn't
except APIError:
pass
pass
# self.config = config
# return None
# logging.debug(f"{self}: Sending config.") # noqa - This doesnt work...
# conf = config.as_avalon(user_suffix=user_suffix)
# try:
# data = await self.api.ascset( # noqa
# 0, "setpool", f"root,root,{conf}"
# ) # this should work but doesn't
# except APIError:
# pass
# return data
@staticmethod
@@ -115,8 +123,17 @@ class CGMinerAvalon(CGMiner):
data = item.replace("]", "").split("[")
data_list = [i.split(": ") for i in data[1].strip().split(", ")]
data_dict = {}
for key, val in [tuple(item) for item in data_list]:
data_dict[key] = val
try:
for key, val in [tuple(item) for item in data_list]:
data_dict[key] = val
except ValueError:
# --avalon args
for arg_item in data_list:
item_data = arg_item[0].split(" ")
for idx in range(len(item_data)):
if idx % 2 == 0 or idx == 0:
data_dict[item_data[idx]] = item_data[idx + 1]
raw_data = [data[0].strip(), data_dict]
else:
raw_data = [
@@ -162,22 +179,23 @@ class CGMinerAvalon(CGMiner):
pass
async def get_hostname(self, mac: str = None) -> Optional[str]:
if not mac:
mac = await self.get_mac()
return None
# if not mac:
# mac = await self.get_mac()
#
# if mac:
# return f"Avalon{mac.replace(':', '')[-6:]}"
if mac:
return f"Avalon{mac.replace(':', '')[-6:]}"
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
if not api_summary:
async def get_hashrate(self, api_devs: dict = None) -> Optional[float]:
if not api_devs:
try:
api_summary = await self.api.summary()
api_devs = await self.api.devs()
except APIError:
pass
if api_summary:
if api_devs:
try:
return round(float(api_summary["SUMMARY"][0]["MHS 1m"] / 1000000), 2)
return round(float(api_devs["DEVS"][0]["MHS 1m"] / 1000000), 2)
except (KeyError, IndexError, ValueError, TypeError):
pass
@@ -195,38 +213,87 @@ class CGMinerAvalon(CGMiner):
if api_stats:
try:
stats_data = api_stats[0].get("STATS")
if stats_data:
for key in stats_data[0].keys():
if key.startswith("MM ID"):
raw_data = self.parse_stats(stats_data[0][key])
for board in range(self.ideal_hashboards):
chip_temp = raw_data.get("MTmax")
if chip_temp:
hashboards[board].chip_temp = chip_temp[board]
temp = raw_data.get("MTavg")
if temp:
hashboards[board].temp = temp[board]
chips = raw_data.get(f"PVT_T{board}")
if chips:
hashboards[board].chips = len(
[item for item in chips if not item == "0"]
)
unparsed_stats = api_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
except (IndexError, KeyError, ValueError, TypeError):
pass
return hashboards
for board in range(self.ideal_hashboards):
try:
hashboards[board].chip_temp = int(parsed_stats["MTmax"][board])
except LookupError:
pass
try:
board_hr = parsed_stats["MGHS"][board]
hashboards[board].hashrate = round(float(board_hr) / 1000, 2)
except LookupError:
pass
try:
hashboards[board].temp = int(parsed_stats["MTavg"][board])
except LookupError:
pass
try:
chip_data = parsed_stats[f"PVT_T{board}"]
hashboards[board].missing = False
if chip_data:
hashboards[board].chips = len(
[item for item in chip_data if not item == "0"]
)
except LookupError:
pass
return hashboards
async def get_env_temp(self) -> Optional[float]:
return None
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
unparsed_stats = api_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
return round(float(parsed_stats["GHSmm"]) / 1000, 2)
except (IndexError, KeyError, ValueError, TypeError):
pass
async def get_env_temp(self, api_stats: dict = None) -> Optional[float]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
unparsed_stats = api_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
return float(parsed_stats["Temp"])
except (IndexError, KeyError, ValueError, TypeError):
pass
async def get_wattage(self) -> Optional[int]:
return None
async def get_wattage_limit(self) -> Optional[int]:
return None
async def get_wattage_limit(self, api_stats: dict = None) -> Optional[int]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
unparsed_stats = api_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
return int(parsed_stats["MPO"])
except (IndexError, KeyError, ValueError, TypeError):
pass
async def get_fans(self, api_stats: dict = None) -> List[Fan]:
if not api_stats:
@@ -235,19 +302,19 @@ class CGMinerAvalon(CGMiner):
except APIError:
pass
fans_data = [Fan(), Fan(), Fan(), Fan()]
fans_data = [Fan() for _ in range(self.fan_count)]
if api_stats:
try:
stats_data = api_stats[0].get("STATS")
if stats_data:
for key in stats_data[0].keys():
if key.startswith("MM ID"):
raw_data = self.parse_stats(stats_data[0][key])
for fan in range(self.fan_count):
fans_data[fan] = Fan(int(raw_data[f"Fan{fan + 1}"]))
except (KeyError, IndexError, ValueError, TypeError):
pass
unparsed_stats = api_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
except LookupError:
return fans_data
for fan in range(self.fan_count):
try:
fans_data[fan].speed = int(parsed_stats[f"Fan{fan + 1}"])
except (IndexError, KeyError, ValueError, TypeError):
pass
return fans_data
async def get_pools(self, api_pools: dict = None) -> List[dict]:
@@ -279,13 +346,34 @@ class CGMinerAvalon(CGMiner):
async def get_errors(self) -> List[MinerErrorData]:
return []
async def get_fault_light(self) -> bool:
async def get_fault_light(self, api_stats: dict = None) -> bool: # noqa
if self.light:
return self.light
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
unparsed_stats = api_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
led = int(parsed_stats["Led"])
return True if led == 1 else False
except (IndexError, KeyError, ValueError, TypeError):
pass
try:
data = await self.api.ascset(0, "led", "1-255")
except APIError:
return False
if data["STATUS"][0]["Msg"] == "ASC 0 set info: LED[1]":
return True
try:
if data["STATUS"][0]["Msg"] == "ASC 0 set info: LED[1]":
return True
except LookupError:
pass
return False
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
return None

View File

@@ -0,0 +1,310 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from typing import List, Optional, Tuple, Union
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData, X19Error
from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.miners.backends.bmminer import BMMiner
from pyasic.web.epic import ePICWebAPI
EPIC_DATA_LOC = {
"mac": {"cmd": "get_mac", "kwargs": {"web_summary": {"web": "network"}}},
"model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}},
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"web_summary": {"web": "summary"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {"web_summary": {"web": "summary"}}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"web_summary": {"web": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"kwargs": {"web_summary": {"web": "summary"}},
},
"hashboards": {
"cmd": "get_hashboards",
"kwargs": {
"web_summary": {"web": "summary"},
"web_hashrate": {"web": "hashrate"},
},
},
"env_temp": {"cmd": "get_env_temp", "kwargs": {}},
"wattage": {"cmd": "get_wattage", "kwargs": {"web_summary": {"web": "summary"}}},
"fans": {"cmd": "get_fans", "kwargs": {"web_summary": {"web": "summary"}}},
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
"fault_light": {
"cmd": "get_fault_light",
"kwargs": {"web_summary": {"web": "summary"}},
},
"pools": {"cmd": "get_pools", "kwargs": {"web_summary": {"web": "summary"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {"web_summary": {"web": "summary"}}},
"errors": {"cmd": "get_errors", "kwargs": {"web_summary": {"web": "summary"}}},
}
class ePIC(BMMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver)
# interfaces
self.web = ePICWebAPI(ip)
# static data
self.api_type = "ePIC"
# data gathering locations
self.data_locations = EPIC_DATA_LOC
async def get_model(self) -> Optional[str]:
if self.model is not None:
return self.model + " (ePIC)"
return "? (ePIC)"
async def restart_backend(self) -> bool:
data = await self.web.restart_epic()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def stop_mining(self) -> bool:
data = await self.web.stop_mining()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def resume_mining(self) -> bool:
data = await self.web.resume_mining()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def reboot(self) -> bool:
data = await self.web.reboot()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def get_mac(self, web_summary: dict = None) -> str:
if not web_summary:
web_summary = await self.web.network()
if web_summary:
try:
for network in web_summary:
mac = web_summary[network]["mac_address"]
return mac
except KeyError:
pass
async def get_hostname(self, web_summary: dict = None) -> str:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
hostname = web_summary["Hostname"]
return hostname
except KeyError:
pass
async def get_wattage(self, web_summary: dict = None) -> Optional[int]:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
wattage = web_summary["Power Supply Stats"]["Input Power"]
wattage = round(wattage)
return wattage
except KeyError:
pass
async def get_hashrate(self, web_summary: dict = None) -> Optional[float]:
# get hr from API
if not web_summary:
try:
web_summary = await self.web.summary()
except APIError:
pass
if web_summary:
try:
hashrate = 0
if web_summary["HBs"] != None:
for hb in web_summary["HBs"]:
hashrate += hb["Hashrate"][0]
return round(float(float(hashrate / 1000000)), 2)
except (LookupError, ValueError, TypeError) as e:
logger.error(e)
pass
async def get_nominal_hashrate(self, web_summary: dict = None) -> Optional[float]:
# get hr from API
if not web_summary:
try:
web_summary = await self.web.summary()
except APIError:
pass
if web_summary:
try:
hashrate = 0
if web_summary["HBs"] != None:
for hb in web_summary["HBs"]:
if hb["Hashrate"][1] == 0:
ideal = 1.0
else:
ideal = hb["Hashrate"][1] / 100
hashrate += hb["Hashrate"][0] / ideal
return round(float(float(hashrate / 1000000)), 2)
except (IndexError, KeyError, ValueError, TypeError) as e:
logger.error(e)
pass
async def get_fw_ver(self, web_summary: dict = None) -> Optional[str]:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
fw_ver = web_summary["Software"]
fw_ver = fw_ver.split(" ")[1].replace("v", "")
return fw_ver
except KeyError:
pass
async def get_fans(self, web_summary: dict = None) -> List[Fan]:
if not web_summary:
try:
web_summary = await self.web.summary()
except APIError:
pass
fans = []
if web_summary:
for fan in web_summary["Fans Rpm"]:
try:
fans.append(Fan(web_summary["Fans Rpm"][fan]))
except (LookupError, ValueError, TypeError):
fans.append(Fan())
return fans
async def get_hashboards(
self, web_summary: dict = None, web_hashrate: dict = None
) -> List[HashBoard]:
if not web_summary:
try:
web_summary = await self.web.summary()
except APIError:
pass
if not web_hashrate:
try:
web_hashrate = await self.web.hashrate()
except APIError:
pass
hb_list = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
]
if web_summary["HBs"] != None:
for hb in web_summary["HBs"]:
for hr in web_hashrate:
if hr["Index"] == hb["Index"]:
num_of_chips = len(hr["Data"])
hashrate = hb["Hashrate"][0]
# Update the Hashboard object
hb_list[hr["Index"]].expected_chips = num_of_chips
hb_list[hr["Index"]].missing = False
hb_list[hr["Index"]].hashrate = round(hashrate / 1000000, 2)
hb_list[hr["Index"]].chips = num_of_chips
hb_list[hr["Index"]].temp = hb["Temperature"]
return hb_list
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
return None
async def get_pools(self, web_summary: dict = None) -> List[dict]:
groups = []
if not web_summary:
try:
web_summary = await self.api.summary()
except APIError:
pass
if web_summary:
try:
pools = {}
for i, pool in enumerate(web_summary["StratumConfigs"]):
pools[f"pool_{i + 1}_url"] = (
pool["pool"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["login"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_uptime(self, web_summary: dict = None) -> Optional[int]:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
uptime = web_summary["Session"]["Uptime"]
return uptime
except KeyError:
pass
return None
async def get_fault_light(self, web_summary: dict = None) -> bool:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
light = web_summary["Misc"]["Locate Miner State"]
return light
except KeyError:
pass
return False
async def get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
if not web_summary:
web_summary = await self.web.summary()
errors = []
if web_summary:
try:
error = web_summary["Status"]["Last Error"]
if error != None:
errors.append(X19Error(str(error)))
return errors
except KeyError:
pass
return errors

View File

@@ -26,4 +26,6 @@ class Hiveon(BMMiner):
self.api_type = "Hiveon"
async def get_model(self) -> Optional[str]:
return self.model + " (Hiveon)"
if self.model is not None:
return self.model + " (Hiveon)"
return "? (Hiveon)"

View File

@@ -0,0 +1,441 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
import logging
from collections import namedtuple
from typing import List, Optional, Tuple, Union
import toml
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API.luxminer import LUXMinerAPI
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.web.bosminer import BOSMinerWebAPI
LUXMINER_DATA_LOC = {
"mac": {
"cmd": "get_mac",
"kwargs": {"api_config": {"api": "config"}},
},
"model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {
"cmd": "get_api_ver",
"kwargs": {},
},
"fw_ver": {
"cmd": "get_fw_ver",
"kwargs": {},
},
"hostname": {
"cmd": "get_hostname",
"kwargs": {},
},
"hashrate": {
"cmd": "get_hashrate",
"kwargs": {},
},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"kwargs": {},
},
"hashboards": {
"cmd": "get_hashboards",
"kwargs": {},
},
"wattage": {
"cmd": "get_wattage",
"kwargs": {},
},
"wattage_limit": {
"cmd": "get_wattage_limit",
"kwargs": {},
},
"fans": {
"cmd": "get_fans",
"kwargs": {},
},
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
"env_temp": {"cmd": "get_env_temp", "kwargs": {}},
"errors": {
"cmd": "get_errors",
"kwargs": {},
},
"fault_light": {
"cmd": "get_fault_light",
"kwargs": {},
},
"pools": {
"cmd": "get_pools",
"kwargs": {},
},
"is_mining": {
"cmd": "is_mining",
"kwargs": {},
},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_stats": {"api": "stats"}},
},
}
class LUXMiner(BaseMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip)
# interfaces
self.api = LUXMinerAPI(ip, api_ver)
# self.web = BOSMinerWebAPI(ip)
# static data
self.api_type = "LUXMiner"
# data gathering locations
self.data_locations = LUXMINER_DATA_LOC
# autotuning/shutdown support
# self.supports_autotuning = True
# self.supports_shutdown = True
# data storage
self.api_ver = api_ver
async def _get_session(self) -> Optional[str]:
try:
data = await self.api.session()
if not data["SESSION"][0]["SessionID"] == "":
return data["SESSION"][0]["SessionID"]
except APIError:
pass
try:
data = await self.api.logon()
return data["SESSION"][0]["SessionID"]
except (LookupError, APIError):
return
async def fault_light_on(self) -> bool:
"""Sends command to turn on fault light on the miner."""
try:
session_id = await self._get_session()
if session_id:
await self.api.ledset(session_id, "red", "blink")
return True
except (APIError, LookupError):
pass
return False
async def fault_light_off(self) -> bool:
"""Sends command to turn off fault light on the miner."""
try:
session_id = await self._get_session()
if session_id:
await self.api.ledset(session_id, "red", "off")
return True
except (APIError, LookupError):
pass
return False
async def restart_backend(self) -> bool:
"""Restart luxminer hashing process. Wraps [`restart_luxminer`][pyasic.miners.backends.luxminer.LUXMiner.restart_luxminer] to standardize."""
return await self.restart_luxminer()
async def restart_luxminer(self) -> bool:
"""Restart luxminer hashing process."""
try:
session_id = await self._get_session()
if session_id:
await self.api.resetminer(session_id)
return True
except (APIError, LookupError):
pass
return False
async def stop_mining(self) -> bool:
try:
session_id = await self._get_session()
if session_id:
await self.api.curtail(session_id)
return True
except (APIError, LookupError):
pass
return False
async def resume_mining(self) -> bool:
try:
session_id = await self._get_session()
if session_id:
await self.api.wakeup(session_id)
return True
except (APIError, LookupError):
pass
async def reboot(self) -> bool:
"""Reboots power to the physical miner."""
try:
session_id = await self._get_session()
if session_id:
await self.api.rebootdevice(session_id)
return True
except (APIError, LookupError):
pass
return False
async def get_config(self) -> MinerConfig:
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
pass
async def set_power_limit(self, wattage: int) -> bool:
return False
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_mac(self, api_config: dict = None) -> Optional[str]:
mac = None
if not api_config:
try:
api_config = await self.api.config()
except APIError:
return None
if api_config:
try:
mac = api_config["CONFIG"][0]["MACAddr"]
except KeyError:
return None
return mac
async def get_model(self) -> Optional[str]:
if self.model is not None:
return self.model + " (LuxOS)"
return "? (LuxOS)"
async def get_version(self) -> Tuple[Optional[str], Optional[str]]:
pass
async def get_api_ver(self) -> Optional[str]:
pass
async def get_fw_ver(self) -> Optional[str]:
pass
async def get_hostname(self) -> Union[str, None]:
pass
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return round(float(api_summary["SUMMARY"][0]["GHS 5s"] / 1000), 2)
except (IndexError, KeyError, ValueError, TypeError):
pass
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
hashboards = []
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
board_offset = -1
boards = api_stats["STATS"]
if len(boards) > 1:
for board_num in range(1, 16, 5):
for _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
for i in range(board_offset, board_offset + self.ideal_hashboards):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
)
chip_temp = boards[1].get(f"temp{i}")
if chip_temp:
hashboard.chip_temp = round(chip_temp)
temp = boards[1].get(f"temp2_{i}")
if temp:
hashboard.temp = round(temp)
hashrate = boards[1].get(f"chain_rate{i}")
if hashrate:
hashboard.hashrate = round(float(hashrate) / 1000, 2)
chips = boards[1].get(f"chain_acn{i}")
if chips:
hashboard.chips = chips
hashboard.missing = False
if (not chips) or (not chips > 0):
hashboard.missing = True
hashboards.append(hashboard)
except (IndexError, KeyError, ValueError, TypeError):
pass
return hashboards
async def get_env_temp(self) -> Optional[float]:
return None
async def get_wattage(self, api_power: dict) -> Optional[int]:
if not api_power:
try:
api_power = await self.api.power()
except APIError:
pass
if api_power:
try:
return api_power["POWER"][0]["Watts"]
except (IndexError, KeyError, ValueError, TypeError):
pass
async def get_wattage_limit(self) -> Optional[int]:
return None
async def get_fans(self, api_fans: dict = None) -> List[Fan]:
if not api_fans:
try:
api_fans = await self.api.fans()
except APIError:
pass
fans = []
if api_fans:
for fan in range(self.fan_count):
try:
fans.append(Fan(api_fans["FANS"][0]["RPM"]))
except (IndexError, KeyError, ValueError, TypeError):
fans.append(Fan())
return fans
async def get_fan_psu(self) -> Optional[int]:
return None
async def get_pools(self, api_pools: dict = None) -> List[dict]:
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
seen = []
groups = [{"quota": "0"}]
if api_pools.get("POOLS"):
for i, pool in enumerate(api_pools["POOLS"]):
if len(seen) == 0:
seen.append(pool["User"])
if not pool["User"] in seen:
# need to use get_config, as this will never read perfectly as there are some bad edge cases
groups = []
cfg = await self.get_config()
if cfg:
for group in cfg.pool_groups:
pools = {"quota": group.quota}
for _i, _pool in enumerate(group.pools):
pools[f"pool_{_i + 1}_url"] = _pool.url.replace(
"stratum+tcp://", ""
).replace("stratum2+tcp://", "")
pools[f"pool_{_i + 1}_user"] = _pool.username
groups.append(pools)
return groups
else:
groups[0][f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
groups[0][f"pool_{i + 1}_user"] = pool["User"]
else:
groups = []
cfg = await self.get_config()
if cfg:
for group in cfg.pool_groups:
pools = {"quota": group.quota}
for _i, _pool in enumerate(group.pools):
pools[f"pool_{_i + 1}_url"] = _pool.url.replace(
"stratum+tcp://", ""
).replace("stratum2+tcp://", "")
pools[f"pool_{_i + 1}_user"] = _pool.username
groups.append(pools)
return groups
return groups
async def get_errors(self) -> List[MinerErrorData]:
pass
async def get_fault_light(self) -> bool:
pass
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
try:
rate_unit = api_stats["STATS"][1]["rate_unit"]
except KeyError:
rate_unit = "GH"
if rate_unit == "GH":
return round(ideal_rate / 1000, 2)
if rate_unit == "MH":
return round(ideal_rate / 1000000, 2)
else:
return round(ideal_rate, 2)
except (KeyError, IndexError):
pass
async def is_mining(self) -> Optional[bool]:
pass
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass

View File

@@ -17,6 +17,7 @@
from typing import Optional
from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.miners.backends.bmminer import BMMiner
from pyasic.web.vnish import VNishWebAPI
@@ -43,6 +44,8 @@ VNISH_DATA_LOC = {
"errors": {"cmd": "get_errors", "kwargs": {}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {}},
}
@@ -58,7 +61,9 @@ class VNish(BMMiner):
self.data_locations = VNISH_DATA_LOC
async def get_model(self) -> Optional[str]:
return self.model + " (VNISH)"
if self.model is not None:
return self.model + " (VNish)"
return "? (VNish)"
async def restart_backend(self) -> bool:
data = await self.web.restart_vnish()
@@ -69,6 +74,24 @@ class VNish(BMMiner):
pass
return False
async def stop_mining(self) -> bool:
data = await self.web.stop_mining()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def resume_mining(self) -> bool:
data = await self.web.resume_mining()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def reboot(self) -> bool:
data = await self.web.reboot()
if data:
@@ -140,7 +163,7 @@ class VNish(BMMiner):
float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2
)
except (IndexError, KeyError, ValueError, TypeError) as e:
print(e)
logger.error(e)
pass
async def get_wattage_limit(self, web_settings: dict = None) -> Optional[int]:
@@ -167,3 +190,9 @@ class VNish(BMMiner):
return fw_ver
except KeyError:
pass
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
return None
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
return None

View File

@@ -16,6 +16,12 @@
from pyasic.miners.backends.btminer import BTMiner
class M6X(BTMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.supports_autotuning = True
class M5X(BTMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
import ipaddress
import logging
from abc import ABC, abstractmethod
@@ -24,6 +24,7 @@ import asyncssh
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData
from pyasic.logger import logger
class BaseMiner(ABC):
@@ -32,6 +33,8 @@ class BaseMiner(ABC):
self.api = None
self.web = None
self.ssh_pwd = "root"
# static data
self.ip = ip
self.api_type = None
@@ -71,6 +74,53 @@ class BaseMiner(ABC):
def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
@property
def pwd(self): # noqa - Skip PyCharm inspection
data = []
try:
if self.web is not None:
data.append(f"web={self.web.pwd}")
except TypeError:
pass
try:
if self.api is not None:
data.append(f"api={self.api.pwd}")
except TypeError:
pass
return ",".join(data)
@pwd.setter
def pwd(self, val):
self.ssh_pwd = val
try:
if self.web is not None:
self.web.pwd = val
except TypeError:
pass
try:
if self.api is not None:
self.api.pwd = val
except TypeError:
pass
@property
def username(self): # noqa - Skip PyCharm inspection
data = []
try:
if self.web is not None:
data.append(f"web={self.web.username}")
except TypeError:
pass
return ",".join(data)
@username.setter
def username(self, val):
try:
if self.web is not None:
self.web.username = val
except TypeError:
pass
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
@@ -78,7 +128,7 @@ class BaseMiner(ABC):
str(self.ip),
known_hosts=None,
username="root",
password="root",
password=self.ssh_pwd,
server_host_key_algs=["ssh-rsa"],
)
return conn
@@ -345,83 +395,99 @@ class BaseMiner(ABC):
"""
pass
async def _get_data(self, allow_warning: bool, data_to_get: list = None) -> dict:
if not data_to_get:
@abstractmethod
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
"""Check whether the miner is mining.
Returns:
A boolean value representing if the miner is mining.
"""
pass
@abstractmethod
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
"""Get the uptime of the miner in seconds.
Returns:
The uptime of the miner in seconds.
"""
pass
async def _get_data(
self, allow_warning: bool, include: list = None, exclude: list = None
) -> dict:
if include is None:
# everything
data_to_get = [
"mac",
"model",
"api_ver",
"fw_ver",
"hostname",
"hashrate",
"nominal_hashrate",
"hashboards",
"env_temp",
"wattage",
"wattage_limit",
"fans",
"fan_psu",
"errors",
"fault_light",
"pools",
]
api_multicommand = []
include = list(self.data_locations.keys())
if exclude is not None:
for item in exclude:
if item in include:
include.remove(item)
api_multicommand = set()
web_multicommand = []
for data_name in data_to_get:
for data_name in include:
try:
fn_args = self.data_locations[data_name]["kwargs"]
for arg_name in fn_args:
if fn_args[arg_name].get("api"):
api_multicommand.append(fn_args[arg_name]["api"])
api_multicommand.add(fn_args[arg_name]["api"])
if fn_args[arg_name].get("web"):
web_multicommand.append(fn_args[arg_name]["web"])
if not fn_args[arg_name]["web"] in web_multicommand:
web_multicommand.append(fn_args[arg_name]["web"])
except KeyError as e:
print(e, data_name)
logger.error(e, data_name)
continue
api_multicommand = list(set(api_multicommand))
_web_multicommand = web_multicommand
for item in web_multicommand:
if item not in _web_multicommand:
_web_multicommand.append(item)
web_multicommand = _web_multicommand
if len(api_multicommand) > 0:
api_command_data = await self.api.multicommand(
*api_multicommand, allow_warning=allow_warning
api_command_task = asyncio.create_task(
self.api.multicommand(*api_multicommand, allow_warning=allow_warning)
)
else:
api_command_data = {}
api_command_task = asyncio.sleep(0)
if len(web_multicommand) > 0:
web_command_data = await self.web.multicommand(
*web_multicommand, allow_warning=allow_warning
web_command_task = asyncio.create_task(
self.web.multicommand(*web_multicommand, allow_warning=allow_warning)
)
else:
web_command_task = asyncio.sleep(0)
web_command_data = await web_command_task
if web_command_data is None:
web_command_data = {}
api_command_data = await api_command_task
if api_command_data is None:
api_command_data = {}
miner_data = {}
for data_name in data_to_get:
for data_name in include:
try:
fn_args = self.data_locations[data_name]["kwargs"]
args_to_send = {k: None for k in fn_args}
for arg_name in fn_args:
if fn_args[arg_name].get("api"):
if api_command_data.get("multicommand"):
args_to_send[arg_name] = api_command_data[
fn_args[arg_name]["api"]
][0]
else:
args_to_send[arg_name] = api_command_data
if fn_args[arg_name].get("web"):
if web_command_data.get("multicommand"):
args_to_send[arg_name] = web_command_data[
fn_args[arg_name]["web"]
]
else:
if not web_command_data == {"multicommand": False}:
args_to_send[arg_name] = web_command_data
except (KeyError, IndexError):
try:
if fn_args[arg_name].get("api"):
if api_command_data.get("multicommand"):
args_to_send[arg_name] = api_command_data[
fn_args[arg_name]["api"]
][0]
else:
args_to_send[arg_name] = api_command_data
if fn_args[arg_name].get("web"):
if web_command_data is not None:
if web_command_data.get("multicommand"):
args_to_send[arg_name] = web_command_data[
fn_args[arg_name]["web"]
]
else:
if not web_command_data == {"multicommand": False}:
args_to_send[arg_name] = web_command_data
except LookupError:
args_to_send[arg_name] = None
except LookupError:
continue
function = getattr(self, self.data_locations[data_name]["cmd"])
@@ -436,8 +502,8 @@ class BaseMiner(ABC):
except KeyError:
pass
if len(pools_data) > 1:
miner_data["pool_2_url"] = pools_data[1]["pool_2_url"]
miner_data["pool_2_user"] = pools_data[1]["pool_2_user"]
miner_data["pool_2_url"] = pools_data[1]["pool_1_url"]
miner_data["pool_2_user"] = pools_data[1]["pool_1_user"]
miner_data[
"pool_split"
] = f"{pools_data[0]['quota']}/{pools_data[1]['quota']}"
@@ -451,13 +517,14 @@ class BaseMiner(ABC):
return miner_data
async def get_data(
self, allow_warning: bool = False, data_to_get: list = None
self, allow_warning: bool = False, include: list = None, exclude: list = None
) -> MinerData:
"""Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData].
Parameters:
allow_warning: Allow warning when an API command fails.
data_to_get: Names of data items you want to gather. Defaults to all data.
include: Names of data items you want to gather. Defaults to all data.
exclude: Names of data items to exclude. Exclusion happens after considering included items.
Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing data from the miner.
@@ -473,7 +540,9 @@ class BaseMiner(ABC):
],
)
gathered_data = await self._get_data(allow_warning, data_to_get=data_to_get)
gathered_data = await self._get_data(
allow_warning=allow_warning, include=include, exclude=exclude
)
for item in gathered_data:
if gathered_data[item] is not None:
setattr(data, item, gathered_data[item])

View File

@@ -254,7 +254,7 @@ class CGMinerA10X(CGMiner, A10X):
else:
web_get_all = web_get_all["all"]
fan_data = [Fan(), Fan(), Fan(), Fan()]
fans = [Fan() for _ in range(self.fan_count)]
if web_get_all:
try:
spd = web_get_all["fansSpeed"]
@@ -263,9 +263,9 @@ class CGMinerA10X(CGMiner, A10X):
else:
round((int(spd) * 6000) / 100)
for i in range(self.fan_count):
fan_data[i] = Fan(spd)
fans[i].speed = spd
return fan_data
return fans
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []

View File

@@ -38,16 +38,13 @@ class CGMinerT3HPlus(CGMiner, T3HPlus):
return False
async def get_config(self, api_pools: dict = None) -> MinerConfig:
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError as e:
logging.warning(e)
# get pool data
try:
pools = await self.api.pools()
except APIError:
return self.config
if api_pools:
if "POOLS" in api_pools.keys():
cfg = MinerConfig().from_api(api_pools["POOLS"])
self.config = cfg
self.config = MinerConfig.from_api(pools)
return self.config
async def reboot(self) -> bool:
@@ -236,7 +233,7 @@ class CGMinerT3HPlus(CGMiner, T3HPlus):
else:
web_get_all = web_get_all["all"]
fan_data = [Fan(), Fan(), Fan(), Fan()]
fans = [Fan() for _ in range(self.fan_count)]
if web_get_all:
try:
spd = web_get_all["fansSpeed"]
@@ -245,9 +242,9 @@ class CGMinerT3HPlus(CGMiner, T3HPlus):
else:
round((int(spd) * 6000) / 100)
for i in range(self.fan_count):
fan_data[i] = Fan(spd)
fans[i].speed = spd
return fan_data
return fans
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []

View File

@@ -13,17 +13,17 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
import enum
import ipaddress
import json
import re
from typing import Callable, List, Optional, Tuple, Union
from typing import AsyncGenerator, Callable, List, Optional, Tuple, Union
import aiohttp
import anyio
import httpx
from pyasic import settings
from pyasic.logger import logger
from pyasic.miners.antminer import *
from pyasic.miners.avalonminer import *
@@ -35,7 +35,9 @@ from pyasic.miners.backends import (
CGMiner,
CGMinerAvalon,
Hiveon,
LUXMiner,
VNish,
ePIC,
)
from pyasic.miners.base import AnyMiner
from pyasic.miners.goldshell import *
@@ -43,9 +45,6 @@ from pyasic.miners.innosilicon import *
from pyasic.miners.unknown import UnknownMiner
from pyasic.miners.whatsminer import *
TIMEOUT = 20
RETRIES = 3
class MinerTypes(enum.Enum):
ANTMINER = 0
@@ -56,15 +55,17 @@ class MinerTypes(enum.Enum):
BRAIINS_OS = 5
VNISH = 6
HIVEON = 7
LUX_OS = 8
EPIC = 9
MINER_CLASSES = {
MinerTypes.ANTMINER: {
None: BMMiner,
"ANTMINER DR5": CGMinerDR5,
"ANTMINER D3": CGMinerD3,
"ANTMINER HS3": BMMinerHS3,
"ANTMINER L3+": BMMinerL3Plus,
"ANTMINER DR5": CGMinerDR5,
"ANTMINER L7": BMMinerL7,
"ANTMINER E9 PRO": BMMinerE9Pro,
"ANTMINER S9": BMMinerS9,
@@ -83,6 +84,8 @@ MINER_CLASSES = {
"ANTMINER S19L": BMMinerS19L,
"ANTMINER S19 PRO": BMMinerS19Pro,
"ANTMINER S19J": BMMinerS19j,
"ANTMINER S19I": BMMinerS19i,
"ANTMINER S19+": BMMinerS19Plus,
"ANTMINER S19J88NOPIC": BMMinerS19jNoPIC,
"ANTMINER S19PRO+": BMMinerS19ProPlus,
"ANTMINER S19J PRO": BMMinerS19jPro,
@@ -97,6 +100,8 @@ MINER_CLASSES = {
"M20SV10": BTMinerM20SV10,
"M20SV20": BTMinerM20SV20,
"M20SV30": BTMinerM20SV30,
"M20PV10": BTMinerM20PV10,
"M20PV30": BTMinerM20PV30,
"M20S+V30": BTMinerM20SPlusV30,
"M21V10": BTMinerM21V10,
"M21SV20": BTMinerM21SV20,
@@ -106,6 +111,8 @@ MINER_CLASSES = {
"M29V10": BTMinerM29V10,
"M30V10": BTMinerM30V10,
"M30V20": BTMinerM30V20,
"M30KV10": BTMinerM30KV10,
"M30LV10": BTMinerM30LV10,
"M30SV10": BTMinerM30SV10,
"M30SV20": BTMinerM30SV20,
"M30SV30": BTMinerM30SV30,
@@ -155,6 +162,7 @@ MINER_CLASSES = {
"M30S+VE100": BTMinerM30SPlusVE100,
"M30S+VF20": BTMinerM30SPlusVF20,
"M30S+VF30": BTMinerM30SPlusVF30,
"M30S+VG20": BTMinerM30SPlusVG20,
"M30S+VG30": BTMinerM30SPlusVG30,
"M30S+VG40": BTMinerM30SPlusVG40,
"M30S+VG50": BTMinerM30SPlusVG50,
@@ -188,6 +196,9 @@ MINER_CLASSES = {
"M30S++VJ30": BTMinerM30SPlusPlusVJ30,
"M31V10": BTMinerM31V10,
"M31V20": BTMinerM31V20,
"M31HV10": BTMinerM31HV10,
"M31HV40": BTMinerM31HV40,
"M31LV10": BTMinerM31LV10,
"M31SV10": BTMinerM31SV10,
"M31SV20": BTMinerM31SV20,
"M31SV30": BTMinerM31SV30,
@@ -203,7 +214,6 @@ MINER_CLASSES = {
"M31SEV10": BTMinerM31SEV10,
"M31SEV20": BTMinerM31SEV20,
"M31SEV30": BTMinerM31SEV30,
"M31HV40": BTMinerM31HV40,
"M31S+V10": BTMinerM31SPlusV10,
"M31S+V20": BTMinerM31SPlusV20,
"M31S+V30": BTMinerM31SPlusV30,
@@ -230,6 +240,7 @@ MINER_CLASSES = {
"M33V20": BTMinerM33V20,
"M33V30": BTMinerM33V30,
"M33SVG30": BTMinerM33SVG30,
"M33S+VG20": BTMinerM33SPlusVG20,
"M33S+VH20": BTMinerM33SPlusVH20,
"M33S+VH30": BTMinerM33SPlusVH30,
"M33S++VH20": BTMinerM33SPlusPlusVH20,
@@ -239,7 +250,10 @@ MINER_CLASSES = {
"M36SVE10": BTMinerM36SVE10,
"M36S+VG30": BTMinerM36SPlusVG30,
"M36S++VH30": BTMinerM36SPlusPlusVH30,
"M39V10": BTMinerM39V10,
"M39V20": BTMinerM39V20,
"M39V30": BTMinerM39V30,
"M50VE30": BTMinerM50VE30,
"M50VG30": BTMinerM50VG30,
"M50VH10": BTMinerM50VH10,
"M50VH20": BTMinerM50VH20,
@@ -263,6 +277,7 @@ MINER_CLASSES = {
"M50S+VH30": BTMinerM50SPlusVH30,
"M50S+VH40": BTMinerM50SPlusVH40,
"M50S+VJ30": BTMinerM50SPlusVJ30,
"M50S+VK20": BTMinerM50SPlusVK20,
"M50S++VK10": BTMinerM50SPlusPlusVK10,
"M50S++VK20": BTMinerM50SPlusPlusVK20,
"M50S++VK30": BTMinerM50SPlusPlusVK30,
@@ -273,6 +288,25 @@ MINER_CLASSES = {
"M56SVH30": BTMinerM56SVH30,
"M56S+VJ30": BTMinerM56SPlusVJ30,
"M59VH30": BTMinerM59VH30,
"M60VK10": BTMinerM60VK10,
"M60VK20": BTMinerM60VK20,
"M60VK30": BTMinerM60VK30,
"M60VK40": BTMinerM60VK40,
"M60SVK10": BTMinerM60SVK10,
"M60SVK20": BTMinerM60SVK20,
"M60SVK30": BTMinerM60SVK30,
"M60SVK40": BTMinerM60SVK40,
"M63VK10": BTMinerM63VK10,
"M63VK20": BTMinerM63VK20,
"M63VK30": BTMinerM63VK30,
"M63SVK10": BTMinerM63SVK10,
"M63SVK20": BTMinerM63SVK20,
"M63SVK30": BTMinerM63SVK30,
"M66VK20": BTMinerM66VK20,
"M66VK30": BTMinerM66VK30,
"M66SVK20": BTMinerM66SVK20,
"M66SVK30": BTMinerM66SVK30,
"M66SVK40": BTMinerM66SVK40,
},
MinerTypes.AVALONMINER: {
None: CGMinerAvalon,
@@ -316,12 +350,17 @@ MINER_CLASSES = {
"ANTMINER S19J": BOSMinerS19j,
"ANTMINER S19J88NOPIC": BOSMinerS19jNoPIC,
"ANTMINER S19J PRO": BOSMinerS19jPro,
"ANTMINER S19J PRO NOPIC": BOSMinerS19jPro,
"ANTMINER T19": BOSMinerT19,
"ANTMINER S19K PRO NOPIC": BOSMinerS19kProNoPIC,
},
MinerTypes.VNISH: {
None: VNish,
"ANTMINER L3+": VnishL3Plus,
"ANTMINER S17+": VNishS17Plus,
"ANTMINER S17 PRO": VNishS17Pro,
"ANTMINER S19": VNishS19,
"ANTMINER S19NOPIC": VNishS19NoPIC,
"ANTMINER S19 PRO": VNishS19Pro,
"ANTMINER S19J": VNishS19j,
"ANTMINER S19J PRO": VNishS19jPro,
@@ -329,32 +368,40 @@ MINER_CLASSES = {
"ANTMINER S19A PRO": VNishS19aPro,
"ANTMINER T19": VNishT19,
},
MinerTypes.EPIC: {
None: ePIC,
"ANTMINER S19": ePICS19,
"ANTMINER S19 PRO": ePICS19Pro,
"ANTMINER S19J": ePICS19j,
"ANTMINER S19J PRO": ePICS19jPro,
"ANTMINER S19J PRO+": ePICS19jProPlus,
"ANTMINER S19K PRO": ePICS19kPro,
"ANTMINER S19 XP": ePICS19XP,
},
MinerTypes.HIVEON: {
None: Hiveon,
"ANTMINER T9": HiveonT9,
},
MinerTypes.LUX_OS: {
None: LUXMiner,
"ANTMINER S9": LUXMinerS9,
},
}
async def concurrent_get_first_result(tasks: list, verification_func: Callable):
while True:
await asyncio.sleep(0)
if len(tasks) == 0:
return
for task in tasks:
if task.done():
try:
result = await task
except asyncio.CancelledError:
for t in tasks:
t.cancel()
raise
else:
if not verification_func(result):
continue
for t in tasks:
t.cancel()
return result
res = None
for fut in asyncio.as_completed(tasks):
res = await fut
if verification_func(res):
break
for t in tasks:
t.cancel()
try:
await t
except asyncio.CancelledError:
pass
return res
class MinerFactory:
@@ -364,7 +411,9 @@ class MinerFactory:
def clear_cached_miners(self):
self.cache = {}
async def get_multiple_miners(self, ips: List[str], limit: int = 200):
async def get_multiple_miners(
self, ips: List[str], limit: int = 200
) -> List[AnyMiner]:
results = []
async for miner in self.get_miner_generator(ips, limit):
@@ -372,7 +421,7 @@ class MinerFactory:
return results
async def get_miner_generator(self, ips: list, limit: int = 200):
async def get_miner_generator(self, ips: list, limit: int = 200) -> AsyncGenerator:
tasks = []
semaphore = asyncio.Semaphore(limit)
@@ -380,58 +429,66 @@ class MinerFactory:
tasks.append(asyncio.create_task(self.get_miner(ip)))
for task in tasks:
await semaphore.acquire()
try:
async with semaphore:
result = await task
if result is not None:
yield result
finally:
semaphore.release()
async def get_miner(self, ip: str):
ip = str(ip)
if ip in self.cache:
return self.cache[ip]
miner_type = None
for _ in range(RETRIES):
for _ in range(settings.get("factory_get_retries", 1)):
task = asyncio.create_task(self._get_miner_type(ip))
try:
miner_type = await asyncio.wait_for(task, timeout=TIMEOUT)
miner_type = await asyncio.wait_for(
task, timeout=settings.get("factory_get_timeout", 3)
)
except asyncio.TimeoutError:
task.cancel()
continue
else:
if miner_type is not None:
break
if miner_type is not None:
miner_model = None
fn = None
if miner_type == MinerTypes.ANTMINER:
fn = self.get_miner_model_antminer
if miner_type == MinerTypes.WHATSMINER:
fn = self.get_miner_model_whatsminer
if miner_type == MinerTypes.AVALONMINER:
fn = self.get_miner_model_avalonminer
if miner_type == MinerTypes.INNOSILICON:
fn = self.get_miner_model_innosilicon
if miner_type == MinerTypes.GOLDSHELL:
fn = self.get_miner_model_goldshell
if miner_type == MinerTypes.BRAIINS_OS:
fn = self.get_miner_model_braiins_os
if miner_type == MinerTypes.VNISH:
fn = self.get_miner_model_vnish
if miner_type == MinerTypes.HIVEON:
fn = self.get_miner_model_hiveon
miner_model_fns = {
MinerTypes.ANTMINER: self.get_miner_model_antminer,
MinerTypes.WHATSMINER: self.get_miner_model_whatsminer,
MinerTypes.AVALONMINER: self.get_miner_model_avalonminer,
MinerTypes.INNOSILICON: self.get_miner_model_innosilicon,
MinerTypes.GOLDSHELL: self.get_miner_model_goldshell,
MinerTypes.BRAIINS_OS: self.get_miner_model_braiins_os,
MinerTypes.VNISH: self.get_miner_model_vnish,
MinerTypes.EPIC: self.get_miner_model_epic,
MinerTypes.HIVEON: self.get_miner_model_hiveon,
MinerTypes.LUX_OS: self.get_miner_model_luxos,
}
fn = miner_model_fns.get(miner_type)
if fn is not None:
task = asyncio.create_task(fn(ip))
try:
miner_model = await asyncio.wait_for(task, timeout=30)
miner_model = await asyncio.wait_for(
task, timeout=settings.get("factory_get_timeout", 3)
)
except asyncio.TimeoutError:
task.cancel()
pass
boser_enabled = None
if miner_type == MinerTypes.BRAIINS_OS:
boser_enabled = await self.get_boser_braiins_os(ip)
miner = self._select_miner_from_classes(
ip, miner_type=miner_type, miner_model=miner_model
ip,
miner_type=miner_type,
miner_model=miner_model,
boser_enabled=boser_enabled,
)
if miner is not None and not isinstance(miner, UnknownMiner):
self.cache[ip] = miner
return miner
@@ -445,65 +502,104 @@ class MinerFactory:
return await concurrent_get_first_result(tasks, lambda x: x is not None)
async def _get_miner_web(self, ip: str):
urls = [f"http://{ip}/", f"https://{ip}/"]
async with aiohttp.ClientSession() as session:
tasks = [asyncio.create_task(self._web_ping(session, url)) for url in urls]
tasks = []
try:
urls = [f"http://{ip}/", f"https://{ip}/"]
async with httpx.AsyncClient(
transport=settings.transport(verify=False)
) as session:
tasks = [
asyncio.create_task(self._web_ping(session, url)) for url in urls
]
text, resp = await concurrent_get_first_result(
tasks, lambda x: x[0] is not None
)
if text is not None:
return self._parse_web_type(text, resp)
text, resp = await concurrent_get_first_result(
tasks,
lambda x: x[0] is not None
and self._parse_web_type(x[0], x[1]) is not None,
)
if text is not None:
return self._parse_web_type(text, resp)
except asyncio.CancelledError:
for t in tasks:
t.cancel()
try:
await t
except asyncio.CancelledError:
pass
@staticmethod
async def _web_ping(
session: aiohttp.ClientSession, url: str
) -> Tuple[Optional[str], Optional[aiohttp.ClientResponse]]:
session: httpx.AsyncClient, url: str
) -> Tuple[Optional[str], Optional[httpx.Response]]:
try:
resp = await session.get(url)
return await resp.text(), resp
except aiohttp.ClientError:
resp = await session.get(url, follow_redirects=True)
return resp.text, resp
except (
httpx.HTTPError,
asyncio.TimeoutError,
anyio.EndOfStream,
anyio.ClosedResourceError,
):
pass
return None, None
@staticmethod
def _parse_web_type(web_text: str, web_resp: aiohttp.ClientResponse) -> MinerTypes:
if web_resp.status == 401 and 'realm="antMiner' in web_resp.headers.get(
def _parse_web_type(web_text: str, web_resp: httpx.Response) -> MinerTypes:
if web_resp.status_code == 401 and 'realm="antMiner' in web_resp.headers.get(
"www-authenticate", ""
):
return MinerTypes.ANTMINER
if web_resp.status == 307 and "https://" in web_resp.headers.get(
"location", ""
):
return MinerTypes.WHATSMINER
if "Braiins OS" in web_text or 'href="/cgi-bin/luci"' in web_text:
if len(web_resp.history) > 0:
history_resp = web_resp.history[0]
if (
"/cgi-bin/luci" in web_text
and history_resp.status_code == 307
and "https://" in history_resp.headers.get("location", "")
):
return MinerTypes.WHATSMINER
if "Braiins OS" in web_text:
return MinerTypes.BRAIINS_OS
if "cloud-box" in web_text:
return MinerTypes.GOLDSHELL
if "AnthillOS" in web_text:
return MinerTypes.VNISH
if "Miner Web Dashboard" in web_text:
return MinerTypes.EPIC
if "Avalon" in web_text:
return MinerTypes.AVALONMINER
if "DragonMint" in web_text:
return MinerTypes.INNOSILICON
async def _get_miner_socket(self, ip: str):
commands = ["devdetails", "version"]
tasks = [asyncio.create_task(self._socket_ping(ip, cmd)) for cmd in commands]
tasks = []
try:
commands = ["version", "devdetails"]
tasks = [
asyncio.create_task(self._socket_ping(ip, cmd)) for cmd in commands
]
data = await concurrent_get_first_result(
tasks, lambda x: x is not None and self._parse_socket_type(x) is not None
)
if data is not None:
return self._parse_socket_type(data)
data = await concurrent_get_first_result(
tasks,
lambda x: x is not None and self._parse_socket_type(x) is not None,
)
if data is not None:
d = self._parse_socket_type(data)
return d
except asyncio.CancelledError:
for t in tasks:
t.cancel()
try:
await t
except asyncio.CancelledError:
pass
@staticmethod
async def _socket_ping(ip: str, cmd: str) -> Optional[str]:
data = b""
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(str(ip), 4028), timeout=30
asyncio.open_connection(str(ip), 4028),
timeout=settings.get("factory_get_timeout", 3),
)
except (ConnectionError, OSError, asyncio.TimeoutError):
return
@@ -554,7 +650,9 @@ class MinerFactory:
return MinerTypes.VNISH
if "HIVEON" in upper_data:
return MinerTypes.HIVEON
if "ANTMINER" in upper_data:
if "LUXMINER" in upper_data:
return MinerTypes.LUX_OS
if "ANTMINER" in upper_data and not "DEVDETAILS" in upper_data:
return MinerTypes.ANTMINER
if "INTCHAINS_QOMO" in upper_data:
return MinerTypes.GOLDSHELL
@@ -565,26 +663,26 @@ class MinerFactory:
self,
ip: Union[ipaddress.ip_address, str],
location: str,
auth: Optional[aiohttp.BasicAuth] = None,
auth: Optional[httpx.DigestAuth] = None,
) -> Optional[dict]:
async with aiohttp.ClientSession() as session:
async with httpx.AsyncClient(transport=settings.transport()) as session:
try:
data = await session.get(
f"http://{str(ip)}{location}",
auth=auth,
timeout=30,
timeout=settings.get("factory_get_timeout", 3),
)
except (aiohttp.ClientError, asyncio.TimeoutError):
except (httpx.HTTPError, asyncio.TimeoutError):
logger.info(f"{ip}: Web command timeout.")
return
if data is None:
return
try:
json_data = await data.json()
except (aiohttp.ContentTypeError, asyncio.TimeoutError):
json_data = data.json()
except (json.JSONDecodeError, asyncio.TimeoutError):
try:
return json.loads(await data.text())
except (json.JSONDecodeError, aiohttp.ClientError):
return json.loads(data.text)
except (json.JSONDecodeError, httpx.HTTPError):
return
else:
return json_data
@@ -619,10 +717,15 @@ class MinerFactory:
return
except (ConnectionError, OSError):
return
if data == b"Socket connect failed: Connection refused\n":
return
data = await self._fix_api_data(data)
data = json.loads(data)
try:
data = json.loads(data)
except json.JSONDecodeError:
return {}
return data
@@ -666,25 +769,29 @@ class MinerFactory:
ip: ipaddress.ip_address,
miner_model: Union[str, None],
miner_type: Union[MinerTypes, None],
boser_enabled: bool = None,
) -> AnyMiner:
kwargs = {}
if boser_enabled is not None:
kwargs["boser"] = boser_enabled
try:
return MINER_CLASSES[miner_type][str(miner_model).upper()](ip)
return MINER_CLASSES[miner_type][str(miner_model).upper()](ip, **kwargs)
except LookupError:
if miner_type in MINER_CLASSES:
return MINER_CLASSES[miner_type][None](ip)
return UnknownMiner(str(ip))
async def get_miner_model_antminer(self, ip: str):
sock_json_data = await self.send_api_command(ip, "version")
try:
miner_model = sock_json_data["VERSION"][0]["Type"]
tasks = [
asyncio.create_task(self._get_model_antminer_web(ip)),
asyncio.create_task(self._get_model_antminer_sock(ip)),
]
return miner_model
except (TypeError, LookupError):
pass
return await concurrent_get_first_result(tasks, lambda x: x is not None)
async def _get_model_antminer_web(self, ip: str):
# last resort, this is slow
auth = aiohttp.BasicAuth("root", "root")
auth = httpx.DigestAuth("root", "root")
web_json_data = await self.send_web_command(
ip, "/cgi-bin/get_system_info.cgi", auth=auth
)
@@ -696,6 +803,31 @@ class MinerFactory:
except (TypeError, LookupError):
pass
async def _get_model_antminer_sock(self, ip: str):
sock_json_data = await self.send_api_command(ip, "version")
try:
miner_model = sock_json_data["VERSION"][0]["Type"]
if " (" in miner_model:
split_miner_model = miner_model.split(" (")
miner_model = split_miner_model[0]
return miner_model
except (TypeError, LookupError):
pass
sock_json_data = await self.send_api_command(ip, "stats")
try:
miner_model = sock_json_data["STATS"][0]["Type"]
if " (" in miner_model:
split_miner_model = miner_model.split(" (")
miner_model = split_miner_model[0]
return miner_model
except (TypeError, LookupError):
pass
async def get_miner_model_goldshell(self, ip: str):
json_data = await self.send_web_command(ip, "/mcb/status")
@@ -709,7 +841,8 @@ class MinerFactory:
async def get_miner_model_whatsminer(self, ip: str):
sock_json_data = await self.send_api_command(ip, "devdetails")
try:
miner_model = sock_json_data["DEVDETAILS"][0]["Model"]
miner_model = sock_json_data["DEVDETAILS"][0]["Model"].replace("_", "")
miner_model = miner_model[:-1] + "0"
return miner_model
except (TypeError, LookupError):
@@ -728,14 +861,14 @@ class MinerFactory:
async def get_miner_model_innosilicon(self, ip: str) -> Optional[str]:
try:
async with aiohttp.ClientSession() as session:
async with httpx.AsyncClient(transport=settings.transport()) as session:
auth_req = await session.post(
f"http://{ip}/api/auth",
data={"username": "admin", "password": "admin"},
)
auth = (await auth_req.json())["jwt"]
auth = auth_req.json()["jwt"]
web_data = await (
web_data = (
await session.post(
f"http://{ip}/api/type",
headers={"Authorization": "Bearer " + auth},
@@ -743,38 +876,64 @@ class MinerFactory:
)
).json()
return web_data["type"]
except (aiohttp.ClientError, LookupError):
except (httpx.HTTPError, LookupError):
pass
async def get_miner_model_braiins_os(self, ip: str) -> Optional[str]:
sock_json_data = await self.send_api_command(ip, "devdetails")
try:
miner_model = sock_json_data["DEVDETAILS"][0]["Model"]
miner_model = sock_json_data["DEVDETAILS"][0]["Model"].replace(
"Bitmain ", ""
)
return miner_model
except (TypeError, LookupError):
pass
try:
async with aiohttp.ClientSession() as session:
async with httpx.AsyncClient(transport=settings.transport()) as session:
d = await session.post(
url, json={"query": "{bosminer {info{modelName}}}"}
f"http://{ip}/graphql",
json={"query": "{bosminer {info{modelName}}}"},
)
if d.status == 200:
json_data = await d.json()
if d.status_code == 200:
json_data = d.json()
miner_model = json_data["data"]["bosminer"]["info"]["modelName"]
return miner_model
except (aiohttp.ClientError, LookupError):
except (httpx.HTTPError, LookupError):
pass
async def get_boser_braiins_os(self, ip: str):
# TODO: refine this check
try:
sock_json_data = await self.send_api_command(ip, "version")
return sock_json_data["STATUS"][0]["Msg"].split(" ")[0].upper() == "BOSER"
except LookupError:
# let the bosminer class decide
return None
async def get_miner_model_vnish(self, ip: str) -> Optional[str]:
sock_json_data = await self.send_api_command(ip, "stats")
try:
miner_model = sock_json_data["STATS"][0]["Type"].upper()
if " (VNISH" in miner_model:
split_miner_model = miner_model.split(" (VNISH ")
miner_model = sock_json_data["STATS"][0]["Type"]
if " (" in miner_model:
split_miner_model = miner_model.split(" (")
miner_model = split_miner_model[0]
if "(88)" in miner_model:
miner_model = miner_model.replace("(88)", "NOPIC")
if " AML" in miner_model:
miner_model = miner_model.replace(" AML", "")
return miner_model
except (TypeError, LookupError):
pass
async def get_miner_model_epic(self, ip: str) -> Optional[str]:
sock_json_data = await self.send_web_command(ip, ":4028/capabilities")
try:
miner_model = sock_json_data["Model"]
return miner_model
except (TypeError, LookupError):
pass
@@ -788,5 +947,17 @@ class MinerFactory:
except (TypeError, LookupError):
pass
async def get_miner_model_luxos(self, ip: str):
sock_json_data = await self.send_api_command(ip, "version")
try:
miner_model = sock_json_data["VERSION"][0]["Type"]
if " (" in miner_model:
split_miner_model = miner_model.split(" (")
miner_model = split_miner_model[0]
return miner_model
except (TypeError, LookupError):
pass
miner_factory = MinerFactory()

View File

@@ -26,6 +26,15 @@ class S19(AntMiner): # noqa - ignore ABC method implementation
self.fan_count = 4
class S19NoPIC(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19 No PIC"
self.nominal_chips = 88
self.fan_count = 4
class S19Pro(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
@@ -35,6 +44,24 @@ class S19Pro(AntMiner): # noqa - ignore ABC method implementation
self.fan_count = 4
class S19i(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19i"
self.nominal_chips = 80
self.fan_count = 4
class S19Plus(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19+"
self.nominal_chips = 80
self.fan_count = 4
class S19ProPlus(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
@@ -98,6 +125,24 @@ class S19jPro(AntMiner): # noqa - ignore ABC method implementation
self.fan_count = 4
class S19jProPlus(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19j Pro+"
self.nominal_chips = 120
self.fan_count = 4
class S19kPro(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19k Pro"
self.nominal_chips = 77
self.fan_count = 4
class S19L(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
@@ -105,3 +150,12 @@ class S19L(AntMiner): # noqa - ignore ABC method implementation
self.model = "S19L"
self.nominal_chips = 76
self.fan_count = 4
class S19kProNoPIC(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19k Pro No PIC"
self.nominal_chips = 77
self.fan_count = 4

View File

@@ -20,9 +20,15 @@ from .S19 import (
S19XP,
S19a,
S19aPro,
S19i,
S19j,
S19jNoPIC,
S19jPro,
S19jProPlus,
S19kPro,
S19kProNoPIC,
S19NoPIC,
S19Plus,
S19Pro,
S19ProPlus,
)

View File

@@ -22,8 +22,6 @@ class Avalon1166Pro(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 1166"
warnings.warn(
f"Unknown chip count for miner type {self.model}, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.model = "Avalon 1166 Pro"
self.nominal_chips = 120
self.fan_count = 4

View File

@@ -23,7 +23,5 @@ class Avalon1246(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 1246"
warnings.warn(
f"Unknown chip count for miner type {self.model}, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.nominal_chips = 120
self.fan_count = 4

View File

@@ -20,3 +20,4 @@ class A10X(InnosiliconMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver)
self.ip = ip
self.model = "A10X"

View File

@@ -0,0 +1,35 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners.makes import WhatsMiner
class M20PV10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M20P V10"
self.nominal_chips = 156
self.fan_count = 2
class M20PV30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M20P V30"
self.nominal_chips = 148
self.fan_count = 2

View File

@@ -42,8 +42,5 @@ class M20SV30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M20S V30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M20SV30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.nominal_chips = 140
self.fan_count = 2

View File

@@ -24,8 +24,5 @@ class M21V10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M21 V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M21V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.nominal_chips = 33
self.fan_count = 2

View File

@@ -42,8 +42,5 @@ class M21SV70(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M21S V70"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M21SV70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.nominal_chips = 111
self.fan_count = 2

View File

@@ -24,8 +24,5 @@ class M29V10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M29 V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M29V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.nominal_chips = 50
self.fan_count = 2

Some files were not shown because too many files have changed in this diff Show More