Compare commits

...

158 Commits

Author SHA1 Message Date
Upstream Data
b89ea1fa92 version: bump version number. 2023-12-22 16:29:03 -07:00
Upstream Data
3588197741 dependencies: bump httpx version 2023-12-22 16:28:46 -07:00
Upstream Data
8adc3d2adf version: bump version number. 2023-12-22 15:47:25 -07:00
Upstream Data
040c0b6842 feature: add support for S19 Pro Hydro. 2023-12-22 15:40:23 -07:00
Upstream Data
550b4a97a1 bug: fix a bunch of missed instances of nominal_ naming. 2023-12-22 15:32:01 -07:00
UpstreamData
d84d95fe5f version: bump version number. 2023-12-21 15:25:57 -07:00
UpstreamData
0e5b811fb9 Add config attribute to data and refactor data naming (#81)
* feature: add config to MinerData.  Remove related attributes.

* feature: rename ideal and nominal to expected to make data naming consistent across files.

* refactor: run isort on all files.

* docs: update docstrings.
2023-12-21 15:20:50 -07:00
UpstreamData
3d31179562 feature: add more BOS+ supported types. 2023-12-19 08:40:31 -07:00
UpstreamData
69f39bef0c docs: update tagline 2023-12-19 08:18:06 -07:00
UpstreamData
1076dab7f5 Update README.md 2023-12-19 08:17:39 -07:00
UpstreamData
3ae1f700c2 docs: update README.md 2023-12-18 14:48:19 -07:00
UpstreamData
dc3f061b9b docs: update shields. 2023-12-18 14:44:43 -07:00
UpstreamData
52758dd8b3 docs: update README. 2023-12-18 14:33:42 -07:00
UpstreamData
0e492f1cfd tests: add more tests for miners. 2023-12-18 14:11:16 -07:00
UpstreamData
659dc55f3c bug: add missing key to epic data locations. 2023-12-18 14:07:46 -07:00
UpstreamData
eb9b29aca1 tests: add tests for config and update tests. 2023-12-18 14:00:40 -07:00
UpstreamData
b045abe76e bug: reorder config information and fix bad key. 2023-12-18 13:59:56 -07:00
Upstream Data
7a75818a20 version: bump version number. 2023-12-17 09:09:00 -07:00
Upstream Data
d2be68d35e bug: fix MinerConfig default values for 3.11+. Add MinerConfig.as_epic default implementation. 2023-12-17 09:08:14 -07:00
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
181 changed files with 8515 additions and 2586 deletions

291
README.md
View File

@@ -1,142 +1,249 @@
# 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/)
[![python](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![Read the Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![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)
## Documentation and Supported Miners
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/).
[![PyPI - Version](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pyasic)](https://pypi.org/project/pyasic/)
Supported miners are listed in the docs, [here](https://pyasic.readthedocs.io/en/latest/miners/supported_types/).
[![Python - Supported Versions](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![CodeFactor - Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
[![Commit Activity - master](https://img.shields.io/github/commit-activity/y/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/commits/master/)
## Installation
You can install pyasic directly from pip with the command `pip install pyasic`.
[![Code Style - Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Read The Docs - Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![License - Apache 2.0](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
For those of you who aren't comfortable with code and developer tools, there are windows builds of GUI applications that use this library [here](https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing).
---
## Intro
## Developer Setup
It is highly reccommended that you contribute to this project through [`pyasic-super`](https://github.com/UpstreamData/pyasic-super) using its submodules. This allows testing in conjunction with other `pyasic` related programs.
Welcome to `pyasic`! `pyasic` uses an asynchronous method of communicating with ASIC miners on your network, which makes it super fast.
<br>
[Click here to view supported miner types](miners/supported_types.md)
This repo uses poetry for dependencies, which can be installed by following the guide on their website [here](https://python-poetry.org/docs/#installation).
---
## Getting started
After you have poetry installed, run `poetry install --with dev`, or `poetry install --with dev,docs` if you want to include packages required for documentation.
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.
Finally, initialize pre-commit hooks with `poetry run pre-commit install`.
### Documentation Testing
Testing the documentation can be done by running `poetry run mkdocs serve`, whcih will serve the documentation locally on port 8000.
## Interfacing with miners programmatically
There are 2 main ways to get a miner (and the functions attached to it), via scanning or via the `MinerFactory()`.
#### Scanning for miners
##### Scanning for miners
To scan for miners in `pyasic`, we use the class `MinerNetwork`, which abstracts the search, communication, identification, setup, and return of a miner to 1 command.
The command `MinerNetwork.scan()` returns a list that contains any miners found.
```python
import asyncio
from pyasic.network import MinerNetwork
import asyncio # asyncio for handling the async part
from pyasic.network import MinerNetwork # miner network handles the scanning
# define asynchronous function to scan for miners
async def scan_and_get_data():
# Define network range to be used for scanning
# This can take a list of IPs, a constructor string, or an IP and subnet mask
# The standard mask is /24 (x.x.x.0-255), and you can pass any IP address in the subnet
net = MinerNetwork("192.168.1.69", mask=24)
# Scan the network for miners
# This function returns a list of miners of the correct type as a class
miners: list = await net.scan_network_for_miners()
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.from_subnet("192.168.1.50/24") # this uses the 192.168.1.0-255 network
# We can now get data from any of these miners
# To do them all we have to create a list of tasks and gather them
tasks = [miner.get_data() for miner in miners]
# Gather all tasks asynchronously and run them
data = await asyncio.gather(*tasks)
# Data is now a list of MinerData, and we can reference any part of that
# Print out all data for now
for item in data:
print(item)
# scan for miners asynchronously
# this will return the correct type of miners if they are supported with all functionality.
miners = await network.scan()
print(miners)
if __name__ == "__main__":
asyncio.run(scan_and_get_data())
asyncio.run(scan_miners()) # run the scan asynchronously with asyncio.run()
```
---
##### Creating miners based on IP
If you already know the IP address of your miner or miners, you can use the `MinerFactory` to communicate and identify the miners, or an abstraction of its functionality, `get_miner()`.
The function `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
#### Getting a miner if you know the IP
async def get_miners(): # define async scan function to allow awaiting
# get the miner with the miner factory
# the miner factory is a singleton, and will always use the same object and cache
# this means you can always call it as MinerFactory().get_miner(), or just get_miner()
miner_1 = await get_miner("192.168.1.75")
miner_2 = await get_miner("192.168.1.76")
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)
if __name__ == "__main__":
asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run()
```
---
## 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` with all data it can gather from the miner.
Each piece of data in a `MinerData` instance can be referenced by getting it as an attribute, such as `MinerData().hashrate`.
##### One miner
```python
import asyncio
from pyasic import get_miner
async def gather_miner_data():
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
from pyasic.network import MinerNetwork # miner network handles the scanning
async def gather_miner_data(): # define async scan function to allow awaiting
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])
for miner_data in all_miner_data:
print(miner_data) # print out all the data one by one
if __name__ == "__main__":
asyncio.run(gather_miner_data())
```
---
## 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`.
These functions are
`check_light`,
`fault_light_off`,
`fault_light_on`,
`get_config`,
`get_data`,
`get_errors`,
`get_hostname`,
`get_model`,
`reboot`,
`restart_backend`,
`stop_mining`,
`resume_mining`,
`is_mining`,
`send_config`, and
`set_power_limit`.
##### Usage
```python
import asyncio
from pyasic import get_miner
# define asynchronous function to get miner and data
async def get_miner_data(miner_ip: str):
# Use MinerFactory to get miner
# MinerFactory is a singleton, so we can just get the instance in place
miner = await get_miner(miner_ip)
async def set_fault_light():
miner = await get_miner("192.168.1.20")
# Get data from the miner
data = await miner.get_data()
print(data)
# call control function
await miner.fault_light_on()
if __name__ == "__main__":
asyncio.run(get_miner_data("192.168.1.69"))
asyncio.run(set_fault_light())
```
### Advanced data gathering
---
## Helper dataclasses
If needed, this library exposes a wrapper for the miner API that can be used for advanced data gathering.
##### `MinerConfig` and `MinerData`
You can see more information on basic usage of the APIs past this example in the docs [here](https://pyasic.readthedocs.io/en/latest/API/api/).
`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()`.
Please see the appropriate API documentation page (pyasic docs -> Advanced -> Miner APIs -> your API type) for a link to that specific miner's API documentation page and more information.
---
#### List available API commands
##### MinerData
`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.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` instances can also be added to each other to combine their data and can be divided by a number to divide all their data, allowing you to get average data from many miners by doing -
```python
from pyasic import MinerData
# examples of miner data
d1 = MinerData("192.168.1.1")
d2 = MinerData("192.168.1.2")
list_of_miner_data = [d1, d2]
average_data = sum(list_of_miner_data, start=MinerData("0.0.0.0"))/len(list_of_miner_data)
```
---
##### MinerConfig
`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` 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` and will do the conversion to the right type for you.
You can use the `MinerConfig` as follows:
```python
import asyncio
from pyasic import get_miner
async def get_api_commands(miner_ip: str):
# Get the miner
miner = await get_miner(miner_ip)
async def set_fault_light():
miner = await get_miner("192.168.1.20")
# List all available commands
# Can also be called explicitly with the function miner.api.get_commands()
print(miner.api.commands)
# get config
cfg = await miner.get_config()
# send config
await miner.send_config(cfg)
if __name__ == "__main__":
asyncio.run(get_api_commands("192.168.1.69"))
asyncio.run(set_fault_light())
```
#### Use miner API commands to gather data
---
## Settings
The miner API commands will raise an `APIError` if they fail with a bad status code, to bypass this you must send them manually by using `miner.api.send_command(command, ignore_errors=True)`
`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
import asyncio
from pyasic import settings
from pyasic import get_miner
async def get_api_commands(miner_ip: str):
# Get the miner
miner = await get_miner(miner_ip)
# Run the devdetails command
# This is equivalent to await miner.api.send_command("devdetails")
devdetails: dict = await miner.api.devdetails()
print(devdetails)
if __name__ == "__main__":
asyncio.run(get_api_commands("192.168.1.69"))
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

@@ -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

View File

@@ -31,6 +31,8 @@ def backend_str(backend: MinerTypes) -> str:
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:

View File

@@ -1,25 +1,31 @@
# 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/)
[![python](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![Read the Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![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)
[![PyPI - Version](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pyasic)](https://pypi.org/project/pyasic/)
[![Python - Supported Versions](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![CodeFactor - Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
[![Commit Activity - master](https://img.shields.io/github/commit-activity/y/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/commits/master/)
[![Code Style - Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Read The Docs - Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![License - Apache 2.0](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
---
## 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,20 +34,19 @@ 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
---
##### 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
@@ -58,6 +63,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,13 +74,14 @@ 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 import get_miner
@@ -88,7 +96,8 @@ async def gather_miner_data():
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
@@ -96,8 +105,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])
@@ -109,161 +118,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),
[`is_mining`](#is-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>
### Is Mining
::: pyasic.miners.BaseMiner.is_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
@@ -278,13 +186,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

@@ -29,6 +29,20 @@
show_root_heading: false
heading_level: 4
## 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
@@ -113,6 +127,13 @@
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
## T19 (BOS)
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
handler: python
@@ -176,3 +197,38 @@
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

@@ -0,0 +1,8 @@
# pyasic
## ePIC Backend
::: pyasic.miners.backends.epic.ePIC
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,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:

View File

@@ -10,9 +10,6 @@ details {
padding-top:0px;
padding-bottom:0px;
}
ul {
margin:0px;
}
</style>
<details>
@@ -73,6 +70,8 @@ ul {
<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>
@@ -94,6 +93,8 @@ ul {
<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>
@@ -108,6 +109,8 @@ 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>
@@ -157,6 +160,7 @@ ul {
<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>
@@ -190,6 +194,9 @@ ul {
<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>
@@ -205,7 +212,6 @@ ul {
<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#m31h-v40">M31H V40</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>
@@ -232,6 +238,7 @@ ul {
<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>
@@ -241,12 +248,15 @@ ul {
<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>
<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>
@@ -397,6 +407,7 @@ ul {
<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>
@@ -434,6 +445,21 @@ ul {
</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>

View File

@@ -29,6 +29,20 @@
show_root_heading: false
heading_level: 4
## M20P V10
::: pyasic.miners.whatsminer.btminer.M2X.M20P.BTMinerM20PV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M20P V30
::: pyasic.miners.whatsminer.btminer.M2X.M20P.BTMinerM20PV30
handler: python
options:
show_root_heading: false
heading_level: 4
## M20S+ V30
::: pyasic.miners.whatsminer.btminer.M2X.M20S_Plus.BTMinerM20SPlusV30
handler: python

View File

@@ -15,6 +15,20 @@
show_root_heading: false
heading_level: 4
## M30K V10
::: pyasic.miners.whatsminer.btminer.M3X.M30K.BTMinerM30KV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M30L V10
::: pyasic.miners.whatsminer.btminer.M3X.M30L.BTMinerM30LV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S V10
::: pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SV10
handler: python
@@ -358,6 +372,13 @@
show_root_heading: false
heading_level: 4
## M30S+ VG20
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVG20
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S+ VG30
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVG30
handler: python
@@ -589,6 +610,27 @@
show_root_heading: false
heading_level: 4
## M31H V10
::: pyasic.miners.whatsminer.btminer.M3X.M31H.BTMinerM31HV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M31H V40
::: pyasic.miners.whatsminer.btminer.M3X.M31H.BTMinerM31HV40
handler: python
options:
show_root_heading: false
heading_level: 4
## M30L V10
::: pyasic.miners.whatsminer.btminer.M3X.M31L.BTMinerM31LV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S V10
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV10
handler: python
@@ -694,13 +736,6 @@
show_root_heading: false
heading_level: 4
## M31H V40
::: pyasic.miners.whatsminer.btminer.M3X.M31H.BTMinerM31HV40
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+ V10
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV10
handler: python
@@ -883,6 +918,13 @@
show_root_heading: false
heading_level: 4
## M33S+ VG20
::: pyasic.miners.whatsminer.btminer.M3X.M33S_Plus.BTMinerM33SPlusVG20
handler: python
options:
show_root_heading: false
heading_level: 4
## M33S+ VH20
::: pyasic.miners.whatsminer.btminer.M3X.M33S_Plus.BTMinerM33SPlusVH20
handler: python
@@ -946,6 +988,13 @@
show_root_heading: false
heading_level: 4
## M39 V10
::: pyasic.miners.whatsminer.btminer.M3X.M39.BTMinerM39V10
handler: python
options:
show_root_heading: false
heading_level: 4
## M39 V20
::: pyasic.miners.whatsminer.btminer.M3X.M39.BTMinerM39V20
handler: python
@@ -953,3 +1002,10 @@
show_root_heading: false
heading_level: 4
## M39 V30
::: pyasic.miners.whatsminer.btminer.M3X.M39.BTMinerM39V30
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,6 +1,13 @@
# pyasic
## M5X Models
## M50 VE30
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VE30
handler: python
options:
show_root_heading: false
heading_level: 4
## M50 VG30
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VG30
handler: python

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"
@@ -30,6 +30,7 @@ nav:
- 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"
@@ -53,7 +54,8 @@ nav:
- 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

@@ -20,7 +20,7 @@ import json
import logging
import re
import warnings
from typing import Tuple, Union
from typing import Union
from pyasic.errors import APIError, APIWarning
@@ -83,15 +83,15 @@ class BaseMinerAPI:
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 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]}"
)
raise APIError(validation[1])
logging.debug(f"{self} - (Send Command) - Received data.")
return data
@@ -118,6 +118,7 @@ class BaseMinerAPI:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError as e:
# try to identify the error
if e.message is not None:
if ":" in e.message:
err_command = e.message.split(":")[0]
if err_command in commands:
@@ -206,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")
@@ -254,6 +257,12 @@ 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:

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
@@ -481,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
@@ -693,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>
@@ -713,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", '
@@ -729,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,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,664 +13,161 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from copy import deepcopy
from dataclasses import asdict, dataclass, field
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 = field(default_factory=PoolConfig.default)
fan_mode: FanModeConfig = field(default_factory=FanModeConfig.default)
temperature: TemperatureConfig = field(default_factory=TemperatureConfig.default)
mining_mode: MiningModeConfig = field(default_factory=MiningModeConfig.default)
power_scaling: PowerScalingConfig = field(
default_factory=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)
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
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(),
}
def as_epic(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_epic(),
**self.temperature.as_epic(),
**self.mining_mode.as_epic(),
**self.pools.as_epic(user_suffix=user_suffix),
**self.power_scaling.as_epic(),
}
@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")),
)
@classmethod
def from_api(cls, api_pools: dict) -> "MinerConfig":
return cls(pools=PoolConfig.from_api(api_pools))
@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),
)
@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:
cfg["autotuning"]["enabled"] = False
if self.autotuning_mode:
cfg["format"]["version"] = "2.0"
cfg["autotuning"]["mode"] = self.autotuning_mode + "_target"
if self.autotuning_wattage:
cfg["autotuning"]["power_target"] = self.autotuning_wattage
elif self.autotuning_hashrate:
cfg["autotuning"]["hashrate_target"] = self.autotuning_hashrate
else:
if self.autotuning_wattage:
cfg["autotuning"]["psu_power_limit"] = self.autotuning_wattage
if self.asicboost:
cfg["hash_chain_global"] = {}
cfg["hash_chain_global"]["asic_boost"] = self.asicboost
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
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
return toml.dumps(cfg)
result[b_key] = deepcopy(b_val)
return result

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

@@ -0,0 +1,95 @@
# ------------------------------------------------------------------------------
# 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 as_epic(self) -> dict:
return self.value.as_epic()
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 {}
def as_epic(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")
speed: int = 100
minimum_fans: int = 1
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "FanModeManual":
cls_conf = {}
if dict_conf.get("speed") is not None:
cls_conf["speed"] = dict_conf["speed"]
if dict_conf.get("minimum_fans") is not None:
cls_conf["minimum_fans"] = dict_conf["minimum_fans"]
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

@@ -22,6 +22,9 @@ from dataclasses import asdict, dataclass, field, fields
from datetime import datetime, timezone
from typing import Any, List, Union
from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
@@ -35,7 +38,7 @@ class HashBoard:
temp: The temperature of the PCB as an int.
chip_temp: The temperature of the chips as an int.
chips: The chip count of the board as an int.
expected_chips: The ideal chip count of the board as an int.
expected_chips: The expected chip count of the board as an int.
missing: Whether the board is returned from the miners data as a bool.
"""
@@ -105,7 +108,7 @@ class MinerData:
hostname: The network hostname of the miner as a str.
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.
expected_hashrate: The factory nominal hashrate 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.
@@ -114,10 +117,10 @@ class MinerData:
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.
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.
percent_ideal_hashrate: The percent of total hashrate out of the ideal hashrate. Calculated automatically.
percent_ideal_wattage: The percent of total wattage out of the ideal wattage. Calculated automatically.
expected_chips: The expected number of chips in the miner as an int.
percent_expected_chips: The percent of total chips out of the expected count. Calculated automatically.
percent_expected_hashrate: The percent of total hashrate out of the expected hashrate. Calculated automatically.
percent_expected_wattage: The percent of total wattage out of the expected wattage. Calculated automatically.
nominal: Whether the number of chips in the miner is nominal. Calculated automatically.
pool_split: The pool split as a str.
pool_1_url: The first pool url on the miner as a str.
@@ -140,27 +143,24 @@ class MinerData:
fw_ver: str = None
hostname: str = None
hashrate: float = field(init=False)
_hashrate: float = None
nominal_hashrate: float = None
_hashrate: float = field(repr=False, default=None)
expected_hashrate: float = None
hashboards: List[HashBoard] = field(default_factory=list)
ideal_hashboards: int = None
expected_hashboards: int = None
temperature_avg: int = field(init=False)
env_temp: float = None
wattage: int = None
wattage_limit: int = None
wattage_limit: int = field(init=False)
_wattage_limit: int = field(repr=False, default=None)
fans: List[Fan] = field(default_factory=list)
fan_psu: int = None
total_chips: int = field(init=False)
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)
expected_chips: int = None
percent_expected_chips: float = field(init=False)
percent_expected_hashrate: float = field(init=False)
percent_expected_wattage: float = field(init=False)
nominal: bool = field(init=False)
pool_split: str = "0"
pool_1_url: str = "Unknown"
pool_1_user: str = "Unknown"
pool_2_url: str = ""
pool_2_user: str = ""
config: MinerConfig = None
errors: List[
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
] = field(default_factory=list)
@@ -170,7 +170,11 @@ class MinerData:
@classmethod
def fields(cls):
return [f.name for f in fields(cls)]
return [f.name for f in fields(cls) if not f.name.startswith("_")]
@staticmethod
def dict_factory(x):
return {k: v for (k, v) in x if not k.startswith("_")}
def __post_init__(self):
self.datetime = datetime.now(timezone.utc).astimezone()
@@ -241,13 +245,24 @@ class MinerData:
if item.hashrate is not None:
hr_data.append(item.hashrate)
if len(hr_data) > 0:
return sum(hr_data)
return round(sum(hr_data), 2)
return self._hashrate
@hashrate.setter
def hashrate(self, val):
self._hashrate = val
@property
def wattage_limit(self): # noqa - Skip PyCharm inspection
if self.config is not None:
if isinstance(self.config.mining_mode, MiningModePowerTune):
return self.config.mining_mode.power
return self._wattage_limit
@wattage_limit.setter
def wattage_limit(self, val: int):
self._wattage_limit = val
@property
def total_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) > 0:
@@ -265,48 +280,48 @@ class MinerData:
@property
def nominal(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.ideal_chips is None:
if self.total_chips is None or self.expected_chips is None:
return None
return self.ideal_chips == self.total_chips
return self.expected_chips == self.total_chips
@nominal.setter
def nominal(self, val):
pass
@property
def percent_ideal_chips(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.ideal_chips is None:
def percent_expected_chips(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.expected_chips is None:
return None
if self.total_chips == 0 or self.ideal_chips == 0:
if self.total_chips == 0 or self.expected_chips == 0:
return 0
return round((self.total_chips / self.ideal_chips) * 100)
return round((self.total_chips / self.expected_chips) * 100)
@percent_ideal_chips.setter
def percent_ideal_chips(self, val):
@percent_expected_chips.setter
def percent_expected_chips(self, val):
pass
@property
def percent_ideal_hashrate(self): # noqa - Skip PyCharm inspection
if self.hashrate is None or self.nominal_hashrate is None:
def percent_expected_hashrate(self): # noqa - Skip PyCharm inspection
if self.hashrate is None or self.expected_hashrate is None:
return None
if self.hashrate == 0 or self.nominal_hashrate == 0:
if self.hashrate == 0 or self.expected_hashrate == 0:
return 0
return round((self.hashrate / self.nominal_hashrate) * 100)
return round((self.hashrate / self.expected_hashrate) * 100)
@percent_ideal_hashrate.setter
def percent_ideal_hashrate(self, val):
@percent_expected_hashrate.setter
def percent_expected_hashrate(self, val):
pass
@property
def percent_ideal_wattage(self): # noqa - Skip PyCharm inspection
def percent_expected_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)
@percent_ideal_wattage.setter
def percent_ideal_wattage(self, val):
@percent_expected_wattage.setter
def percent_expected_wattage(self, val):
pass
@property
@@ -338,13 +353,16 @@ class MinerData:
pass
def asdict(self) -> dict:
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
return asdict(self, dict_factory=self.dict_factory)
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.

View File

@@ -137,10 +137,10 @@ class _MinerPhaseBalancer:
for miner in self.miners
]
)
pct_ideal_list = [d.percent_ideal for d in data]
pct_expected_list = [d.percent_ideal for d in data]
pct_ideal = 0
if len(pct_ideal_list) > 0:
pct_ideal = sum(pct_ideal_list) / len(pct_ideal_list)
if len(pct_expected_list) > 0:
pct_ideal = sum(pct_expected_list) / len(pct_expected_list)
wattage = round(wattage * 1 / (pct_ideal / 100))

View File

@@ -16,18 +16,16 @@
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:
# 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",
@@ -35,10 +33,10 @@ def init_logger():
_logger = logging.getLogger()
if PyasicSettings().debug:
_logger.setLevel(logging.DEBUG)
logging.getLogger("asyncssh").setLevel(logging.DEBUG)
else:
# if PyasicSettings().debug:
# _logger.setLevel(logging.DEBUG)
# logging.getLogger("asyncssh").setLevel(logging.DEBUG)
# else:
_logger.setLevel(logging.WARNING)
logging.getLogger("asyncssh").setLevel(logging.WARNING)

View File

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

View File

@@ -21,10 +21,13 @@ from pyasic.miners.types import (
S19XP,
S19a,
S19aPro,
S19i,
S19j,
S19jNoPIC,
S19jPro,
S19Plus,
S19Pro,
S19ProHydro,
S19ProPlus,
)
@@ -33,6 +36,14 @@ class BMMinerS19(AntminerModern, S19):
pass
class BMMinerS19Plus(AntminerModern, S19Plus):
pass
class BMMinerS19i(AntminerModern, S19i):
pass
class BMMinerS19Pro(AntminerModern, S19Pro):
pass
@@ -67,3 +78,7 @@ class BMMinerS19jPro(AntminerModern, S19jPro):
class BMMinerS19L(AntminerModern, S19L):
pass
class BMMinerS19ProHydro(AntminerModern, S19ProHydro):
pass

View File

@@ -18,11 +18,14 @@ from .S19 import (
BMMinerS19,
BMMinerS19a,
BMMinerS19aPro,
BMMinerS19i,
BMMinerS19j,
BMMinerS19jNoPIC,
BMMinerS19jPro,
BMMinerS19L,
BMMinerS19Plus,
BMMinerS19Pro,
BMMinerS19ProHydro,
BMMinerS19ProPlus,
BMMinerS19XP,
)

View File

@@ -15,17 +15,37 @@
# ------------------------------------------------------------------------------
from pyasic.miners.backends import BOSMiner
from pyasic.miners.types import S19, S19j, S19jNoPIC, S19jPro, S19Pro
from pyasic.miners.types import (
S19,
S19XP,
S19a,
S19aPro,
S19j,
S19jNoPIC,
S19jPro,
S19jProPlus,
S19kProNoPIC,
S19Plus,
S19Pro,
)
class BOSMinerS19(BOSMiner, S19):
pass
class BOSMinerS19Plus(BOSMiner, S19Plus):
pass
class BOSMinerS19Pro(BOSMiner, S19Pro):
pass
class BOSMinerS19a(BOSMiner, S19a):
pass
class BOSMinerS19j(BOSMiner, S19j):
pass
@@ -36,3 +56,19 @@ class BOSMinerS19jNoPIC(BOSMiner, S19jNoPIC):
class BOSMinerS19jPro(BOSMiner, S19jPro):
pass
class BOSMinerS19kProNoPIC(BOSMiner, S19kProNoPIC):
pass
class BOSMinerS19aPro(BOSMiner, S19aPro):
pass
class BOSMinerS19jProPlus(BOSMiner, S19jProPlus):
pass
class BOSMinerS19XP(BOSMiner, S19XP):
pass

View File

@@ -16,9 +16,15 @@
from .S19 import (
BOSMinerS19,
BOSMinerS19a,
BOSMinerS19aPro,
BOSMinerS19j,
BOSMinerS19jNoPIC,
BOSMinerS19jPro,
BOSMinerS19jProPlus,
BOSMinerS19kProNoPIC,
BOSMinerS19Plus,
BOSMinerS19Pro,
BOSMinerS19XP,
)
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,
ePICS19j,
ePICS19jPro,
ePICS19jProPlus,
ePICS19kPro,
ePICS19Pro,
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

@@ -54,7 +54,7 @@ class HiveonT9(Hiveon, T9):
hashboards = []
for board in board_map:
hashboard = HashBoard(slot=board, expected_chips=self.nominal_chips)
hashboard = HashBoard(slot=board, expected_chips=self.expected_chips)
hashrate = 0
chips = 0
for chipset in board_map[board]:

View File

@@ -21,7 +21,8 @@ from .bosminer import BOSMiner
from .btminer import BTMiner
from .cgminer import CGMiner
from .cgminer_avalon import CGMinerAvalon
from .epic import ePIC
from .hiveon import Hiveon
from .luxminer import LUXMiner
from .vnish import VNish
from .whatsminer import M2X, M3X, M5X
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
@@ -38,8 +38,8 @@ ANTMINER_MODERN_DATA_LOC = {
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_stats": {"api": "stats"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"api_stats": {"api": "stats"}}},
@@ -62,6 +62,10 @@ ANTMINER_MODERN_DATA_LOC = {
"cmd": "get_uptime",
"kwargs": {"api_stats": {"api": "stats"}},
},
"config": {
"cmd": "get_config",
"kwargs": {},
},
}
@@ -80,23 +84,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)
@@ -109,7 +111,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:
@@ -120,13 +122,13 @@ 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
@@ -200,7 +202,7 @@ class AntminerModern(BMMiner):
pass
return self.light
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
async def get_expected_hashrate(self, api_stats: dict = None) -> Optional[float]:
if not api_stats:
try:
api_stats = await self.api.stats()
@@ -209,17 +211,17 @@ class AntminerModern(BMMiner):
if api_stats:
try:
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
expected_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)
return round(expected_rate / 1000, 2)
if rate_unit == "MH":
return round(ideal_rate / 1000000, 2)
return round(expected_rate / 1000000, 2)
else:
return round(ideal_rate, 2)
return round(expected_rate, 2)
except (KeyError, IndexError):
pass
@@ -274,7 +276,11 @@ class AntminerModern(BMMiner):
if web_get_conf:
try:
return False if int(web_get_conf["bitmain-work-mode"]) == 1 else True
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
@@ -294,10 +300,7 @@ class AntminerModern(BMMiner):
ANTMINER_OLD_DATA_LOC = {
"mac": {"cmd": "get_mac", "kwargs": {}},
"model": {
"cmd": "get_model",
"kwargs": {},
},
"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": {
@@ -305,8 +308,8 @@ ANTMINER_OLD_DATA_LOC = {
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_stats": {"api": "stats"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"api_stats": {"api": "stats"}}},
@@ -325,10 +328,8 @@ ANTMINER_OLD_DATA_LOC = {
"cmd": "is_mining",
"kwargs": {"web_get_conf": {"web": "get_miner_conf"}},
},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_stats": {"api": "stats"}},
},
"uptime": {"cmd": "get_uptime", "kwargs": {"api_stats": {"api": "stats"}}},
"config": {"cmd": "get_config", "kwargs": {}},
}
@@ -345,11 +346,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:
@@ -469,9 +471,11 @@ class AntminerOld(CGMiner):
if board_offset == -1:
board_offset = 1
for i in range(board_offset, board_offset + self.ideal_hashboards):
for i in range(
board_offset, board_offset + self.expected_hashboards
):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
slot=i - board_offset, expected_chips=self.expected_chips
)
chip_temp = boards[1].get(f"temp{i}")

View File

@@ -31,8 +31,8 @@ BFGMINER_DATA_LOC = {
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_stats": {"api": "stats"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"api_stats": {"api": "stats"}}},
@@ -46,6 +46,7 @@ BFGMINER_DATA_LOC = {
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {}},
"config": {"cmd": "get_config", "kwargs": {}},
}
@@ -72,7 +73,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:
@@ -197,9 +198,11 @@ class BFGMiner(BaseMiner):
if board_offset == -1:
board_offset = 1
for i in range(board_offset, board_offset + self.ideal_hashboards):
for i in range(
board_offset, board_offset + self.expected_hashboards
):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
slot=i - board_offset, expected_chips=self.expected_chips
)
chip_temp = boards[1].get(f"temp{i}")
@@ -297,7 +300,7 @@ class BFGMiner(BaseMiner):
async def get_fault_light(self) -> bool:
return False
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
async def get_expected_hashrate(self, api_stats: dict = None) -> Optional[float]:
# X19 method, not sure compatibility
if not api_stats:
try:
@@ -307,17 +310,17 @@ class BFGMiner(BaseMiner):
if api_stats:
try:
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
expected_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)
return round(expected_rate / 1000, 2)
if rate_unit == "MH":
return round(ideal_rate / 1000000, 2)
return round(expected_rate / 1000000, 2)
else:
return round(ideal_rate, 2)
return round(expected_rate, 2)
except (KeyError, IndexError):
pass

View File

@@ -29,8 +29,8 @@ GOLDSHELL_DATA_LOC = {
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"web_status": {"web": "status"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_stats": {"api": "stats"}},
},
"hashboards": {
@@ -50,6 +50,10 @@ GOLDSHELL_DATA_LOC = {
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {}},
"config": {
"cmd": "get_config",
"kwargs": {},
},
}
@@ -64,7 +68,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()
@@ -80,7 +91,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"]
)
@@ -121,8 +132,8 @@ class BFGMinerGoldshell(BFGMiner):
pass
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if api_devs:

View File

@@ -32,8 +32,8 @@ BMMINER_DATA_LOC = {
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_stats": {"api": "stats"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"api_stats": {"api": "stats"}}},
@@ -46,10 +46,8 @@ BMMINER_DATA_LOC = {
"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"}},
},
"uptime": {"cmd": "get_uptime", "kwargs": {"api_stats": {"api": "stats"}}},
"config": {"cmd": "get_config", "kwargs": {}},
}
@@ -104,7 +102,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:
@@ -239,18 +237,20 @@ class BMMiner(BaseMiner):
for i in range(board_offset, board_offset + 4):
try:
key = f'chain_acs{i}'
if boards[1].get(key, '') != '':
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))
real_slots = list(
range(board_offset, board_offset + self.expected_hashboards)
)
for i in real_slots:
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
slot=i - board_offset, expected_chips=self.expected_chips
)
chip_temp = boards[1].get(f"temp{i}")
@@ -347,7 +347,7 @@ class BMMiner(BaseMiner):
async def get_fault_light(self) -> bool:
return False
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
async def get_expected_hashrate(self, api_stats: dict = None) -> Optional[float]:
# X19 method, not sure compatibility
if not api_stats:
try:
@@ -357,17 +357,17 @@ class BMMiner(BaseMiner):
if api_stats:
try:
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
expected_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)
return round(expected_rate / 1000, 2)
if rate_unit == "MH":
return round(ideal_rate / 1000000, 2)
return round(expected_rate / 1000000, 2)
else:
return round(ideal_rate, 2)
return round(expected_rate, 2)
except (KeyError, IndexError):
pass

View File

@@ -15,6 +15,7 @@
# ------------------------------------------------------------------------------
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
@@ -36,10 +38,7 @@ BOSMINER_DATA_LOC = {
},
},
"model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {
"cmd": "get_api_ver",
"kwargs": {"api_version": {"api": "version"}},
},
"api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}},
"fw_ver": {
"cmd": "get_fw_ver",
"kwargs": {
@@ -63,8 +62,8 @@ BOSMINER_DATA_LOC = {
},
},
},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_devs": {"api": "devs"}},
},
"hashboards": {
@@ -176,19 +175,17 @@ BOSMINER_DATA_LOC = {
"cmd": "is_mining",
"kwargs": {"api_devdetails": {"api": "devdetails"}},
},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_summary": {"api": "summary"}},
},
"uptime": {"cmd": "get_uptime", "kwargs": {"api_summary": {"api": "summary"}}},
"config": {"cmd": "get_config", "kwargs": {}},
}
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"
@@ -297,23 +294,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 :/
@@ -321,21 +308,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(
@@ -365,7 +373,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}")
@@ -542,7 +552,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:
@@ -587,8 +596,8 @@ class BOSMiner(BaseMiner):
graphql_boards: dict = None,
):
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if not graphql_boards and not (api_devs or api_temps or api_devdetails):
@@ -625,7 +634,7 @@ class BOSMiner(BaseMiner):
offset = 0
if 3 in b_names:
offset = 1
elif 6 in b_names:
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
@@ -790,7 +799,7 @@ class BOSMiner(BaseMiner):
)
except APIError:
pass
if graphql_fans:
if graphql_fans.get("data"):
fans = []
for n in range(self.fan_count):
try:
@@ -1046,10 +1055,10 @@ class BOSMiner(BaseMiner):
if data == "50":
self.light = True
return self.light
except TypeError:
except (TypeError, AttributeError):
return self.light
async def get_nominal_hashrate(self, api_devs: dict = None) -> Optional[float]:
async def get_expected_hashrate(self, api_devs: dict = None) -> Optional[float]:
if not api_devs:
try:
api_devs = await self.api.devs()
@@ -1063,14 +1072,14 @@ class BOSMiner(BaseMiner):
for board in api_devs["DEVS"]:
_id = board["ID"] - offset
nominal_hashrate = round(float(board["Nominal MHS"] / 1000000), 2)
if nominal_hashrate:
hr_list.append(nominal_hashrate)
expected_hashrate = round(float(board["Nominal MHS"] / 1000000), 2)
if expected_hashrate:
hr_list.append(expected_hashrate)
if len(hr_list) == 0:
return 0
else:
return round(
(sum(hr_list) / len(hr_list)) * self.ideal_hashboards, 2
(sum(hr_list) / len(hr_list)) * self.expected_hashboards, 2
)
except (IndexError, KeyError):
pass

View File

@@ -145,7 +145,7 @@ class BOSMinerOld(BOSMiner):
async def get_fault_light(self, *args, **kwargs) -> bool:
return False
async def get_nominal_hashrate(self, *args, **kwargs) -> Optional[float]:
async def get_expected_hashrate(self, *args, **kwargs) -> Optional[float]:
return None
async def get_data(self, allow_warning: bool = False, **kwargs) -> MinerData:

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
@@ -51,8 +50,8 @@ BTMINER_DATA_LOC = {
"kwargs": {"api_get_miner_info": {"api": "get_miner_info"}},
},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_summary": {"api": "summary"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"api_devs": {"api": "devs"}}},
@@ -89,10 +88,8 @@ BTMINER_DATA_LOC = {
},
"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"}},
},
"uptime": {"cmd": "get_uptime", "kwargs": {"api_summary": {"api": "summary"}}},
"config": {"cmd": "get_config", "kwargs": {}},
}
@@ -198,43 +195,67 @@ 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()
is_mining = await self.is_mining(status)
if not is_mining:
cfg.mining_mode = MiningModeConfig.sleep()
return cfg
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:
@@ -386,10 +407,9 @@ 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)
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if not api_devs:
@@ -404,10 +424,10 @@ class BTMiner(BaseMiner):
if len(hashboards) < board["ASC"] + 1:
hashboards.append(
HashBoard(
slot=board["ASC"], expected_chips=self.nominal_chips
slot=board["ASC"], expected_chips=self.expected_chips
)
)
self.ideal_hashboards += 1
self.expected_hashboards += 1
hashboards[board["ASC"]].chip_temp = round(board["Chip Temp Avg"])
hashboards[board["ASC"]].temp = round(board["Temperature"])
hashboards[board["ASC"]].hashrate = round(
@@ -442,7 +462,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
@@ -569,7 +590,7 @@ class BTMiner(BaseMiner):
return errors
async def get_nominal_hashrate(self, api_summary: dict = None):
async def get_expected_hashrate(self, api_summary: dict = None):
if not api_summary:
try:
api_summary = await self.api.summary()
@@ -578,9 +599,9 @@ class BTMiner(BaseMiner):
if api_summary:
try:
nominal_hashrate = api_summary["SUMMARY"][0]["Factory GHS"]
if nominal_hashrate:
return round(nominal_hashrate / 1000, 2)
expected_hashrate = api_summary["SUMMARY"][0]["Factory GHS"]
if expected_hashrate:
return round(expected_hashrate / 1000, 2)
except (KeyError, IndexError):
pass

View File

@@ -32,8 +32,8 @@ CGMINER_DATA_LOC = {
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_stats": {"api": "stats"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"api_stats": {"api": "stats"}}},
@@ -46,10 +46,8 @@ CGMINER_DATA_LOC = {
"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"}},
},
"uptime": {"cmd": "get_uptime", "kwargs": {"api_stats": {"api": "stats"}}},
"config": {"cmd": "get_config", "kwargs": {}},
}
@@ -143,10 +141,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:
@@ -256,9 +257,11 @@ class CGMiner(BaseMiner):
if board_offset == -1:
board_offset = 1
for i in range(board_offset, board_offset + self.ideal_hashboards):
for i in range(
board_offset, board_offset + self.expected_hashboards
):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
slot=i - board_offset, expected_chips=self.expected_chips
)
chip_temp = boards[1].get(f"temp{i}")
@@ -357,7 +360,7 @@ class CGMiner(BaseMiner):
async def get_fault_light(self) -> bool:
return False
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
async def get_expected_hashrate(self, api_stats: dict = None) -> Optional[float]:
# X19 method, not sure compatibility
if not api_stats:
try:
@@ -367,17 +370,17 @@ class CGMiner(BaseMiner):
if api_stats:
try:
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
expected_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)
return round(expected_rate / 1000, 2)
if rate_unit == "MH":
return round(ideal_rate / 1000000, 2)
return round(expected_rate / 1000000, 2)
else:
return round(ideal_rate, 2)
return round(expected_rate, 2)
except (KeyError, IndexError):
pass

View File

@@ -31,8 +31,8 @@ AVALON_DATA_LOC = {
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {"mac": {"api": "version"}}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_devs": {"api": "devs"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_stats": {"api": "stats"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"api_stats": {"api": "stats"}}},
@@ -52,6 +52,7 @@ AVALON_DATA_LOC = {
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {}},
"config": {"cmd": "get_config", "kwargs": {}},
}
@@ -100,17 +101,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
# 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
@@ -201,8 +202,8 @@ class CGMinerAvalon(CGMiner):
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if not api_stats:
@@ -218,7 +219,7 @@ class CGMinerAvalon(CGMiner):
except (IndexError, KeyError, ValueError, TypeError):
return hashboards
for board in range(self.ideal_hashboards):
for board in range(self.expected_hashboards):
try:
hashboards[board].chip_temp = int(parsed_stats["MTmax"][board])
except LookupError:
@@ -247,7 +248,7 @@ class CGMinerAvalon(CGMiner):
return hashboards
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
async def get_expected_hashrate(self, api_stats: dict = None) -> Optional[float]:
if not api_stats:
try:
api_stats = await self.api.stats()

View File

@@ -0,0 +1,312 @@
# ------------------------------------------------------------------------------
# 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"}}},
"expected_hashrate": {
"cmd": "get_expected_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"}}},
"wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}},
"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"}}},
"config": {"cmd": "get_config", "kwargs": {}},
}
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_expected_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.expected_chips)
for i in range(self.expected_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

@@ -30,69 +30,25 @@ from pyasic.miners.base import BaseMiner
from pyasic.web.bosminer import BOSMinerWebAPI
LUXMINER_DATA_LOC = {
"mac": {
"cmd": "get_mac",
"kwargs": {"api_config": {"api": "config"}},
},
"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": {},
},
"api_ver": {"cmd": "get_api_ver", "kwargs": {}},
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {}},
"hostname": {"cmd": "get_hostname", "kwargs": {}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {}},
"expected_hashrate": {"cmd": "get_expected_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"}},
},
"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"}}},
"config": {"cmd": "get_config", "kwargs": {}},
}
@@ -196,15 +152,9 @@ class LUXMiner(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`.
"""
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
pass
async def set_power_limit(self, wattage: int) -> bool:
@@ -283,9 +233,11 @@ class LUXMiner(BaseMiner):
if board_offset == -1:
board_offset = 1
for i in range(board_offset, board_offset + self.ideal_hashboards):
for i in range(
board_offset, board_offset + self.expected_hashboards
):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
slot=i - board_offset, expected_chips=self.expected_chips
)
chip_temp = boards[1].get(f"temp{i}")
@@ -407,7 +359,7 @@ class LUXMiner(BaseMiner):
async def get_fault_light(self) -> bool:
pass
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
async def get_expected_hashrate(self, api_stats: dict = None) -> Optional[float]:
if not api_stats:
try:
api_stats = await self.api.stats()
@@ -416,17 +368,17 @@ class LUXMiner(BaseMiner):
if api_stats:
try:
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
expected_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)
return round(expected_rate / 1000, 2)
if rate_unit == "MH":
return round(ideal_rate / 1000000, 2)
return round(expected_rate / 1000000, 2)
else:
return round(ideal_rate, 2)
return round(expected_rate, 2)
except (KeyError, IndexError):
pass

View File

@@ -28,8 +28,8 @@ VNISH_DATA_LOC = {
"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": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"expected_hashrate": {
"cmd": "get_expected_hashrate",
"kwargs": {"api_stats": {"api": "stats"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"api_stats": {"api": "stats"}}},
@@ -46,6 +46,7 @@ VNISH_DATA_LOC = {
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {}},
"config": {"cmd": "get_config", "kwargs": {}},
}

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

@@ -42,8 +42,8 @@ class BaseMiner(ABC):
self.make = None
self.model = None
# physical attributes
self.ideal_hashboards = 3
self.nominal_chips = 0
self.expected_hashboards = 3
self.expected_chips = 0
self.fan_count = 2
# data gathering locations
self.data_locations = None
@@ -173,7 +173,7 @@ class BaseMiner(ABC):
@abstractmethod
async def get_config(self) -> MinerConfig:
# Not a data gathering function, since this is used for configuration and not MinerData
# Not a data gathering function, since this is used for configuration
"""Get the mining configuration of the miner and return it as a [`MinerConfig`][pyasic.config.MinerConfig].
Returns:
@@ -387,7 +387,7 @@ class BaseMiner(ABC):
pass
@abstractmethod
async def get_nominal_hashrate(self, *args, **kwargs) -> Optional[float]:
async def get_expected_hashrate(self, *args, **kwargs) -> Optional[float]:
"""Get the nominal hashrate from factory if available.
Returns:
@@ -491,29 +491,7 @@ class BaseMiner(ABC):
continue
function = getattr(self, self.data_locations[data_name]["cmd"])
if not data_name == "pools":
miner_data[data_name] = await function(**args_to_send)
else:
pools_data = await function(**args_to_send)
if pools_data:
try:
miner_data["pool_1_url"] = pools_data[0]["pool_1_url"]
miner_data["pool_1_user"] = pools_data[0]["pool_1_user"]
except KeyError:
pass
if len(pools_data) > 1:
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']}"
else:
try:
miner_data["pool_2_url"] = pools_data[0]["pool_2_url"]
miner_data["pool_2_user"] = pools_data[0]["pool_2_user"]
miner_data["quota"] = "0"
except KeyError:
pass
return miner_data
async def get_data(
@@ -532,16 +510,16 @@ class BaseMiner(ABC):
data = MinerData(
ip=str(self.ip),
make=self.make,
ideal_chips=self.nominal_chips * self.ideal_hashboards,
ideal_hashboards=self.ideal_hashboards,
expected_chips=self.expected_chips * self.expected_hashboards,
expected_hashboards=self.expected_hashboards,
hashboards=[
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
],
)
gathered_data = await self._get_data(
allow_warning, include=include, exclude=exclude
allow_warning=allow_warning, include=include, exclude=exclude
)
for item in gathered_data:
if gathered_data[item] is not None:

View File

@@ -152,8 +152,8 @@ class CGMinerA10X(CGMiner, A10X):
self, api_stats: dict = None, web_get_all: dict = None
) -> List[HashBoard]:
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if web_get_all:
web_get_all = web_get_all["all"]

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:
# get pool data
try:
api_pools = await self.api.pools()
except APIError as e:
logging.warning(e)
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:
@@ -136,8 +133,8 @@ class CGMinerT3HPlus(CGMiner, T3HPlus):
web_get_all = web_get_all["all"]
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if not api_stats:

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 anyio
import httpx
from pyasic import settings
from pyasic.logger import logger
from pyasic.miners.antminer import *
from pyasic.miners.avalonminer import *
@@ -37,6 +37,7 @@ from pyasic.miners.backends import (
Hiveon,
LUXMiner,
VNish,
ePIC,
)
from pyasic.miners.base import AnyMiner
from pyasic.miners.goldshell import *
@@ -44,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
@@ -58,6 +56,7 @@ class MinerTypes(enum.Enum):
VNISH = 6
HIVEON = 7
LUX_OS = 8
EPIC = 9
MINER_CLASSES = {
@@ -85,12 +84,15 @@ 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,
"ANTMINER S19 XP": BMMinerS19XP,
"ANTMINER S19A": BMMinerS19a,
"ANTMINER S19A PRO": BMMinerS19aPro,
"ANTMINER S19 PRO HYD.": BMMinerS19ProHydro,
"ANTMINER T19": BMMinerT19,
},
MinerTypes.WHATSMINER: {
@@ -99,6 +101,8 @@ MINER_CLASSES = {
"M20SV10": BTMinerM20SV10,
"M20SV20": BTMinerM20SV20,
"M20SV30": BTMinerM20SV30,
"M20PV10": BTMinerM20PV10,
"M20PV30": BTMinerM20PV30,
"M20S+V30": BTMinerM20SPlusV30,
"M21V10": BTMinerM21V10,
"M21SV20": BTMinerM21SV20,
@@ -108,6 +112,8 @@ MINER_CLASSES = {
"M29V10": BTMinerM29V10,
"M30V10": BTMinerM30V10,
"M30V20": BTMinerM30V20,
"M30KV10": BTMinerM30KV10,
"M30LV10": BTMinerM30LV10,
"M30SV10": BTMinerM30SV10,
"M30SV20": BTMinerM30SV20,
"M30SV30": BTMinerM30SV30,
@@ -157,6 +163,7 @@ MINER_CLASSES = {
"M30S+VE100": BTMinerM30SPlusVE100,
"M30S+VF20": BTMinerM30SPlusVF20,
"M30S+VF30": BTMinerM30SPlusVF30,
"M30S+VG20": BTMinerM30SPlusVG20,
"M30S+VG30": BTMinerM30SPlusVG30,
"M30S+VG40": BTMinerM30SPlusVG40,
"M30S+VG50": BTMinerM30SPlusVG50,
@@ -190,6 +197,9 @@ MINER_CLASSES = {
"M30S++VJ30": BTMinerM30SPlusPlusVJ30,
"M31V10": BTMinerM31V10,
"M31V20": BTMinerM31V20,
"M31HV10": BTMinerM31HV10,
"M31HV40": BTMinerM31HV40,
"M31LV10": BTMinerM31LV10,
"M31SV10": BTMinerM31SV10,
"M31SV20": BTMinerM31SV20,
"M31SV30": BTMinerM31SV30,
@@ -205,7 +215,6 @@ MINER_CLASSES = {
"M31SEV10": BTMinerM31SEV10,
"M31SEV20": BTMinerM31SEV20,
"M31SEV30": BTMinerM31SEV30,
"M31HV40": BTMinerM31HV40,
"M31S+V10": BTMinerM31SPlusV10,
"M31S+V20": BTMinerM31SPlusV20,
"M31S+V30": BTMinerM31SPlusV30,
@@ -232,6 +241,7 @@ MINER_CLASSES = {
"M33V20": BTMinerM33V20,
"M33V30": BTMinerM33V30,
"M33SVG30": BTMinerM33SVG30,
"M33S+VG20": BTMinerM33SPlusVG20,
"M33S+VH20": BTMinerM33SPlusVH20,
"M33S+VH30": BTMinerM33SPlusVH30,
"M33S++VH20": BTMinerM33SPlusPlusVH20,
@@ -241,7 +251,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,
@@ -276,6 +289,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,
@@ -315,11 +347,17 @@ MINER_CLASSES = {
"ANTMINER T17+": BOSMinerT17Plus,
"ANTMINER T17E": BOSMinerT17e,
"ANTMINER S19": BOSMinerS19,
"ANTMINER S19+": BOSMinerS19Plus,
"ANTMINER S19 PRO": BOSMinerS19Pro,
"ANTMINER S19A": BOSMinerS19a,
"ANTMINER S19A Pro": BOSMinerS19aPro,
"ANTMINER S19J": BOSMinerS19j,
"ANTMINER S19J88NOPIC": BOSMinerS19jNoPIC,
"ANTMINER S19J PRO": BOSMinerS19jPro,
"ANTMINER S19J PRO NOPIC": BOSMinerS19jPro,
"ANTMINER S19J PRO+": BOSMinerS19jProPlus,
"ANTMINER S19K PRO NOPIC": BOSMinerS19kProNoPIC,
"ANTMINER S19XP": BOSMinerS19XP,
"ANTMINER T19": BOSMinerT19,
},
MinerTypes.VNISH: {
@@ -336,6 +374,16 @@ 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,
@@ -348,24 +396,18 @@ MINER_CLASSES = {
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():
res = None
for fut in asyncio.as_completed(tasks):
res = await fut
if verification_func(res):
break
for t in tasks:
t.cancel()
try:
result = await task
await t
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
pass
return res
class MinerFactory:
@@ -375,7 +417,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):
@@ -383,7 +427,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)
@@ -391,13 +435,10 @@ 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)
@@ -406,12 +447,14 @@ class MinerFactory:
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
@@ -426,6 +469,7 @@ class MinerFactory:
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,
}
@@ -434,12 +478,21 @@ class MinerFactory:
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):
@@ -455,24 +508,44 @@ class MinerFactory:
return await concurrent_get_first_result(tasks, lambda x: x is not None)
async def _get_miner_web(self, ip: str):
tasks = []
try:
urls = [f"http://{ip}/", f"https://{ip}/"]
async with httpx.AsyncClient(verify=False) as session:
tasks = [asyncio.create_task(self._web_ping(session, url)) for url in urls]
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
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: httpx.AsyncClient, url: str
) -> Tuple[Optional[str], Optional[httpx.Response]]:
try:
resp = await session.get(url, follow_redirects=False)
resp = await session.get(url, follow_redirects=True)
return resp.text, resp
except (httpx.HTTPError, asyncio.TimeoutError):
except (
httpx.HTTPError,
asyncio.TimeoutError,
anyio.EndOfStream,
anyio.ClosedResourceError,
):
pass
return None, None
@@ -482,8 +555,12 @@ class MinerFactory:
"www-authenticate", ""
):
return MinerTypes.ANTMINER
if web_resp.status_code == 307 and "https://" in web_resp.headers.get(
"location", ""
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:
@@ -492,28 +569,43 @@ class MinerFactory:
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):
tasks = []
try:
commands = ["version", "devdetails"]
tasks = [asyncio.create_task(self._socket_ping(ip, cmd)) for cmd in commands]
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
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
@@ -579,12 +671,12 @@ class MinerFactory:
location: str,
auth: Optional[httpx.DigestAuth] = None,
) -> Optional[dict]:
async with httpx.AsyncClient(verify=False) 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 (httpx.HTTPError, asyncio.TimeoutError):
logger.info(f"{ip}: Web command timeout.")
@@ -683,9 +775,13 @@ 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)
@@ -751,7 +847,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):
@@ -770,18 +867,20 @@ class MinerFactory:
async def get_miner_model_innosilicon(self, ip: str) -> Optional[str]:
try:
async with httpx.AsyncClient(verify=False) 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 = auth_req.json()["jwt"]
web_data = (await session.post(
web_data = (
await session.post(
f"http://{ip}/api/type",
headers={"Authorization": "Bearer " + auth},
data={},
)).json()
)
).json()
return web_data["type"]
except (httpx.HTTPError, LookupError):
pass
@@ -798,7 +897,7 @@ class MinerFactory:
pass
try:
async with httpx.AsyncClient(verify=False) as session:
async with httpx.AsyncClient(transport=settings.transport()) as session:
d = await session.post(
f"http://{ip}/graphql",
json={"query": "{bosminer {info{modelName}}}"},
@@ -810,6 +909,15 @@ class MinerFactory:
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:
@@ -828,6 +936,14 @@ class MinerFactory:
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
async def get_miner_model_hiveon(self, ip: str) -> Optional[str]:
sock_json_data = await self.send_api_command(ip, "version")
try:

View File

@@ -22,5 +22,5 @@ class Z15(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Z15"
self.nominal_chips = 3
self.expected_chips = 3
self.fan_count = 2

View File

@@ -22,7 +22,7 @@ class S17(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S17"
self.nominal_chips = 48
self.expected_chips = 48
self.fan_count = 4
@@ -30,7 +30,7 @@ class S17Plus(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.model = "S17+"
self.nominal_chips = 65
self.expected_chips = 65
self.fan_count = 4
@@ -39,7 +39,7 @@ class S17Pro(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S17 Pro"
self.nominal_chips = 48
self.expected_chips = 48
self.fan_count = 4
@@ -48,5 +48,5 @@ class S17e(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S17e"
self.nominal_chips = 135
self.expected_chips = 135
self.fan_count = 4

View File

@@ -22,7 +22,7 @@ class T17(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "T17"
self.nominal_chips = 30
self.expected_chips = 30
self.fan_count = 4
@@ -31,7 +31,7 @@ class T17Plus(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "T17+"
self.nominal_chips = 44
self.expected_chips = 44
self.fan_count = 4
@@ -40,5 +40,5 @@ class T17e(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "T17e"
self.nominal_chips = 78
self.expected_chips = 78
self.fan_count = 4

View File

@@ -22,7 +22,7 @@ class S19(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19"
self.nominal_chips = 76
self.expected_chips = 76
self.fan_count = 4
@@ -31,7 +31,7 @@ class S19NoPIC(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19 No PIC"
self.nominal_chips = 88
self.expected_chips = 88
self.fan_count = 4
@@ -40,7 +40,25 @@ class S19Pro(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19 Pro"
self.nominal_chips = 114
self.expected_chips = 114
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.expected_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.expected_chips = 80
self.fan_count = 4
@@ -49,7 +67,7 @@ class S19ProPlus(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19 Pro+"
self.nominal_chips = 120
self.expected_chips = 120
self.fan_count = 4
@@ -58,7 +76,7 @@ class S19XP(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19 XP"
self.nominal_chips = 110
self.expected_chips = 110
self.fan_count = 4
@@ -67,7 +85,7 @@ class S19a(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19a"
self.nominal_chips = 72
self.expected_chips = 72
self.fan_count = 4
@@ -76,7 +94,7 @@ class S19aPro(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19a Pro"
self.nominal_chips = 100
self.expected_chips = 100
self.fan_count = 4
@@ -85,7 +103,7 @@ class S19j(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19j"
self.nominal_chips = 114
self.expected_chips = 114
self.fan_count = 4
@@ -94,7 +112,7 @@ class S19jNoPIC(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19j No PIC"
self.nominal_chips = 88
self.expected_chips = 88
self.fan_count = 4
@@ -103,7 +121,25 @@ class S19jPro(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19j Pro"
self.nominal_chips = 126
self.expected_chips = 126
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.expected_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.expected_chips = 77
self.fan_count = 4
@@ -112,5 +148,24 @@ class S19L(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S19L"
self.nominal_chips = 76
self.expected_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.expected_chips = 77
self.fan_count = 4
class S19ProHydro(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 Pro Hydro"
self.expected_chips = 180
self.expected_hashboards = 4
self.fan_count = 0

View File

@@ -22,5 +22,5 @@ class T19(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "T19"
self.nominal_chips = 76
self.expected_chips = 76
self.fan_count = 4

View File

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

View File

@@ -22,6 +22,6 @@ class D3(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "D3"
self.nominal_chips = 60
self.ideal_hashboards = 3
self.expected_chips = 60
self.expected_hashboards = 3
self.fan_count = 2

View File

@@ -22,6 +22,6 @@ class HS3(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "HS3"
self.nominal_chips = 92
self.ideal_hashboards = 3
self.expected_chips = 92
self.expected_hashboards = 3
self.fan_count = 2

View File

@@ -21,5 +21,5 @@ class L3Plus(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "L3+"
self.nominal_chips = 72
self.expected_chips = 72
self.fan_count = 2

View File

@@ -22,6 +22,6 @@ class DR5(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "DR5"
self.nominal_chips = 72
self.ideal_hashboards = 3
self.expected_chips = 72
self.expected_hashboards = 3
self.fan_count = 2

View File

@@ -21,5 +21,5 @@ class L7(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "L7"
self.nominal_chips = 120
self.expected_chips = 120
self.fan_count = 4

View File

@@ -22,6 +22,6 @@ class E9Pro(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "E9Pro"
self.nominal_chips = 8
self.ideal_hashboards = 2
self.expected_chips = 8
self.expected_hashboards = 2
self.fan_count = 4

View File

@@ -22,7 +22,7 @@ class S9(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S9"
self.nominal_chips = 63
self.expected_chips = 63
self.fan_count = 2
@@ -31,7 +31,7 @@ class S9i(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S9i"
self.nominal_chips = 63
self.expected_chips = 63
self.fan_count = 2
@@ -40,5 +40,5 @@ class S9j(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "S9j"
self.nominal_chips = 63
self.expected_chips = 63
self.fan_count = 2

View File

@@ -22,5 +22,5 @@ class T9(AntMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "T9"
self.nominal_chips = 54
self.expected_chips = 54
self.fan_count = 2

View File

@@ -22,5 +22,5 @@ class Avalon1026(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 1026"
self.nominal_chips = 80
self.expected_chips = 80
self.fan_count = 2

View File

@@ -22,5 +22,5 @@ class Avalon1047(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 1047"
self.nominal_chips = 80
self.expected_chips = 80
self.fan_count = 2

View File

@@ -22,5 +22,5 @@ class Avalon1066(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 1066"
self.nominal_chips = 114
self.expected_chips = 114
self.fan_count = 4

View File

@@ -23,5 +23,5 @@ class Avalon1166Pro(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 1166 Pro"
self.nominal_chips = 120
self.expected_chips = 120
self.fan_count = 4

View File

@@ -23,5 +23,5 @@ class Avalon1246(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 1246"
self.nominal_chips = 120
self.expected_chips = 120
self.fan_count = 4

View File

@@ -22,6 +22,6 @@ class Avalon721(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 721"
self.ideal_hashboards = 4
self.nominal_chips = 18
self.expected_hashboards = 4
self.expected_chips = 18
self.fan_count = 1

View File

@@ -22,6 +22,6 @@ class Avalon741(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 741"
self.ideal_hashboards = 4
self.nominal_chips = 22
self.expected_hashboards = 4
self.expected_chips = 22
self.fan_count = 1

View File

@@ -22,6 +22,6 @@ class Avalon761(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 761"
self.ideal_hashboards = 4
self.nominal_chips = 18
self.expected_hashboards = 4
self.expected_chips = 18
self.fan_count = 1

View File

@@ -22,6 +22,6 @@ class Avalon821(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 821"
self.ideal_hashboards = 4
self.nominal_chips = 26
self.expected_hashboards = 4
self.expected_chips = 26
self.fan_count = 1

View File

@@ -22,6 +22,6 @@ class Avalon841(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 841"
self.ideal_hashboards = 4
self.nominal_chips = 26
self.expected_hashboards = 4
self.expected_chips = 26
self.fan_count = 1

View File

@@ -22,6 +22,6 @@ class Avalon851(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 851"
self.ideal_hashboards = 4
self.nominal_chips = 26
self.expected_hashboards = 4
self.expected_chips = 26
self.fan_count = 1

View File

@@ -22,6 +22,6 @@ class Avalon921(AvalonMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "Avalon 921"
self.ideal_hashboards = 4
self.nominal_chips = 26
self.expected_hashboards = 4
self.expected_chips = 26
self.fan_count = 1

View File

@@ -21,6 +21,6 @@ class CK5(GoldshellMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "CK5"
self.ideal_hashboards = 4
self.nominal_chips = 46
self.expected_hashboards = 4
self.expected_chips = 46
self.fan_count = 4

View File

@@ -21,6 +21,6 @@ class HS5(GoldshellMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "HS5"
self.ideal_hashboards = 4
self.nominal_chips = 46
self.expected_hashboards = 4
self.expected_chips = 46
self.fan_count = 4

View File

@@ -21,6 +21,6 @@ class KD5(GoldshellMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "KD5"
self.ideal_hashboards = 4
self.nominal_chips = 46
self.expected_hashboards = 4
self.expected_chips = 46
self.fan_count = 4

View File

@@ -21,6 +21,6 @@ class KDMax(GoldshellMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "KD Max"
self.ideal_hashboards = 3
self.nominal_chips = 84
self.expected_hashboards = 3
self.expected_chips = 84
self.fan_count = 4

View File

@@ -22,5 +22,5 @@ class T3HPlus(InnosiliconMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "T3H+"
self.nominal_chips = 114
self.expected_chips = 114
self.fan_count = 4

View File

@@ -22,5 +22,5 @@ class M20V10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M20 V10"
self.nominal_chips = 70
self.expected_chips = 70
self.fan_count = 2

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.expected_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.expected_chips = 148
self.fan_count = 2

View File

@@ -24,7 +24,7 @@ class M20SV10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M20S V10"
self.nominal_chips = 105
self.expected_chips = 105
self.fan_count = 2
@@ -33,7 +33,7 @@ class M20SV20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M20S V20"
self.nominal_chips = 111
self.expected_chips = 111
self.fan_count = 2
@@ -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.expected_chips = 140
self.fan_count = 2

View File

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

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.expected_chips = 33
self.fan_count = 2

View File

@@ -24,7 +24,7 @@ class M21SV20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M21S V20"
self.nominal_chips = 66
self.expected_chips = 66
self.fan_count = 2
@@ -33,7 +33,7 @@ class M21SV60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M21S V60"
self.nominal_chips = 105
self.expected_chips = 105
self.fan_count = 2
@@ -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.expected_chips = 111
self.fan_count = 2

View File

@@ -24,7 +24,7 @@ class M21SPlusV20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M21S+ V20"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M21S+ V20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)

View File

@@ -24,5 +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 = 50
self.expected_chips = 50
self.fan_count = 2

View File

@@ -15,6 +15,7 @@
# ------------------------------------------------------------------------------
from .M20 import M20V10
from .M20P import M20PV10, M20PV30
from .M20S import M20SV10, M20SV20, M20SV30
from .M20S_Plus import M20SPlusV30
from .M21 import M21V10

View File

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

View File

@@ -0,0 +1,29 @@
# ------------------------------------------------------------------------------
# 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 warnings
from pyasic.miners.makes import WhatsMiner
class M30KV10(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 = "M30K V10"
self.expected_hashboards = 4
self.expected_chips = 240
self.fan_count = 2

View File

@@ -0,0 +1,29 @@
# ------------------------------------------------------------------------------
# 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 warnings
from pyasic.miners.makes import WhatsMiner
class M30LV10(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 = "M30L V10"
self.board_num = 4
self.expected_chips = 144
self.fan_count = 2

View File

@@ -24,10 +24,7 @@ class M30SV10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 148
self.fan_count = 2
@@ -36,10 +33,7 @@ class M30SV20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 156
self.fan_count = 2
@@ -48,10 +42,7 @@ class M30SV30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S V30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 164
self.fan_count = 2
@@ -60,10 +51,7 @@ class M30SV40(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S V40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 172
self.fan_count = 2
@@ -72,7 +60,7 @@ class M30SV50(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S V50"
self.nominal_chips = 156
self.expected_chips = 156
self.fan_count = 2
@@ -81,10 +69,7 @@ class M30SV60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S V60"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 164
self.fan_count = 2
@@ -93,7 +78,7 @@ class M30SV70(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S V70"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -105,10 +90,7 @@ class M30SV80(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S V80"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV80, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 129
self.fan_count = 2
@@ -117,7 +99,7 @@ class M30SVE10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VE10"
self.nominal_chips = 105
self.expected_chips = 105
self.fan_count = 2
@@ -126,7 +108,7 @@ class M30SVE20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VE20"
self.nominal_chips = 111
self.expected_chips = 111
self.fan_count = 2
@@ -135,10 +117,7 @@ class M30SVE30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VE30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 117
self.fan_count = 2
@@ -147,10 +126,7 @@ class M30SVE40(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VE40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 123
self.fan_count = 2
@@ -159,10 +135,7 @@ class M30SVE50(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VE50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 129
self.fan_count = 2
@@ -171,7 +144,7 @@ class M30SVE60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VE60"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -183,7 +156,7 @@ class M30SVE70(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VE70"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -195,10 +168,7 @@ class M30SVF10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VF10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVF10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 70
self.fan_count = 2
@@ -207,10 +177,7 @@ class M30SVF20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VF20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVF20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 74
self.fan_count = 2
@@ -219,10 +186,7 @@ class M30SVF30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VF30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVF30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 78
self.fan_count = 2
@@ -231,7 +195,7 @@ class M30SVG10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VG10"
self.nominal_chips = 66
self.expected_chips = 66
self.fan_count = 2
@@ -240,7 +204,7 @@ class M30SVG20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VG20"
self.nominal_chips = 70
self.expected_chips = 70
self.fan_count = 2
@@ -249,10 +213,7 @@ class M30SVG30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VG30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVG30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 74
self.fan_count = 2
@@ -261,10 +222,7 @@ class M30SVG40(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VG40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVG40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 78
self.fan_count = 2
@@ -273,10 +231,7 @@ class M30SVH10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VH10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 64
self.fan_count = 2
@@ -285,10 +240,7 @@ class M30SVH20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VH20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 66
self.fan_count = 2
@@ -297,7 +249,7 @@ class M30SVH30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VH30"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -309,10 +261,7 @@ class M30SVH40(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VH40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 64
self.fan_count = 2
@@ -321,10 +270,7 @@ class M30SVH50(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VH50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 66
self.fan_count = 2
@@ -333,7 +279,7 @@ class M30SVH60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VH60"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -345,8 +291,5 @@ class M30SVI20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S VI20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVI20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 70
self.fan_count = 2

View File

@@ -24,10 +24,7 @@ class M30SPlusV10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 215
self.fan_count = 2
@@ -36,10 +33,7 @@ class M30SPlusV20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 255
self.fan_count = 2
@@ -48,7 +42,7 @@ class M30SPlusV30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V30"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -60,10 +54,7 @@ class M30SPlusV40(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 235
self.fan_count = 2
@@ -72,10 +63,7 @@ class M30SPlusV50(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 225
self.fan_count = 2
@@ -84,10 +72,7 @@ class M30SPlusV60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V60"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 245
self.fan_count = 2
@@ -96,10 +81,7 @@ class M30SPlusV70(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V70"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 235
self.fan_count = 2
@@ -108,10 +90,7 @@ class M30SPlusV80(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V80"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V80, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 245
self.fan_count = 2
@@ -120,10 +99,7 @@ class M30SPlusV90(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V90"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V90, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 225
self.fan_count = 2
@@ -132,10 +108,7 @@ class M30SPlusV100(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ V100"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V100, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 215
self.fan_count = 2
@@ -144,10 +117,7 @@ class M30SPlusVE30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VE30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 148
self.fan_count = 2
@@ -156,7 +126,7 @@ class M30SPlusVE40(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VE40"
self.nominal_chips = 156
self.expected_chips = 156
self.fan_count = 2
@@ -165,7 +135,7 @@ class M30SPlusVE50(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VE50"
self.nominal_chips = 164
self.expected_chips = 164
self.fan_count = 2
@@ -174,10 +144,7 @@ class M30SPlusVE60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VE60"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 172
self.fan_count = 2
@@ -186,7 +153,7 @@ class M30SPlusVE70(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VE70"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -198,7 +165,7 @@ class M30SPlusVE80(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VE80"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE80, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -210,7 +177,7 @@ class M30SPlusVE90(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VE90"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE90, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -222,7 +189,7 @@ class M30SPlusVE100(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VE100"
self.nominal_chips = 0
self.expected_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE100, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
@@ -234,7 +201,7 @@ class M30SPlusVF20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VF20"
self.nominal_chips = 111
self.expected_chips = 111
self.fan_count = 2
@@ -243,22 +210,16 @@ class M30SPlusVF30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VF30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VF30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 117
self.fan_count = 2
class M36SPlusVG30(WhatsMiner): # noqa - ignore ABC method implementation
class M30SPlusVG20(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 = "M36S+ VG30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M36SPlusVG30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.model = "M30S+ VG20"
self.expected_chips = 82
self.fan_count = 2
@@ -267,10 +228,7 @@ class M30SPlusVG30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VG30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VG30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 78
self.fan_count = 2
@@ -279,7 +237,7 @@ class M30SPlusVG40(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VG40"
self.nominal_chips = 105
self.expected_chips = 105
self.fan_count = 2
@@ -288,10 +246,7 @@ class M30SPlusVG50(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VG50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VG50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 111
self.fan_count = 2
@@ -300,7 +255,7 @@ class M30SPlusVG60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VG60"
self.nominal_chips = 86
self.expected_chips = 86
self.fan_count = 2
@@ -309,10 +264,7 @@ class M30SPlusVH10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VH10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 64
self.fan_count = 2
@@ -321,10 +273,7 @@ class M30SPlusVH20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VH20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 66
self.fan_count = 2
@@ -333,7 +282,7 @@ class M30SPlusVH30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VH30"
self.nominal_chips = 70
self.expected_chips = 70
self.fan_count = 2
@@ -342,10 +291,7 @@ class M30SPlusVH40(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VH40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 74
self.fan_count = 2
@@ -354,10 +300,7 @@ class M30SPlusVH50(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VH50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.expected_chips = 64
self.fan_count = 2
@@ -366,5 +309,5 @@ class M30SPlusVH60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver)
self.ip = ip
self.model = "M30S+ VH60"
self.nominal_chips = 66
self.expected_chips = 66
self.fan_count = 2

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