Compare commits

..

259 Commits

Author SHA1 Message Date
UpstreamData
c4b4fa293d version: bump version number 2023-01-27 08:39:13 -07:00
UpstreamData
0204abfead bug: fix a bug with older M20 units not identifying version correctly. 2023-01-27 08:38:43 -07:00
Upstream Data
3510f7b9d3 bump version number 2023-01-26 22:24:44 -07:00
UpstreamData
2d4c063dfa Update get_data to us get_some_data sub functions. (#27) 2023-01-26 22:18:03 -07:00
Colin Crossman
67b3e2f312 version: bump version number 2022-12-19 08:42:56 -07:00
Arceris
82006de30f Merge dev branch into main (#25) 2022-12-19 08:35:29 -07:00
Arceris
5bde9d7fe6 Improvement to fault_light_on
Allow user to set a flash pattern, and if not, set a nice default flash pattern.
2022-12-18 16:30:06 -07:00
Arceris
f7cd428366 updates to set_led
Fix typo in param=auto, and set better raw defaults for flashing
2022-12-18 16:27:34 -07:00
Colin Crossman
d5e797de9e Squash Whatsminer LED bug
old implementation broke command syntax due to since required command parameters would be left off in certain cases.
2022-12-13 11:42:36 -07:00
UpstreamData
b9d5e7b206 version: bump version number. 2022-12-13 10:05:55 -07:00
UpstreamData
2d8c7eb4fd feature: add support for whatsminer M30S+ VG40 2022-12-13 10:05:07 -07:00
UpstreamData
f69e07fe68 version: bump version number. 2022-12-05 09:49:03 -07:00
UpstreamData
84aab38954 bug: properly shorten stratum URLs when gathering data from graphql. 2022-12-05 09:48:40 -07:00
UpstreamData
dcf37481bd version: bump version number. 2022-12-05 09:35:44 -07:00
UpstreamData
1a9cca84d5 bug: fix pool split not being found correctly with braiinsOS. 2022-12-05 09:34:43 -07:00
UpstreamData
c5272d67de version: bump version number 2022-12-03 14:20:57 -07:00
UpstreamData
3bcfb14177 feature: add support for Whatsminer M31SV20, and fix a bug with miner factory not identifying the miners properly by removing a V prefix. 2022-12-03 14:20:37 -07:00
UpstreamData
566280f280 docs: fix some missing data in the docs. 2022-12-02 16:10:30 -07:00
UpstreamData
a814f7eefb Update README.md 2022-12-02 16:06:22 -07:00
UpstreamData
097b8ed534 version: bump version number. 2022-12-02 15:57:52 -07:00
UpstreamData
da47d72749 feature: add wattage limit in get_config when getting config from whatsminers. 2022-12-02 15:57:31 -07:00
UpstreamData
abd4d18a01 feature: add whatsminer M31SV10 and V60. 2022-12-02 15:51:27 -07:00
UpstreamData
2adbce3c21 version: bump version number. 2022-12-02 09:26:53 -07:00
UpstreamData
c41324b324 bug: fix a bug with MinerAPI.commands causing an infinite recursion loop when checking a list of commands with get_commands, and ficx some weirdness where BTMiner doesnt return any data. 2022-12-02 09:26:17 -07:00
UpstreamData
151a4f6c2d version: bump version number. 2022-12-01 16:18:07 -07:00
UpstreamData
d23777a83f feature: Switch to using semaphores in miner network to rate limit as they are much more friendly. 2022-12-01 16:17:46 -07:00
UpstreamData
bb0aee8337 version: bump version number. 2022-12-01 15:54:09 -07:00
UpstreamData
2a23acb73a bug: fix BTMiner not responding to pause_mining and resume_mining causing an exception. 2022-12-01 15:53:47 -07:00
UpstreamData
ad5eb0cef6 bug: fix a bug with updated logging. 2022-12-01 15:40:40 -07:00
UpstreamData
07dd8f55fe format: improve logging. 2022-12-01 15:02:17 -07:00
UpstreamData
bafa91cc47 version: bump version number. 2022-11-30 10:48:20 -07:00
UpstreamData
16399510fa feature: add set_power_limit to all miners that support it. 2022-11-30 10:47:45 -07:00
UpstreamData
e9f54eec0a version: bump version number. 2022-11-30 10:01:27 -07:00
UpstreamData
fbbbc9f215 bug: Add VH60 to miner factory as it was missing. 2022-11-30 10:01:01 -07:00
UpstreamData
69e4f575c0 bug: fix a bug wth bosminer where it will sometimes not get data from graphql 2022-11-30 09:59:12 -07:00
UpstreamData
e95659d2e0 version: bump version number 2022-11-29 09:36:41 -07:00
UpstreamData
35f34310ec bug: fix an issue with incorrect type hinting. 2022-11-29 09:36:24 -07:00
UpstreamData
acc18e20fd Merge remote-tracking branch 'origin/master'
# Conflicts:
#	pyproject.toml
2022-11-24 11:04:19 -07:00
UpstreamData
72460eac08 version: bump version number 2022-11-24 11:03:54 -07:00
UpstreamData
3e5f9d4eca version: bump version number 2022-11-24 11:01:30 -07:00
UpstreamData
e873fa252c feature: add support for setting wattage on whatsminers with the new 2.0.5 API 2022-11-24 09:56:25 -07:00
UpstreamData
ff2c083a19 feature: add new whatsminer commands for version 2.0.5 2022-11-24 09:51:40 -07:00
UpstreamData
a30a84c34b version: bump version number 2022-11-22 18:28:02 -07:00
UpstreamData
97d2023298 feature: add support for whatsminer M31S V70 2022-11-22 18:27:35 -07:00
UpstreamData
1ce8430a14 version: bump version number 2022-11-21 11:28:29 -07:00
UpstreamData
1c0b638818 feature: add S19a Pro 2022-11-21 11:27:52 -07:00
UpstreamData
e852588eeb version: bump version number 2022-11-18 11:15:07 -07:00
UpstreamData
08b9bfd854 bug: fix error with getting miner failing to find a description key. 2022-11-18 11:14:35 -07:00
UpstreamData
7ee2f3a29a version: bump version number 2022-11-17 15:27:24 -07:00
UpstreamData
5ee6a38f39 bug: fix a bug with identifying some older version of BOSMiner. 2022-11-17 15:27:06 -07:00
UpstreamData
8f0bfd5f83 version: bump version number. 2022-11-16 14:30:52 -07:00
UpstreamData
c54f39fc77 bug: fix int and str addition in graphQL. 2022-11-16 14:30:29 -07:00
UpstreamData
eb40769ccf format: improve formatting 2022-11-15 11:28:35 -07:00
UpstreamData
418c62b40d version: bump version number 2022-11-14 13:50:28 -07:00
UpstreamData
198a480c35 bug: fix bug with read timeout not being caught when sending graphql query 2022-11-14 13:49:57 -07:00
UpstreamData
2d4bf4e847 version: bump version number 2022-11-14 11:20:54 -07:00
UpstreamData
774e3d1a62 bug: fix wattage not setting correctly for some BOS+ miners with issues. 2022-11-14 11:20:27 -07:00
UpstreamData
ade3cd6fee version: bump version number 2022-11-14 09:56:18 -07:00
UpstreamData
28dc3ccb84 version: bump version number 2022-11-14 09:51:34 -07:00
UpstreamData
6ca8582ec3 bug: improve robustness of identifying new versions of bosminer running BOSer. 2022-11-14 09:49:19 -07:00
UpstreamData
74b4aeb44a version: bump version number 2022-11-14 09:14:28 -07:00
UpstreamData
9c7ab5ac57 bug: fix support for new miners that return a BOSer string in version data. 2022-11-14 09:10:18 -07:00
Upstream Data
65ecf1fea2 version: bump version number 2022-11-13 19:36:25 -07:00
Upstream Data
44142c658b format: remove print statements 2022-11-13 19:36:07 -07:00
Upstream Data
25a205ce6c version: bump version number 2022-11-13 19:27:55 -07:00
Upstream Data
25094084cf bug: clarify uppercasing data from __get_model_from_graphql() 2022-11-13 19:27:32 -07:00
Upstream Data
4eac601153 version: bump version number 2022-11-13 19:17:43 -07:00
Upstream Data
f0d8d66b9b bug: fix not handling errors in send_graphql_query 2022-11-13 19:16:48 -07:00
Upstream Data
cfa550f8c0 version: bump version number 2022-11-13 19:07:56 -07:00
Arceris
91f6a5bf41 bug: catch TypeError on temps 2022-11-13 19:06:52 -07:00
Upstream Data
464bd6be65 version: bump version number. 2022-11-13 18:58:31 -07:00
Upstream Data
031d7e2186 bug: fix missing catching IndexError 2022-11-13 18:58:05 -07:00
Upstream Data
126b0d124c version: bump version number 2022-11-13 18:54:46 -07:00
Upstream Data
81c84a3e8f bug: fix a bug with no try except blocks on pools. 2022-11-13 18:54:26 -07:00
Upstream Data
406d5bd549 version: bump version number 2022-11-13 18:21:18 -07:00
Upstream Data
cd5fe09fd9 bug: fix wrong formatting for multiple error handling 2022-11-13 18:20:15 -07:00
Upstream Data
766fc4efed bug: fix check light not responding properly from graphQL. 2022-11-13 17:06:35 -07:00
Upstream Data
b70fed40c8 bug: add try except statements to get_data to fix bugs with getting data from graphql. 2022-11-13 17:03:41 -07:00
Upstream Data
255d98fd08 bug: fix some issues with validating data from graphql properly 2022-11-13 16:48:22 -07:00
Upstream Data
78304631f7 bug: fix errors not registering properly on braiins OS miners 2022-11-13 16:34:26 -07:00
Upstream Data
ab230844fc bug: fix an issue with chip count not receiving properly in some cases. 2022-11-13 16:31:41 -07:00
Upstream Data
8a58cb9fd3 format: remove extra print 2022-11-13 16:28:36 -07:00
Upstream Data
70b6ed73dc bug: fix manual return from __get_devdetails_and_version 2022-11-13 16:27:42 -07:00
Upstream Data
d2400bf44e bug: fix identifying stock as bosminer. 2022-11-13 16:22:57 -07:00
Upstream Data
db780fe876 version: bump version number 2022-11-13 15:14:16 -07:00
Upstream Data
cd84ae828a bug: fix a bug with some missing temperatures in graphql. 2022-11-13 15:13:56 -07:00
Upstream Data
970d5d1031 version: bump version number 2022-11-13 14:39:23 -07:00
Upstream Data
da0e327ec7 feature: add support for graphQL for BOS miners which may be having API issues, such as BBB. 2022-11-13 14:38:44 -07:00
Upstream Data
4c84a8d572 bug: ensure bosminer check_light backwards compatibility 2022-11-13 11:06:28 -07:00
Upstream Data
1cfd895deb feature: improve speed of bosminer check_light by 10x using graphql. 2022-11-13 11:01:22 -07:00
Upstream Data
d0da08eb10 version: bump version number 2022-11-12 14:51:30 -07:00
Upstream Data
ee4f2cd87d bug: fix win error check not being cross platform 2022-11-12 14:51:10 -07:00
UpstreamData
8a6577c8aa docs: Update README.md. 2022-11-10 14:20:41 -07:00
UpstreamData
ed5eae4187 format: add isort to dev requirements. 2022-11-10 14:17:31 -07:00
UpstreamData
f3b25027ad format: add isort to pre-commit and sort imports. 2022-11-10 14:15:45 -07:00
UpstreamData
228daecbbf version: bump version number 2022-11-08 15:09:32 -07:00
UpstreamData
81aa569d07 feature: rework whatsminer error codes to be able to allow an arbitrary slot number to be returned from the BTMiner API. 2022-11-08 15:08:47 -07:00
upstreamdata
aba8d7b8b9 bug: fix an issue with get data on unknown miners. 2022-11-08 13:38:19 -07:00
Upstream Data
449403bc57 version: bump version number. 2022-11-07 20:35:51 -07:00
Upstream Data
997a1bbe31 feature: add allow_warning to get_data to allow suppressing warnings. 2022-11-07 20:35:23 -07:00
Arceris
2c7e0e3efe Merge pull request #24 from UpstreamData/crc
Merge in branch refactoring the hashboard data
2022-11-07 19:25:06 -07:00
UpstreamData
6051a46654 docs: update docs to show cls.fields() 2022-11-07 12:54:40 -07:00
UpstreamData
c7847d2789 feature: add fields classmethod to all dataclasses to show what the fields are in each class. 2022-11-07 12:53:01 -07:00
UpstreamData
215af72a6b feature: add 0 as error_code for bos errors and X19 errors to make them consistent with the other types of errors. 2022-11-07 12:49:01 -07:00
UpstreamData
b33586f8eb feature: add fields() method to MinerData dataclass. 2022-11-07 10:51:34 -07:00
UpstreamData
8db81d9b3c format: remove prints. 2022-11-07 10:18:50 -07:00
UpstreamData
cf3b2fedf4 bug: fix a missed key step in btminer get errors. 2022-11-07 10:18:22 -07:00
UpstreamData
81db6d8e28 format: remove print in tunerstatus. 2022-11-07 10:11:08 -07:00
UpstreamData
d47e59d057 bug: fix a bug with older versions of BOSMiner having tuner status in a different place. 2022-11-07 10:09:56 -07:00
UpstreamData
8388d2f4ac bug: change MinerData().total_chips from reduce to sum, as reduce was causing issues when instantiating MinerData with a hashboard list that is empty. 2022-11-07 09:37:22 -07:00
UpstreamData
f5ec513ce0 bug: fix BMMiner warning when getting data from X19. 2022-11-07 09:07:32 -07:00
UpstreamData
8975842f0b bug: fix for some BMMiner models which report no boards causing an error when getting data. 2022-11-07 08:52:24 -07:00
Upstream Data
c6ca1df112 bug: fix some issues with get data on X19 from CGMiner on Vnish to BMMiner on stock. 2022-11-05 10:15:18 -06:00
Upstream Data
92f58a9682 bug: fix fan count and ideal chips for M34S+. 2022-11-05 10:04:17 -06:00
Upstream Data
5dc19dbd78 feature: add support for avalonminers using new hashboard model. 2022-11-05 10:03:02 -06:00
Upstream Data
b0fc11bcc1 bug: fix M34S+ not having ideal board count. 2022-11-05 09:50:03 -06:00
Upstream Data
23ebe460a4 format: Reformat some files. 2022-11-05 09:47:25 -06:00
Upstream Data
92db8c8161 format: Improve T9 formatting with get data on hiveon. 2022-11-05 09:44:38 -06:00
Upstream Data
d9b522734a format: reformat T3H+ get_data to improve error handling and checking of data. 2022-11-05 09:40:56 -06:00
Colin Crossman
e02457f0b2 bug: fix import for HashBoard 2022-11-04 21:55:44 -06:00
Colin Crossman
104b4c18c2 format: remove vestigial elements of btminer 2022-11-04 21:28:59 -06:00
Colin Crossman
1970f8c924 feature: add innosilicon support for new HashBoard format. 2022-11-04 21:28:33 -06:00
Colin Crossman
e431a06ac5 feature: add cgminer support for new HashBoard structure 2022-11-04 21:28:07 -06:00
Upstream Data
6da9b58088 bug: fix missing initialization variables on HashBoard when being called from within get_data on bos, T9, and btminer. 2022-11-04 20:26:28 -06:00
Upstream Data
3108443041 feature: add bosminer support for new HashBoard format. 2022-11-04 20:22:07 -06:00
Upstream Data
37ebb9e6c3 feature: add T9 support for new HashBoard format. 2022-11-04 20:10:50 -06:00
Upstream Data
6c2e0f59b1 feature: add the remainder of the backwards compatible MinerData board related parts that were lost in converting to HashBoard dataclass. 2022-11-04 20:00:03 -06:00
Upstream Data
6eb9cdce90 feature: add left_chips, center_chips, and right_chips to MinerData as properties that compute the correct board position from hashboards. 2022-11-04 19:55:13 -06:00
Upstream Data
e96d9447d1 format: reformat btminer.py 2022-11-04 19:44:49 -06:00
Upstream Data
d3cca11322 feature: add new HashBoard data structure to bmminer, and add ideal_hashboards to BaseMiner. 2022-11-04 19:44:19 -06:00
Colin Crossman
86155db455 Initial modifications to account for differential hashboard counts
Now storing the hasboard data as an array of datastructs. Not fully fleshed out yet, but this is where I'm going.
2022-11-04 15:48:30 -06:00
UpstreamData
5b078b4b27 feature: add support for M34S+ and M34S+ VE10 2022-11-04 13:12:28 -06:00
UpstreamData
9672dd6873 bug: fix warnings when new whatsminer API commands don't exist. 2022-11-02 10:32:33 -06:00
Upstream Data
1587f65196 version: bump version number 2022-11-01 22:52:56 -06:00
Upstream Data
5ff10c0cdd bug: fix the v2.0.4 whatsminer error codes bug to work with new versions where it was fixed. 2022-11-01 22:15:13 -06:00
UpstreamData
aa0a028564 Update .gitignore 2022-11-01 14:17:07 -06:00
UpstreamData
82f6d2f274 version: bump version number
resolves #22
2022-11-01 12:59:55 -06:00
UpstreamData
833de3ab43 feature: add support for whatsminer API v2.0.4, which moves the location of error codes to a new function (that happens to have very incorrect JSON) 2022-11-01 12:57:33 -06:00
UpstreamData
08d273c7c1 version: bump version number 2022-11-01 08:14:56 -06:00
UpstreamData
aac7598187 bug: Fix whatsminers not reporting error codes due to self.api.get_psu() causing a failed multicommand when used with older miners. 2022-11-01 08:14:06 -06:00
UpstreamData
5c0ac4e665 feature: added the ability to call btminer privileged commands as their own function, and abstracted out most of the complexity. 2022-10-31 09:46:28 -06:00
Upstream Data
7bd5e49412 bug: fix for btminer get_data() for cases where arbitrary amounts of errors with no error information are returned from the api to have them not be added to the error list. 2022-10-30 21:17:26 -06:00
Upstream Data
53ff3c5f79 bug: fix _load_api_data() raising an error when api data is an arbitrarily large size and overflows the receive buffer by parsing out the last incomplete item and repairing the json. 2022-10-30 21:14:14 -06:00
Upstream Data
36ae6e5272 docs: add M5X to nav directory 2022-10-21 18:43:13 -06:00
Upstream Data
fb3dffb216 docs: add relative paths to supported miner types to fix read the docs not going to the correct page. 2022-10-21 18:39:15 -06:00
Colin Crossman
ff6a6d2ec6 Add support for M50, forgot to update this file 2022-10-21 18:13:54 -06:00
Colin Crossman
f4bbc2c3e5 Add support for M50
Received M50 units, added support for them.
2022-10-21 18:12:05 -06:00
UpstreamData
4dbfdbe29c bump version number 2022-10-12 15:50:12 -06:00
UpstreamData
5e2a18f91e add fan_psu to MinerData, only works for whatsminers. 2022-10-12 15:42:27 -06:00
UpstreamData
3363bdc592 add MinerData().as_csv() to documentation. 2022-10-04 14:44:02 -06:00
UpstreamData
08180a2d59 bump version number 2022-10-04 08:30:26 -06:00
UpstreamData
1a64ff4038 add support for whatsminer in miner listener, and fix space in MinerData.as_csv() 2022-10-04 08:28:24 -06:00
UpstreamData
8ad90a6abb bump version number 2022-10-03 13:52:04 -06:00
UpstreamData
8cdd5ff015 improve MinerData().as_csv()` 2022-10-03 13:51:44 -06:00
UpstreamData
a08f434e1f bump version number 2022-10-03 13:43:08 -06:00
UpstreamData
9acd6d2fea add MinerData().as_csv() 2022-10-03 13:42:47 -06:00
UpstreamData
0a1cdea2e3 bump version number 2022-09-30 16:01:28 -06:00
UpstreamData
73b1a0493c Add the ability to add MinerData together and sum them, as well as compute division and floor division on them. 2022-09-30 16:00:45 -06:00
UpstreamData
d3a5517fa9 bump version number 2022-09-29 14:10:19 -06:00
UpstreamData
ad42251ee9 add S19 XP to docs 2022-09-29 14:09:26 -06:00
Arceris
0670938ed3 Add in S19 XP spec, and also make an adjustment to BMMiner so it won't fail when hashboards are missing (#20)
* Add in spec for S19 XP

* Adjust BMMiner to account for missing hashboards
2022-09-29 14:03:53 -06:00
UpstreamData
3e96889976 improve poetry setup instructions and improve documentation 2022-09-28 12:54:04 -06:00
UpstreamData
5a7b43ad74 Update README.md 2022-09-28 11:46:56 -06:00
UpstreamData
08c4863a2e improve poetry setup and install instructions 2022-09-28 11:39:25 -06:00
UpstreamData
4dbab75cf4 bump version number 2022-09-28 10:20:17 -06:00
UpstreamData
a90ad3ba6e fix incorrect chip count on M31S+ V30 2022-09-28 10:19:56 -06:00
UpstreamData
98a94ce4a6 bump version number 2022-09-28 10:06:53 -06:00
UpstreamData
f0a8b6e1c7 add support for whatsminer M31S+ V30 2022-09-28 10:05:28 -06:00
UpstreamData
e07bd3bffb bump version number 2022-09-26 13:08:27 -06:00
UpstreamData
dcce944390 Fix a bug where older version of bosminer return excessive hashboard error information 2022-09-26 12:27:37 -06:00
UpstreamData
03ecd118a3 add support for M31S+ V60 and V90 2022-09-26 11:51:47 -06:00
UpstreamData
97c0331762 bump version number 2022-09-26 11:33:42 -06:00
UpstreamData
eda9804dea add support for some new whatsminers types, M31S+ v40 and v80, and improve documentation of supported types. 2022-09-26 11:32:55 -06:00
UpstreamData
e94c81ce44 improve miner network functionality 2022-09-26 09:15:37 -06:00
UpstreamData
c95c58138e bump version number 2022-09-22 10:07:31 -06:00
UpstreamData
03c93b4de1 added pause_mining and resume_mining to all miners, added get_errors to whatsminers, and improved get_errors type hinting 2022-09-22 10:06:27 -06:00
UpstreamData
ff0d15c365 bump version number 2022-09-22 09:06:51 -06:00
UpstreamData
eadcb76d31 add stop_mining and resume_mining for X19 devices 2022-09-22 09:06:22 -06:00
UpstreamData
b7ce9288f8 bump version number 2022-09-13 09:53:03 -06:00
UpstreamData
e077a099d9 add global Innosilicon password option to settings 2022-09-13 09:52:33 -06:00
UpstreamData
8542acfb01 improve documentation 2022-09-13 09:11:15 -06:00
UpstreamData
0d80ce5a0e bump version number 2022-09-12 15:28:22 -06:00
UpstreamData
ddcafe0f2b finish abstracting BaseMiner by implementing get_data() as abstract 2022-09-12 15:27:51 -06:00
UpstreamData
ea195b34db update tests and add code coverage with coverage, although coverage is not required 2022-09-12 15:18:00 -06:00
UpstreamData
7377cb0d26 refactor some classes into their own files and fill base __init__.py with imports 2022-09-12 15:15:13 -06:00
UpstreamData
24b66de971 bump version number 2022-09-06 11:18:34 -06:00
UpstreamData
62d664a14c strip file output when checking for fault light in bosminer 2022-09-06 11:18:17 -06:00
UpstreamData
03b9a90f68 bump version number 2022-09-06 11:02:05 -06:00
UpstreamData
fefe0324b9 fix a bug with checking miner fault lights in bosminer 2022-09-06 11:01:42 -06:00
UpstreamData
62b14a78b7 bump version number 2022-09-06 10:49:16 -06:00
UpstreamData
0ff505bbb4 add support for innosilicon T3H+ 2022-09-06 10:48:04 -06:00
UpstreamData
b6c8c930a2 bump version number 2022-08-30 16:01:48 -06:00
UpstreamData
903bb93c4e add check_light() for bosminers by checking if delay_on exists in the Red LED directory 2022-08-30 10:51:22 -06:00
UpstreamData
59667cf104 bump version number 2022-08-29 09:53:01 -06:00
UpstreamData
3fd1b41bec add support for whatsminer VH60 2022-08-29 09:52:35 -06:00
UpstreamData
6569107f64 bump version number 2022-08-29 09:07:02 -06:00
UpstreamData
9d746a6dcb add errors to MinerData().as_influxdb() 2022-08-29 09:06:20 -06:00
UpstreamData
fce4c07c32 bump version number 2022-08-25 15:34:28 -06:00
UpstreamData
094857758a update MinerData().as_influxdb() to include properties. 2022-08-25 15:34:04 -06:00
UpstreamData
2a49b89849 bump version number 2022-08-25 13:09:19 -06:00
UpstreamData
4ecd135734 fix a bug with incorrect types in miner data 2022-08-25 13:09:03 -06:00
UpstreamData
836defc216 bump version number 2022-08-25 13:04:06 -06:00
UpstreamData
f8f777b5b5 fix tag data to be escaped properly 2022-08-25 13:03:46 -06:00
UpstreamData
b15e0a7363 bump version number 2022-08-25 12:50:55 -06:00
UpstreamData
5c1d06f743 attempt to fix a bug with influx db miner data 2022-08-25 12:50:30 -06:00
UpstreamData
51de56feb3 bump version number 2022-08-25 12:43:05 -06:00
UpstreamData
256a4ac909 fix boolean bug in miner data 2022-08-25 12:39:57 -06:00
UpstreamData
09800c8ad2 bump version number 2022-08-24 15:18:04 -06:00
UpstreamData
83a7d8c60f add MinerData().as_json() 2022-08-24 15:17:36 -06:00
UpstreamData
ee2698be50 update poetry.lock 2022-08-24 13:56:05 -06:00
UpstreamData
dc43087b0d bump version number 2022-08-24 13:51:25 -06:00
UpstreamData
4fa3511725 add float support to MinerData().as_influx() 2022-08-24 13:50:55 -06:00
UpstreamData
4b9ae70424 bump version number 2022-08-24 13:35:48 -06:00
UpstreamData
74ebffb4fc add MinerData().as_influx() to write miner data as influx db line format data. 2022-08-24 13:34:57 -06:00
UpstreamData
54206da449 add getitem and setitem methods to MinerData 2022-08-24 10:02:51 -06:00
UpstreamData
dd54ff7ee4 bump version number 2022-08-22 15:19:08 -06:00
UpstreamData
abef0c3d59 add efficiency to MinerData 2022-08-22 14:50:49 -06:00
UpstreamData
957c9a3678 Refactor MinerFactory._get_miner_type(), move BaseMiner to its own file, and improve interface of miner.send_config() (#17) 2022-08-22 14:10:37 -06:00
Arceris
50ccfec1b3 Add a check in _parse_type_from_version (#16) 2022-08-18 10:12:19 -06:00
UpstreamData
8e7d6751e2 update docs to include all currently supported miners including BOS devices. 2022-08-12 12:30:29 -06:00
UpstreamData
b77c4173c6 update supported miners in docs and add link to it in README.md 2022-08-12 12:22:49 -06:00
UpstreamData
4941cffb70 fix a bad character in requirements.txt caused by pre-commit 2022-08-12 12:14:12 -06:00
UpstreamData
81d5d23189 Revert "Revert "attempt to improve the readability of miner_factory.py""
This reverts commit 9da5a836ce.
2022-08-11 15:22:00 -06:00
UpstreamData
9da5a836ce Revert "attempt to improve the readability of miner_factory.py"
This reverts commit c9a536fc60.
2022-08-11 15:16:42 -06:00
UpstreamData
c9a536fc60 attempt to improve the readability of miner_factory.py 2022-08-11 15:12:45 -06:00
UpstreamData
fa172b56b0 bump version number 2022-08-11 15:11:09 -06:00
UpstreamData
ee45f2342e improve how the fault light looks on whatsminers 2022-08-11 11:58:42 -06:00
UpstreamData
1f59ef025d bump version number 2022-08-10 16:26:24 -06:00
UpstreamData
d6a153144f remove print statement from btminer configuration 2022-08-10 16:25:54 -06:00
UpstreamData
99001e2e13 added the ability to configure whatsminer via API 2022-08-10 16:21:47 -06:00
UpstreamData
92b847656e add light functions for btminer, and add a way to reset to admin password for btminers to allow unlocking of priviledged API. 2022-08-10 15:31:42 -06:00
UpstreamData
a41525e828 bump version number 2022-08-10 11:18:44 -06:00
UpstreamData
5e9588cc56 add M32V20 2022-08-10 11:17:12 -06:00
UpstreamData
b8239703c1 move M32 to separate file. 2022-08-10 11:14:06 -06:00
Colin Crossman
5d49135b59 Add hooks for M32 (not S) 2022-08-10 11:06:54 -06:00
UpstreamData
3a5a76080b add pre-commit hooks 2022-08-10 09:57:31 -06:00
UpstreamData
f23e10d629 add better hiveon support and improve T9 functionality. 2022-08-10 09:04:01 -06:00
UpstreamData
b7d4891140 bump version number 2022-08-09 11:12:10 -06:00
UpstreamData
5f5cbd9060 add support for setting X19 web passwords and X17 web passwords. 2022-08-09 11:04:08 -06:00
UpstreamData
8379359caf update documentation and make BaseMiner and BaseMinerAPI unable to be instantiated directly. Add more unittests for miners. 2022-08-08 13:19:59 -06:00
UpstreamData
62238192ce bump version number 2022-08-05 16:34:00 -06:00
UpstreamData
1997003643 fix a bug with whatsminer crashing if hitting a S19 condition 2022-08-05 16:33:40 -06:00
UpstreamData
3a81844898 bump version number 2022-08-05 12:12:25 -06:00
UpstreamData
0ac80fb205 fix a bug with vnish miner identification 2022-08-05 12:12:10 -06:00
UpstreamData
9494018c12 bump version number 2022-08-05 12:08:05 -06:00
UpstreamData
0bc86c98c5 add support for some X19 models running vnish to be able to get miner type from them 2022-08-05 12:07:33 -06:00
UpstreamData
f0d69c9ca7 bump version number 2022-08-05 10:23:22 -06:00
UpstreamData
b81590bd2e add support for X19 miner errors codes shown on their dashboard 2022-08-05 10:23:03 -06:00
UpstreamData
a53e01df6f bump version number 2022-08-02 08:19:03 -06:00
UpstreamData
f63e063954 fix a bug with not capitalizing BITMAIN for a model check 2022-08-02 08:18:46 -06:00
upstreamdata
9cbaf7076a bump version number 2022-07-28 12:29:24 -06:00
upstreamdata
daa5ac5870 fixed a bug with capitalization of "Pro" in antminer models 2022-07-28 12:28:55 -06:00
upstreamdata
0b8c08016b bump version number. 2022-07-27 23:48:42 -06:00
upstreamdata
8c768d351b fix a bug with braiinsOS+ miners which return with a capital letter in their model instead of a lowercase letter. 2022-07-27 23:44:19 -06:00
UpstreamData
c9e7fa2629 bump version number 2022-07-22 13:05:13 -06:00
UpstreamData
9d3f2b5968 add support for M20 versions and update docs 2022-07-22 13:04:47 -06:00
UpstreamData
283e3d5e11 bump version number 2022-07-21 08:43:30 -06:00
UpstreamData
add4b575c2 update shields and improve typing and handling of fault light checks 2022-07-21 08:42:35 -06:00
UpstreamData
af2f1e9ad5 misc docs changes 2022-07-20 14:54:59 -06:00
205 changed files with 8800 additions and 3635 deletions

17
.coveragerc Normal file
View File

@@ -0,0 +1,17 @@
[report]
exclude_lines =
# Skip @abstractmethod
@abstractmethod
@abc.abstractmethod
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
# Don't complain about missing debug-only code:
def __repr__
if self\.debug
# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:

View File

@@ -19,4 +19,4 @@ jobs:
- name: Build using poetry and publish to PyPi
uses: JRubics/poetry-publish@v1.11
with:
pypi_token: ${{ secrets.PYPI_API_KEY }}
pypi_token: ${{ secrets.PYPI_API_KEY }}

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ pyvenv.cfg
.env/
bin/
lib/
.idea/

27
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,27 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 22.6.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: isort (python)
- repo: local
hooks:
- id: unittest
name: unittest
entry: python -m unittest discover
language: system
'types': [python]
args: ["-p '*test.py'"] # Probably this option is absolutely not needed.
pass_filenames: false
stages: [commit]

115
README.md
View File

@@ -5,104 +5,82 @@
[![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)
![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)
## Documentation
Documentation is located on Read the Docs as [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/).
## Usage
Supported miners are listed in the docs, [here](https://pyasic.readthedocs.io/en/latest/miners/supported_types/).
### Standard Usage
You can install pyasic directly from pip with the command `pip install pyasic`
## Installation
You can install pyasic directly from pip with the command `pip install pyasic`.
For those of you who aren't comfortable with code and developer tools, there are windows builds of GUI applications that use this library here -> (https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing)
For those of you who aren't comfortable with code and developer tools, there are windows builds of GUI applications that use this library [here](https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing).
### Developers
To use this repo, first download it, create a virtual environment, enter the virtual environment, and install relevant packages by navigating to this directory and running ```pip install -r requirements.txt``` on Windows or ```pip3 install -r requirements.txt``` on Mac or UNIX if the first command fails.
## 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.
You can also use poetry by initializing and running ```poetry install```
### Interfacing with miners programmatically
##### Note: If you are trying to interface with Whatsminers, there is a bug in the way they are interacted with on Windows, so to fix that you need to change the event loop policy using this code:
```python
# need to import these 2 libraries, you need asyncio anyway so make sure you have sys imported
import sys
import asyncio
# if the computer is windows, set the event loop policy to a WindowsSelector policy
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
```
##### It is likely a good idea to use this code in your program anyway to be preventative.
<br>
To write your own custom programs with this repo, you have many options.
This repo uses poetry for dependencies, which can be installed by following the guide on their website [here](https://python-poetry.org/docs/#installation).
It is recommended that you explore the files in this repo to familiarize yourself with them, try starting with the miners module and going from there.
After you have poetry installed, run `poetry install --with dev`, or `poetry install --with dev,docs` if you want to include packages required for documentation.
There are 2 main ways to get a miner and it's functions via scanning or via the MinerFactory.
Finally, initialize pre-commit hooks with `poetry run pre-commit install`.
### Documentation Testing
Testing the documentation can be done by running `poetry run mkdocs serve`, whcih will serve the documentation locally on port 8000.
## Interfacing with miners programmatically
There are 2 main ways to get a miner (and the functions attached to it), via scanning or via the `MinerFactory()`.
#### Scanning for miners
```python
import asyncio
import sys
from pyasic.network import MinerNetwork
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# define asynchronous function to scan for miners
async def scan_and_get_data():
# Define network range to be used for scanning
# This can take a list of IPs, a constructor string, or an IP and subnet mask
# The standard mask is /24, and you can pass any IP address in the subnet
# 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()
# 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)
if __name__ == "__main__":
asyncio.run(scan_and_get_data())
```
</br>
#### Getting a miner if you know the IP
```python
import asyncio
import sys
from pyasic.miners.miner_factory import MinerFactory
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
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 MinerFactory().get_miner(miner_ip)
miner = await get_miner(miner_ip)
# Get data from the miner
data = await miner.get_data()
print(data)
@@ -115,27 +93,26 @@ if __name__ == "__main__":
If needed, this library exposes a wrapper for the miner API that can be used for advanced data gathering.
You can see more information on basic usage of the APIs past this example in the docs [here](https://pyasic.readthedocs.io/en/latest/API/api/).
Please see the appropriate API documentation page (pyasic docs -> Advanced -> Miner APIs -> your API type) for a link to that specific miner's API documentation page and more information.
#### List available API commands
```python
import asyncio
import sys
from pyasic.miners.miner_factory import MinerFactory
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
from pyasic import get_miner
async def get_api_commands(miner_ip: str):
# Get the miner
miner = await MinerFactory().get_miner(miner_ip)
miner = await get_miner(miner_ip)
# List all available commands
print(miner.api.get_commands())
# Can also be called explicitly with the function miner.api.get_commands()
print(miner.api.commands)
if __name__ == "__main__":
asyncio.run(get_api_commands("192.168.1.69"))
```
@@ -146,26 +123,20 @@ The miner API commands will raise an `APIError` if they fail with a bad status c
```python
import asyncio
import sys
from pyasic.miners.miner_factory import MinerFactory
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
from pyasic import get_miner
async def get_api_commands(miner_ip: str):
# Get the miner
miner = await MinerFactory().get_miner(miner_ip)
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"))
```

View File

@@ -2,11 +2,12 @@
## Miner APIs
Each miner has a unique API that is used to communicate with it.
Each of these API types has commands that differ between them, and some commands have data that others do not.
Each miner that is a subclass of `BaseMiner` should have an API linked to it as `Miner.api`.
Each miner that is a subclass of [`BaseMiner`][pyasic.miners.BaseMiner] should have an API linked to it as `Miner.api`.
All API implementations inherit from [`BaseMinerAPI`][pyasic.API.BaseMinerAPI], which implements the basic communications protocols.
BaseMinerAPI should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] cannot be instantiated directly, it will raise a `TypeError`.
Use these instead -
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]

33
docs/data/error_codes.md Normal file
View File

@@ -0,0 +1,33 @@
# pyasic
<br>
## Whatsminer Error Codes
::: pyasic.data.error_codes.WhatsminerError
handler: python
options:
show_root_heading: false
heading_level: 4
<br>
## Braiins OS Error Codes
::: pyasic.data.error_codes.BraiinsOSError
handler: python
options:
show_root_heading: false
heading_level: 4
<br>
## X19 Error Codes
::: pyasic.data.error_codes.X19Error
handler: python
options:
show_root_heading: false
heading_level: 4
<br>
## Innosilicon Error Codes
::: pyasic.data.error_codes.InnosiliconError
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -5,6 +5,8 @@
[![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)
## Intro
Welcome to pyasic! Pyasic uses an asynchronous method of communicating with asic miners on your network, which makes it super fast.
@@ -27,13 +29,13 @@ async def scan_miners(): # define async scan function to allow awaiting
# create a miner network
# you can pass in any IP and it will use that in a subnet with a /24 mask (255 IPs).
network = MinerNetwork("192.168.1.50") # this uses the 192.168.1.0-255 network
# scan for miners asynchronously
# this will return the correct type of miners if they are supported with all functionality.
miners = await network.scan_network_for_miners()
print(miners)
if __name__ == "__main__":
if __name__ == "__main__":
asyncio.run(scan_miners()) # run the scan asynchronously with asyncio.run()
```
@@ -54,8 +56,8 @@ async def get_miners(): # define async scan function to allow awaiting
miner_1 = await MinerFactory().get_miner("192.168.1.75")
miner_2 = await MinerFactory().get_miner("192.168.1.76")
print(miner_1, miner_2)
if __name__ == "__main__":
if __name__ == "__main__":
asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run()
```
@@ -76,7 +78,7 @@ async def gather_miner_data():
print(miner_data) # all data from the dataclass
print(miner_data.hashrate) # hashrate of the miner in TH/s
if __name__ == "__main__":
if __name__ == "__main__":
asyncio.run(gather_miner_data())
```
@@ -89,13 +91,184 @@ from pyasic.network import MinerNetwork # miner network handles the scanning
async def gather_miner_data(): # define async scan function to allow awaiting
network = MinerNetwork("192.168.1.50")
miners = await network.scan_network_for_miners()
# we need to asyncio.gather() all the miners get_data() functions to make them run together
all_miner_data = await asyncio.gather(*[miner.get_data() for miner in miners])
for miner_data in all_miner_data:
print(miner_data) # print out all the data one by one
if __name__ == "__main__":
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].
These functions are
[`check_light`](#check-light),
[`fault_light_off`](#fault-light-off),
[`fault_light_on`](#fault-light-on),
[`get_config`](#get-config),
[`get_data`](#get-data),
[`get_errors`](#get-errors),
[`get_hostname`](#get-hostname),
[`get_model`](#get-model),
[`reboot`](#reboot),
[`restart_backend`](#restart-backend),
[`stop_mining`](#stop-mining),
[`resume_mining`](#resume-mining),
[`send_config`](#send-config), and
[`set_power_limit`](#set-power-limit).
<br>
### Check Light
::: pyasic.miners.BaseMiner.check_light
handler: python
options:
heading_level: 4
<br>
### Fault Light Off
::: pyasic.miners.BaseMiner.fault_light_off
handler: python
options:
heading_level: 4
<br>
### Fault Light On
::: pyasic.miners.BaseMiner.fault_light_on
handler: python
options:
heading_level: 4
<br>
### Get Config
::: pyasic.miners.BaseMiner.get_config
handler: python
options:
heading_level: 4
<br>
### Get Data
::: pyasic.miners.BaseMiner.get_data
handler: python
options:
heading_level: 4
<br>
### Get Errors
::: pyasic.miners.BaseMiner.get_errors
handler: python
options:
heading_level: 4
<br>
### Get Hostname
::: pyasic.miners.BaseMiner.get_hostname
handler: python
options:
heading_level: 4
<br>
### Get Model
::: pyasic.miners.BaseMiner.get_model
handler: python
options:
heading_level: 4
<br>
### Reboot
::: pyasic.miners.BaseMiner.reboot
handler: python
options:
heading_level: 4
<br>
### Restart Backend
::: pyasic.miners.BaseMiner.restart_backend
handler: python
options:
heading_level: 4
<br>
### Stop Mining
::: pyasic.miners.BaseMiner.stop_mining
handler: python
options:
heading_level: 4
<br>
### Resume Mining
::: pyasic.miners.BaseMiner.resume_mining
handler: python
options:
heading_level: 4
<br>
### Send Config
::: pyasic.miners.BaseMiner.send_config
handler: python
options:
heading_level: 4
<br>
### Set Power Limit
::: pyasic.miners.BaseMiner.set_power_limit
handler: python
options:
heading_level: 4
<br>
## [`MinerConfig`][pyasic.config.MinerConfig] and [`MinerData`][pyasic.data.MinerData]
Pyasic implements a few dataclasses as helpers to make data return types consistent across different miners and miner APIs. The different fields of these dataclasses can all be viewed with the classmethod `cls.fields()`.
<br>
### [`MinerData`][pyasic.data.MinerData]
[`MinerData`][pyasic.data.MinerData] is a return from the [`get_data()`](#get-data) function, and is used to have a consistent dataset across all returns.
You can call [`MinerData.asdict()`][pyasic.data.MinerData.asdict] to get the dataclass as a dictionary, and there are many other helper functions contained in the class to convert to different data formats.
[`MinerData`][pyasic.data.MinerData] instances can also be added to each other to combine their data and can be divided by a number to divide all their data, allowing you to get average data from many miners by doing -
```python
from pyasic import MinerData
# examples of miner data
d1 = MinerData("192.168.1.1")
d2 = MinerData("192.168.1.2")
list_of_miner_data = [d1, d2]
average_data = sum(list_of_miner_data, start=MinerData("0.0.0.0"))/len(list_of_miner_data)
```
<br>
### [`MinerConfig`][pyasic.config.MinerConfig]
[`MinerConfig`][pyasic.config.MinerConfig] is pyasic's way to represent a configuration file from a miner.
It is the return from [`get_config()`](#get-config).
Each miner has a unique way to convert the [`MinerConfig`][pyasic.config.MinerConfig] to their specific type, there are helper functions in the class.
In most cases these helper functions should not be used, as [`send_config()`](#send-config) takes a [`MinerConfig`][pyasic.config.MinerConfig] and will do the conversion to the right type for you.

View File

@@ -57,3 +57,61 @@
options:
show_root_heading: false
heading_level: 4
## S17 (BOS)
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17
handler: python
options:
show_root_heading: false
heading_level: 4
## S17+ (BOS)
::: pyasic.miners.antminer.bosminer.X17.S17_Plus.BOSMinerS17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## S17 Pro (BOS)
::: pyasic.miners.antminer.bosminer.X17.S17_Pro.BOSMinerS17Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S17e (BOS)
::: pyasic.miners.antminer.bosminer.X17.S17e.BOSMinerS17e
handler: python
options:
show_root_heading: false
heading_level: 4
## T17 (BOS)
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17
handler: python
options:
show_root_heading: false
heading_level: 4
## T17+ (BOS)
::: pyasic.miners.antminer.bosminer.X17.T17_Plus.BOSMinerT17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## T17e (BOS)
::: pyasic.miners.antminer.bosminer.X17.T17e.BOSMinerT17e
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -43,6 +43,14 @@
show_root_heading: false
heading_level: 4
## S19 XP
::: pyasic.miners.antminer.bmminer.X19.S19_XP.BMMinerS19XP
handler: python
options:
show_root_heading: false
heading_level: 4
## T19
::: pyasic.miners.antminer.bmminer.X19.T19.BMMinerT19
@@ -50,3 +58,45 @@
options:
show_root_heading: false
heading_level: 4
## S19 (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19_Pro.BOSMinerS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19j.BOSMinerS19j
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19j_Pro.BOSMinerS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## T19 (BOS)
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,6 +1,16 @@
# pyasic
## X9 Models
## X9 (BOS)
::: pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9
handler: python
options:
show_root_heading: false
heading_level: 4
## S9
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9

10
docs/miners/base_miner.md Normal file
View File

@@ -0,0 +1,10 @@
# pyasic
## Base Miner
[`BaseMiner`][pyasic.miners.BaseMiner] is the basis for all miner classes, they all subclass (usually indirectly) from this class.
You may not instantiate this class on its own, only subclass from it. Trying to instantiate an instance of this class will raise `TypeError`.
::: pyasic.miners.BaseMiner
handler: python
options:
heading_level: 4

View File

@@ -0,0 +1,10 @@
# pyasic
## T3X Models
## T3H+
::: pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -6,3 +6,15 @@
options:
show_root_heading: false
heading_level: 4
<br>
## AnyMiner
::: pyasic.miners.miner_factory.AnyMiner
handler: python
options:
show_root_heading: false
heading_level: 4
[`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] is a placeholder type variable used for typing returns of functions.
A function returning [`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] will always return a subclass of [`BaseMiner`][pyasic.miners.BaseMiner],
and is used to specify a function returning some arbitrary type of miner class instance.

View File

@@ -3,71 +3,246 @@
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
## Miner List
##### pyasic currently supports the following miners and subtypes:
* Braiins OS+ Devices:
* All devices supported by BraiinsOS+ are supported here.
* Stock Firmware Whatsminers:
* M3X Series:
* [M30S][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30S]:
* [VE10][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVE10]
* [VG20][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVG20]
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVE20]
* [V50][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SV50]
* [M30S+][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlus]:
* [VF20][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVF20]
* [VE40][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVE40]
* [VG60][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVG60]
* [M30S++][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlus]:
* [VG30][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30]
* [VG40][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40]
* [M31S][pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S]
* [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]:
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20]
* [M32S][pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S]
* M2X Series:
* [M20S][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S]:
* [V10][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV10]
* [V20][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV20]
* [M20S+][pyasic.miners.whatsminer.btminer.M2X.M20S_Plus.BTMinerM20SPlus]
* [M21][pyasic.miners.whatsminer.btminer.M2X.M21.BTMinerM21]
* [M21S][pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21S]:
* [V20][pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV20]
* [V60][pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV60]
* [M21S+][pyasic.miners.whatsminer.btminer.M2X.M21S_Plus.BTMinerM21SPlus]
* Stock Firmware Antminers:
* X19 Series:
* [S19][pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19]
* [S19 Pro][pyasic.miners.antminer.bmminer.X19.S19_Pro.BMMinerS19Pro]
* [S19a][pyasic.miners.antminer.bmminer.X19.S19a.BMMinerS19a]
* [S19j][pyasic.miners.antminer.bmminer.X19.S19j.BMMinerS19j]
* [S19j Pro][pyasic.miners.antminer.bmminer.X19.S19j_Pro.BMMinerS19jPro]
* [T19][pyasic.miners.antminer.bmminer.X19.T19.BMMinerT19]
* X17 Series:
* [S17][pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17]
* [S17+][pyasic.miners.antminer.bmminer.X17.S17_Plus.BMMinerS17Plus]
* [S17 Pro][pyasic.miners.antminer.bmminer.X17.S17_Pro.BMMinerS17Pro]
* [S17e][pyasic.miners.antminer.bmminer.X17.S17e.BMMinerS17e]
* [T17][pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17]
* [T17+][pyasic.miners.antminer.bmminer.X17.T17_Plus.BMMinerT17Plus]
* [T17e][pyasic.miners.antminer.bmminer.X17.T17e.BMMinerT17e]
* X9 Series:
* [S9][pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9]
* [S9i][pyasic.miners.antminer.bmminer.X9.S9i.BMMinerS9i]
* [T9][pyasic.miners.antminer.bmminer.X9.T9.BMMinerT9]
* Stock Firmware Avalonminers:
* A7X Series:
* [A721][pyasic.miners.avalonminer.cgminer.A7X.A721.CGMinerAvalon721]
* [A741][pyasic.miners.avalonminer.cgminer.A7X.A741.CGMinerAvalon741]
* [A761][pyasic.miners.avalonminer.cgminer.A7X.A761.CGMinerAvalon761]
* A8X Series:
* [A821][pyasic.miners.avalonminer.cgminer.A8X.A821.CGMinerAvalon821]
* [A841][pyasic.miners.avalonminer.cgminer.A8X.A841.CGMinerAvalon841]
* [A851][pyasic.miners.avalonminer.cgminer.A8X.A851.CGMinerAvalon851]
* A9X Series:
* [A921][pyasic.miners.avalonminer.cgminer.A9X.A921.CGMinerAvalon921]
* A10X Series:
* [A1026][pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026]
* [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047]
* [A1066][pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066]
<style>
details {
margin:0px;
padding-top:0px;
padding-bottom:0px;
}
</style>
<details style="margin:0px; padding-top:0px; padding-bottom:0px;">
<summary>Braiins OS+ Devices:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-bos">S19</a></li>
<li><a href="../antminer/X19#s19-pro-bos">S19 Pro</a></li>
<li><a href="../antminer/X19#s19j-bos">S19j</a></li>
<li><a href="../antminer/X19#s19j-pro-bos">S19j Pro</a></li>
<li><a href="../antminer/X19#t19-bos">T19</a></li>
</ul>
</details>
<details>
<summary>X17 Series:</summary>
<ul>
<li><a href="../antminer/X17#s17-bos">S17</a></li>
<li><a href="../antminer/X17#s17-plus-bos">S17+</a></li>
<li><a href="../antminer/X17#s17-pro-bos">S17 Pro</a></li>
<li><a href="../antminer/X17#s17e-bos">S17e</a></li>
<li><a href="../antminer/X17#t17-bos">T17</a></li>
<li><a href="../antminer/X17#t17-plus-bos">T17+</a></li>
<li><a href="../antminer/X17#t17e-bos">T17e</a></li>
</ul>
</details>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9#s9-bos">S9</a></li>
<li><a href="../antminer/X9#s9-bos">S9i</a></li>
<li><a href="../antminer/X9#s9-bos">S9j</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Whatsminers:</summary>
<ul>
<details>
<summary>M5X Series:</summary>
<ul>
<details>
<summary><a href="../whatsminer/M5X/#m50">M50</a></summary>
<ul>
<li><a href="../whatsminer/M5X/#m50vh50">VH50</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>M3X Series:</summary>
<ul>
<details>
<summary><a href="../whatsminer/M3X/#m30s">M30S</a></summary>
<ul>
<li><a href="../whatsminer/M3X/#m30sve10">VE10</a></li>
<li><a href="../whatsminer/M3X/#m30svg10">VG10</a></li>
<li><a href="../whatsminer/M3X/#m30svg20">VG20</a></li>
<li><a href="../whatsminer/M3X/#m30sve20">VE20</a></li>
<li><a href="../whatsminer/M3X/#m30sv50">V50</a></li>
</ul>
</details>
<details>
<summary><a href="../whatsminer/M3X/#m30s_1">M30S+</a></summary>
<ul>
<li><a href="../whatsminer/M3X/#m30svf20">VF20</a></li>
<li><a href="../whatsminer/M3X/#m30svh30">VH30</a></li>
<li><a href="../whatsminer/M3X/#m30sve40">VE40</a></li>
<li><a href="../whatsminer/M3X/#m30svg40">VG40</a></li>
<li><a href="../whatsminer/M3X/#m30svg60">VG60</a></li>
<li><a href="../whatsminer/M3X/#m30svh60">VH60</a></li>
</ul>
</details>
<details>
<summary><a href="../whatsminer/M3X/#m30s_2">M30S++</a></summary>
<ul>
<li><a href="../whatsminer/M3X/#m30svg30">VG30</a></li>
<li><a href="../whatsminer/M3X/#m30svg40">VG40</a></li>
<li><a href="../whatsminer/M3X/#m30svh60">VH60</a></li>
</ul>
</details>
<details>
<summary><a href="../whatsminer/M3X/#m31s">M31S</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv10">M31SV10</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv20">M31SV20</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv60">M31SV60</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv70">M31SV70</a></summary>
</details>
<details>
<summary><a href="../whatsminer/M3X/#m31s_1">M31S+</a></summary>
<ul>
<li><a href="../whatsminer/M3X/#m31sve20">VE20</a></li>
<li><a href="../whatsminer/M3X/#m31sv30">V30</a></li>
<li><a href="../whatsminer/M3X/#m31sv40">V40</a></li>
<li><a href="../whatsminer/M3X/#m31sv60">V60</a></li>
<li><a href="../whatsminer/M3X/#m31sv80">V80</a></li>
<li><a href="../whatsminer/M3X/#m31sv90">V90</a></li>
</ul>
</details>
<details>
<summary><a href="../whatsminer/M3X/#m32">M32</a></summary>
<ul>
<li><a href="../whatsminer/M3X/#m32v20">V20</a></li>
</ul>
</details>
<details>
<summary><a href="../whatsminer/M3X/#m32s">M32S</a></summary>
</details>
<details>
<summary><a href="../whatsminer/M3X/#m34s">M34S+</a></summary>
<ul>
<li><a href="../whatsminer/M3X/#m34sve10">VE10</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>M2X Series:</summary>
<ul>
<details>
<summary><a href="../whatsminer/M2X/#m20">M20</a></summary>
<ul>
<li><a href="../whatsminer/M2X/#m20v10">V10</a></li>
</ul>
</details>
<details>
<summary><a href="../whatsminer/M2X/#m20s">M20S</a></summary>
<ul>
<li><a href="../whatsminer/M2X/#m20sv10">V10</a></li>
<li><a href="../whatsminer/M2X/#m20sv20">V20</a></li>
</ul>
</details>
<details>
<summary><a href="../whatsminer/M2X/#m20s_1">M20S+</a></summary>
</details>
<details>
<summary><a href="../whatsminer/M2X/#m21">M21</a></summary>
</details>
<details>
<summary><a href="../whatsminer/M2X/#m21s">M21S</a></summary>
<ul>
<li><a href="../whatsminer/M2X/#m21sv20">V20</a></li>
<li><a href="../whatsminer/M2X/#m21sv60">V60</a></li>
</ul>
</details>
<details>
<summary><a href="../whatsminer/M2X/#m21s_1">M21S+</a></summary>
</details>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Antminers:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19/#s19">S19</a></li>
<li><a href="../antminer/X19/#s19-pro">S19 Pro</a></li>
<li><a href="../antminer/X19/#s19a">S19a</a></li>
<li><a href="../antminer/X19/#s19j">S19j</a></li>
<li><a href="../antminer/X19/#s19j-pro">S19j Pro</a></li>
<li><a href="../antminer/X19/#s19-xp">S19 XP</a></li>
<li><a href="../antminer/X19/#t19">T19</a></li>
</ul>
</details>
<details>
<summary>X17 Series:</summary>
<ul>
<li><a href="../antminer/X17/#s17">S17</a></li>
<li><a href="../antminer/X17/#s17_1">S17+</a></li>
<li><a href="../antminer/X17/#s17-pro">S17 Pro</a></li>
<li><a href="../antminer/X17/#s17e">S17e</a></li>
<li><a href="../antminer/X17/#t17">T17</a></li>
<li><a href="../antminer/X17/#t17_1">T17+</a></li>
<li><a href="../antminer/X17/#t17e">T17e</a></li>
</ul>
</details>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9/#s9">S9</a></li>
<li><a href="../antminer/X9/#s9i">S9i</a></li>
<li><a href="../antminer/X9/#t9">T9</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Avalonminers:</summary>
<ul>
<details>
<summary>A7X Series:</summary>
<ul>
<li><a href="../avalonminer/A7X/#a721">A721</a></li>
<li><a href="../avalonminer/A7X/#a741">A741</a></li>
<li><a href="../avalonminer/A7X/#a761">A761</a></li>
</ul>
</details>
<details>
<summary>A8X Series:</summary>
<ul>
<li><a href="../avalonminer/A8X/#a821">A821</a></li>
<li><a href="../avalonminer/A8X/#a841">A841</a></li>
<li><a href="../avalonminer/A8X/#a851">A851</a></li>
</ul>
</details>
<details>
<summary>A9X Series:</summary>
<ul>
<li><a href="../avalonminer/A9X/#a921">A921</a></li>
</ul>
</details>
<details>
<summary>A10X Series:</summary>
<ul>
<li><a href="../avalonminer/A10X/#a1026">A1026</a></li>
<li><a href="../avalonminer/A10X/#a1047">A1047</a></li>
<li><a href="../avalonminer/A10X/#a1066">A1066</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Innosilicon Miners:</summary>
<ul>
<details>
<summary>T3X Series:</summary>
<ul>
<li><a href="../innosilicon/T3X/#t3h">T3H+</a></li>
</ul>
</details>
</ul>
</details>

View File

@@ -1,6 +1,22 @@
# pyasic
## M2X Models
## M20
::: pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20
handler: python
options:
show_root_heading: false
heading_level: 4
## M20V10
::: pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20V10
handler: python
options:
show_root_heading: false
heading_level: 4
## M20S
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S

View File

@@ -17,6 +17,14 @@
show_root_heading: false
heading_level: 4
## M30SVG10
::: pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVG10
handler: python
options:
show_root_heading: false
heading_level: 4
## M30SVG20
::: pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVG20
@@ -57,6 +65,14 @@
show_root_heading: false
heading_level: 4
## M30S+VH30
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVH30
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S+VE40
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVE40
@@ -65,6 +81,14 @@
show_root_heading: false
heading_level: 4
## M30S+VG40
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVG40
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S+VG60
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVG60
@@ -73,6 +97,14 @@
show_root_heading: false
heading_level: 4
## M30S+VH60
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVH60
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S++
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlus
@@ -89,7 +121,7 @@
show_root_heading: false
heading_level: 4
## M30S+VG40
## M30S++VG40
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
handler: python
@@ -97,6 +129,14 @@
show_root_heading: false
heading_level: 4
## M30S++VH60
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVH60
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S
@@ -106,6 +146,38 @@
show_root_heading: false
heading_level: 4
## M31SV10
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M31SV20
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV20
handler: python
options:
show_root_heading: false
heading_level: 4
## M31SV60
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV60
handler: python
options:
show_root_heading: false
heading_level: 4
## M31SV70
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV70
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus
@@ -122,6 +194,62 @@
show_root_heading: false
heading_level: 4
## M31S+V30
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV30
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+V40
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV40
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+V60
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV60
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+V80
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV80
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+V90
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV90
handler: python
options:
show_root_heading: false
heading_level: 4
## M32
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32
handler: python
options:
show_root_heading: false
heading_level: 4
## M32V20
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20
handler: python
options:
show_root_heading: false
heading_level: 4
## M32S
::: pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S
@@ -129,3 +257,19 @@
options:
show_root_heading: false
heading_level: 4
## M34S+
::: pyasic.miners.whatsminer.btminer.M3X.M34S_Plus.BTMinerM34SPlus
handler: python
options:
show_root_heading: false
heading_level: 4
## M34S+VE10
::: pyasic.miners.whatsminer.btminer.M3X.M34S_Plus.BTMinerM34SPlusVE10
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,18 @@
# pyasic
## M5X Models
## M50
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50
handler: python
options:
show_root_heading: false
heading_level: 4
## M50VH50
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH50
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,7 +1,7 @@
# pyasic
## Miner Network Range
`MinerNetworkRange` is a class used by [`MinerNetwork`][pyasic.network.MinerNetwork] to handle any constructor stings.
[`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.

View File

@@ -11,7 +11,6 @@ nav:
- BTMiner: "miners/backends/btminer.md"
- CGMiner: "miners/backends/cgminer.md"
- Hiveon: "miners/backends/hiveon.md"
- Classes:
- Antminer X9: "miners/antminer/X9.md"
- Antminer X17: "miners/antminer/X17.md"
@@ -22,23 +21,27 @@ nav:
- Avalon 10X: "miners/avalonminer/A10X.md"
- Whatsminer M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md"
- Whatsminer M5X: "miners/whatsminer/M5X.md"
- Innosilicon T3X: "miners/innosilicon/T3X.md"
- Network:
- Miner Network: "network/miner_network.md"
- Miner Network Range: "network/miner_network_range.md"
- Data:
- Dataclasses:
- Miner Data: "data/miner_data.md"
- Config:
- Error Codes: "data/error_codes.md"
- Miner Config: "config/miner_config.md"
- Advanced:
- Miner APIs:
- Base: "API/api.md"
- Intro: "API/api.md"
- BMMiner: "API/bmminer.md"
- BOSMiner: "API/bosminer.md"
- BTMiner: "API/btminer.md"
- CGMiner: "API/cgminer.md"
- Unknown: "API/unknown.md"
- Base Miner: "miners/base_miner.md"
plugins:
- mkdocstrings
- search

653
poetry.lock generated
View File

@@ -11,13 +11,13 @@ idna = ">=2.8"
sniffio = ">=1.1"
[package.extras]
doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
trio = ["trio (>=0.16)"]
[[package]]
name = "asyncssh"
version = "2.11.0"
version = "2.12.0"
description = "AsyncSSH: Asynchronous SSHv2 client and server library"
category = "main"
optional = false
@@ -38,7 +38,7 @@ pywin32 = ["pywin32 (>=227)"]
[[package]]
name = "certifi"
version = "2022.6.15"
version = "2022.9.24"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
@@ -55,9 +55,36 @@ python-versions = "*"
[package.dependencies]
pycparser = "*"
[[package]]
name = "cfgv"
version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.6.1"
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "cryptography"
version = "37.0.4"
version = "38.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
@@ -68,11 +95,56 @@ cffi = ">=1.12"
[package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools_rust (>=0.11.4)"]
sdist = ["setuptools-rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
[[package]]
name = "distlib"
version = "0.3.6"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "filelock"
version = "3.8.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]
[[package]]
name = "ghp-import"
version = "2.1.0"
description = "Copy your docs directly to the gh-pages branch."
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
python-dateutil = ">=2.8.1"
[package.extras]
dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
name = "griffe"
version = "0.22.2"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
async = ["aiofiles (>=0.7,<1.0)"]
[[package]]
name = "h11"
@@ -115,19 +187,181 @@ rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
sniffio = "*"
[package.extras]
brotli = ["brotlicffi", "brotli"]
cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"]
brotli = ["brotli", "brotlicffi"]
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "identify"
version = "2.5.5"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
license = ["ukkonen"]
[[package]]
name = "idna"
version = "3.3"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "importlib-metadata"
version = "4.12.0"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
perf = ["ipython"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
[[package]]
name = "Jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "Markdown"
version = "3.3.7"
description = "Python implementation of Markdown."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
[package.extras]
testing = ["coverage", "pyyaml"]
[[package]]
name = "MarkupSafe"
version = "2.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mkdocs"
version = "1.4.0"
description = "Project documentation with Markdown."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
click = ">=7.0"
ghp-import = ">=1.0"
importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
Jinja2 = ">=2.11.1"
Markdown = ">=3.2.1,<3.4"
mergedeep = ">=1.3.4"
packaging = ">=20.5"
PyYAML = ">=5.1"
pyyaml-env-tag = ">=0.1"
watchdog = ">=2.0"
[package.extras]
i18n = ["babel (>=2.9.0)"]
[[package]]
name = "mkdocs-autorefs"
version = "0.4.1"
description = "Automatically link across pages in MkDocs."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
Markdown = ">=3.3"
mkdocs = ">=1.1"
[[package]]
name = "mkdocstrings"
version = "0.19.0"
description = "Automatic documentation from sources, for MkDocs."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
Jinja2 = ">=2.11.1"
Markdown = ">=3.3"
MarkupSafe = ">=1.1"
mkdocs = ">=1.2"
mkdocs-autorefs = ">=0.3.1"
mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
pymdown-extensions = ">=6.3"
[package.extras]
crystal = ["mkdocstrings-crystal (>=0.3.4)"]
python = ["mkdocstrings-python (>=0.5.2)"]
python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
[[package]]
name = "mkdocstrings-python"
version = "0.7.1"
description = "A Python handler for mkdocstrings."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
griffe = ">=0.11.1"
mkdocstrings = ">=0.19"
[[package]]
name = "nodeenv"
version = "1.7.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
[package.dependencies]
setuptools = "*"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "passlib"
version = "1.7.4"
@@ -139,9 +373,37 @@ python-versions = "*"
[package.extras]
argon2 = ["argon2-cffi (>=18.2.0)"]
bcrypt = ["bcrypt (>=3.1.0)"]
build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"]
build_docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"]
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
name = "pre-commit"
version = "2.20.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
toml = "*"
virtualenv = ">=20.0.8"
[[package]]
name = "pyaml"
version = "21.10.1"
@@ -162,13 +424,57 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyyaml"
name = "pymdown-extensions"
version = "9.6"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
markdown = ">=3.2"
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
six = ">=1.5"
[[package]]
name = "PyYAML"
version = "6.0"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "pyyaml_env_tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyyaml = "*"
[[package]]
name = "rfc3986"
version = "1.5.0"
@@ -183,13 +489,34 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
[package.extras]
idna2008 = ["idna"]
[[package]]
name = "setuptools"
version = "65.4.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "sniffio"
version = "1.2.0"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
category = "main"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.7"
[[package]]
name = "toml"
@@ -207,10 +534,50 @@ category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "virtualenv"
version = "20.16.5"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
distlib = ">=0.3.5,<1"
filelock = ">=3.4.1,<4"
platformdirs = ">=2.4,<3"
[package.extras]
docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"]
testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
[[package]]
name = "watchdog"
version = "2.1.9"
description = "Filesystem events monitoring"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "zipp"
version = "3.8.1"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "8d93eafd928d7fed4b0a00d13e46982c2d4310c37acb2faec7e7a477b3f35e9c"
content-hash = "98c3026a6f27c29c0357bbfa07166d6d8b604a869f3a802adc3bb3610f86964c"
[metadata.files]
anyio = [
@@ -218,12 +585,12 @@ anyio = [
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
]
asyncssh = [
{file = "asyncssh-2.11.0-py3-none-any.whl", hash = "sha256:7302348cbd54c58d3259da17f13e77912de1b005e366b15c8b183d948c8a91a8"},
{file = "asyncssh-2.11.0.tar.gz", hash = "sha256:59c36ce77ba9dda8dd57ad875776e7105ddb1fa851bc039bb3aeadeac4f67b56"},
{file = "asyncssh-2.12.0-py3-none-any.whl", hash = "sha256:6841c4242c606fd51188c974ec2f4887efeec67ecdfa5b84140711dacd985ab3"},
{file = "asyncssh-2.12.0.tar.gz", hash = "sha256:274101322c4b941823aeed8e1ab6e7be5191686c6db2d2bd35afeba30505e780"},
]
certifi = [
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
]
cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
@@ -291,29 +658,61 @@ cffi = [
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
]
cfgv = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
cryptography = [
{file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"},
{file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"},
{file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"},
{file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"},
{file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"},
{file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"},
{file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"},
{file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"},
{file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"},
{file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"},
{file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"},
{file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"},
{file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"},
{file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"},
{file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"},
{file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"},
{file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"},
{file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"},
{file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"},
{file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"},
{file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"},
{file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"},
{file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"},
{file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"},
{file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"},
{file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"},
{file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"},
]
distlib = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
filelock = [
{file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
{file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
]
ghp-import = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
]
griffe = [
{file = "griffe-0.22.2-py3-none-any.whl", hash = "sha256:cea5415ac6a92f4a22638e3f1f2e661402bac09fb8e8266936d67185a7e0d0fb"},
{file = "griffe-0.22.2.tar.gz", hash = "sha256:1408e336a4155392bbd81eed9f2f44bf144e71b9c664e905630affe83bbc088e"},
]
h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
@@ -327,14 +726,108 @@ httpx = [
{file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"},
{file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"},
]
identify = [
{file = "identify-2.5.5-py2.py3-none-any.whl", hash = "sha256:ef78c0d96098a3b5fe7720be4a97e73f439af7cf088ebf47b620aeaa10fadf97"},
{file = "identify-2.5.5.tar.gz", hash = "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
importlib-metadata = [
{file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
{file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
]
Jinja2 = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
Markdown = [
{file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"},
{file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"},
]
MarkupSafe = [
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
]
mergedeep = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
mkdocs = [
{file = "mkdocs-1.4.0-py3-none-any.whl", hash = "sha256:ce057e9992f017b8e1496b591b6c242cbd34c2d406e2f9af6a19b97dd6248faa"},
{file = "mkdocs-1.4.0.tar.gz", hash = "sha256:e5549a22d59e7cb230d6a791edd2c3d06690908454c0af82edc31b35d57e3069"},
]
mkdocs-autorefs = [
{file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
]
mkdocstrings = [
{file = "mkdocstrings-0.19.0-py3-none-any.whl", hash = "sha256:3217d510d385c961f69385a670b2677e68e07b5fea4a504d86bf54c006c87c7d"},
{file = "mkdocstrings-0.19.0.tar.gz", hash = "sha256:efa34a67bad11229d532d89f6836a8a215937548623b64f3698a1df62e01cc3e"},
]
mkdocstrings-python = [
{file = "mkdocstrings-python-0.7.1.tar.gz", hash = "sha256:c334b382dca202dfa37071c182418a6df5818356a95d54362a2b24822ca3af71"},
{file = "mkdocstrings_python-0.7.1-py3-none-any.whl", hash = "sha256:a22060bfa374697678e9af4e62b020d990dad2711c98f7a9fac5c0345bef93c7"},
]
nodeenv = [
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
passlib = [
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
]
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pre-commit = [
{file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
{file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
]
pyaml = [
{file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"},
{file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"},
@@ -343,7 +836,19 @@ pycparser = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
pyyaml = [
pymdown-extensions = [
{file = "pymdown_extensions-9.6-py3-none-any.whl", hash = "sha256:1e36490adc7bfcef1fdb21bb0306e93af99cff8ec2db199bd17e3bf009768c11"},
{file = "pymdown_extensions-9.6.tar.gz", hash = "sha256:b956b806439bbff10f726103a941266beb03fbe99f897c7d5e774d7170339ad9"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
PyYAML = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
@@ -351,6 +856,13 @@ pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
@@ -378,13 +890,25 @@ pyyaml = [
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
pyyaml_env_tag = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
rfc3986 = [
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
setuptools = [
{file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"},
{file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
sniffio = [
{file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
{file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
@@ -394,3 +918,38 @@ typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
]
virtualenv = [
{file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"},
{file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"},
]
watchdog = [
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"},
{file = "watchdog-2.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"},
{file = "watchdog-2.1.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591"},
{file = "watchdog-2.1.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33"},
{file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846"},
{file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3"},
{file = "watchdog-2.1.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654"},
{file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39"},
{file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7"},
{file = "watchdog-2.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd"},
{file = "watchdog-2.1.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3"},
{file = "watchdog-2.1.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d"},
{file = "watchdog-2.1.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_armv7l.whl", hash = "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_i686.whl", hash = "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64.whl", hash = "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6"},
{file = "watchdog-2.1.9-py3-none-win32.whl", hash = "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1"},
{file = "watchdog-2.1.9-py3-none-win_amd64.whl", hash = "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c"},
{file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"},
{file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"},
]
zipp = [
{file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"},
{file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"},
]

View File

@@ -13,39 +13,14 @@
# limitations under the License.
import asyncio
import json
import ipaddress
import warnings
import json
import logging
import re
import warnings
from typing import Union
class APIError(Exception):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Incorrect API parameters."
class APIWarning(Warning):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Incorrect API parameters."
from pyasic.errors import APIError, APIWarning
class BaseMinerAPI:
@@ -55,6 +30,89 @@ class BaseMinerAPI:
# ip address of the miner
self.ip = ipaddress.ip_address(ip)
def __new__(cls, *args, **kwargs):
if cls is BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls)
def __repr__(self):
return f"{self.__class__.__name__}: {str(self.ip)}"
async def send_command(
self,
command: Union[str, bytes],
parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
allow_warning: bool = True,
) -> dict:
"""Send an API command to the miner and return the result.
Parameters:
command: The command to sent to the miner.
parameters: Any additional parameters to be sent with the command.
ignore_errors: Whether to raise APIError when the command returns an error.
allow_warning: Whether to warn if the command fails.
Returns:
The return data from the API command parsed from JSON into a dict.
"""
logging.debug(
f"{self} - (Send Privileged Command) - {command} "
+ f"with args {parameters}"
if parameters
else ""
)
# create the command
cmd = {"command": command}
if parameters:
cmd["parameter"] = parameters
# send the command
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
data = self._load_api_data(data)
# check for if the user wants to allow errors to return
if not ignore_errors:
# validate the command succeeded
validation = self._validate_command_output(data)
if not validation[0]:
if allow_warning:
logging.warning(
f"{self.ip}: API Command Error: {command}: {validation[1]}"
)
raise APIError(validation[1])
logging.debug(f"{self} - (Send Command) - Received data.")
return data
# Privileged command handler, only used by whatsminers, defined here for consistency.
async def send_privileged_command(self, *args, **kwargs) -> dict:
return await self.send_command(*args, **kwargs)
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner.
Parameters:
*commands: The commands to send as a multicommand to the miner.
allow_warning: A boolean to supress APIWarnings.
"""
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# standard format doesn't work for X19
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
return {command: [{}] for command in commands}
logging.debug(f"{self} - (Multicommand) - Received data")
return data
@property
def commands(self) -> list:
return self.get_commands()
def get_commands(self) -> list:
"""Get a list of command accessible to a specific type of API on the miner.
@@ -66,9 +124,10 @@ class BaseMinerAPI:
for func in
# each function in self
dir(self)
if not func == "commands"
if callable(getattr(self, func)) and
# no __ methods
not func.startswith("__") and
# no __ or _ methods
not func.startswith("__") and not func.startswith("_") and
# remove all functions that are in this base class
func
not in [
@@ -92,108 +151,50 @@ If you are sure you want to use this command please use API.send_command("{comma
)
return return_commands
async def multicommand(
self, *commands: str, ignore_x19_error: bool = False
) -> dict:
"""Creates and sends multiple commands as one command to the miner.
Parameters:
*commands: The commands to send as a multicommand to the miner.
ignore_x19_error: Whether or not to ignore errors raised by x19 miners when using the "+" delimited style.
"""
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesnt work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, x19_command=ignore_x19_error)
except APIError:
logging.debug(f"{self.ip}: Handling X19 multicommand.")
data = await self._x19_multicommand(*command.split("+"))
logging.debug(f"{self.ip}: Received multicommand data.")
return data
async def _x19_multicommand(self, *commands):
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, x19_command=True))
except APIError as e:
raise APIError(e)
except Exception as e:
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
return data
async def send_command(
self,
command: Union[str, bytes],
parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
x19_command: bool = False,
) -> dict:
"""Send an API command to the miner and return the result.
Parameters:
command: The command to sent to the miner.
parameters: Any additional parameters to be sent with the command.
ignore_errors: Whether or not to raise APIError when the command returns an error.
x19_command: Whether this is a command for an x19 that may be an issue (such as a "+" delimited multicommand)
Returns:
The return data from the API command parsed from JSON into a dict.
"""
async def _send_bytes(self, data: bytes, timeout: int = 100) -> bytes:
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
# handle OSError 121
except OSError as e:
if e.winerror == "121":
logging.warning("Semaphore Timeout has Expired.")
return {}
# create the command
cmd = {"command": command}
if parameters:
cmd["parameter"] = parameters
if getattr(e, "winerror") == "121":
logging.warning(
f"{self} - ([Hidden] Send Bytes) - Semaphore timeout expired."
)
return b"{}"
# send the command
writer.write(json.dumps(cmd).encode("utf-8"))
logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing")
writer.write(data)
logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
await writer.drain()
# instantiate data
data = b""
ret_data = b""
# loop to receive all the data
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
try:
while True:
d = await reader.read(4096)
if not d:
break
data += d
try:
d = await asyncio.wait_for(reader.read(4096), timeout=timeout)
if not d:
break
ret_data += d
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except Exception as e:
logging.warning(f"{self.ip}: API Command Error: - {e}")
data = self._load_api_data(data)
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
# close the connection
logging.debug(f"{self} - ([Hidden] Send Bytes) - Closing")
writer.close()
await writer.wait_closed()
# 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 x19_command:
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
raise APIError(validation[1])
return data
return ret_data
@staticmethod
def _validate_command_output(data: dict) -> tuple:
@@ -224,32 +225,41 @@ If you are sure you want to use this command please use API.send_command("{comma
@staticmethod
def _load_api_data(data: bytes) -> dict:
str_data = None
# some json from the API returns with a null byte (\x00) on the end
if data.endswith(b"\x00"):
# handle the null byte
str_data = data.decode("utf-8")[:-1]
else:
# no null byte
str_data = data.decode("utf-8")
# fix an error with a btminer return having an extra comma that breaks json.loads()
str_data = str_data.replace(",}", "}")
# fix an error with a btminer return having a newline that breaks json.loads()
str_data = str_data.replace("\n", "")
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
str_data = str_data.replace("}{", "},{")
# fix an error with a bmminer return having a specific comma that breaks json.loads()
str_data = str_data.replace("[,{", "[{")
# fix an error with Avalonminers returning inf and nan
str_data = str_data.replace("info", "1nfo")
str_data = str_data.replace("inf", "0")
str_data = str_data.replace("1nfo", "info")
str_data = str_data.replace("nan", "0")
# fix whatever this garbage from avalonminers is `,"id":1}`
if str_data.startswith(","):
str_data = f"{{{str_data[1:]}"
# try to fix an error with overflowing the receive buffer
# this can happen in cases such as bugged btminers returning arbitrary length error info with 100s of errors.
if not str_data.endswith("}"):
str_data = ",".join(str_data.split(",")[:-1]) + "}"
# fix a really nasty bug with whatsminer API v2.0.4 where they return a list structured like a dict
if re.search(r"\"error_code\":\[\".+\"\]", str_data):
str_data = str_data.replace("[", "{").replace("]", "}")
# parse the json
try:
# some json from the API returns with a null byte (\x00) on the end
if data.endswith(b"\x00"):
# handle the null byte
str_data = data.decode("utf-8")[:-1]
else:
# no null byte
str_data = data.decode("utf-8")
# fix an error with a btminer return having an extra comma that breaks json.loads()
str_data = str_data.replace(",}", "}")
# fix an error with a btminer return having a newline that breaks json.loads()
str_data = str_data.replace("\n", "")
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
str_data = str_data.replace("}{", "},{")
# fix an error with a bmminer return having a specific comma that breaks json.loads()
str_data = str_data.replace("[,{", "[{")
# fix an error with Avalonminers returning inf and nan
str_data = str_data.replace("inf", "0")
str_data = str_data.replace("nan", "0")
# fix whatever this garbage from avalonminers is `,"id":1}`
if str_data.startswith(","):
str_data = f"{{{str_data[1:]}"
# parse the json
parsed_data = json.loads(str_data)
# handle bad json
except json.decoder.JSONDecodeError as e:
raise APIError(f"Decode Error {e}: {str_data}")
return parsed_data

View File

@@ -11,8 +11,9 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from pyasic.API import BaseMinerAPI
from pyasic.API import APIError, BaseMinerAPI
class BMMinerAPI(BaseMinerAPI):
@@ -33,8 +34,42 @@ class BMMinerAPI(BaseMinerAPI):
port: The port to reference the API on. Default is 4028.
"""
def __init__(self, ip: str, port: int = 4028) -> None:
super().__init__(ip, port)
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028) -> None:
super().__init__(ip, port=port)
self.api_ver = api_ver
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesn't work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
data = await self._x19_multicommand(
*command.split("+"), allow_warning=allow_warning
)
return data
async def _x19_multicommand(self, *commands, allow_warning: bool = True):
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(
await self.send_command(cmd, allow_warning=allow_warning)
)
except APIError as e:
raise APIError(e)
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
)
return data
async def version(self) -> dict:
"""Get miner version info.

View File

@@ -33,8 +33,9 @@ class BOSMinerAPI(BaseMinerAPI):
port: The port to reference the API on. Default is 4028.
"""
def __init__(self, ip: str, port: int = 4028):
super().__init__(ip, port)
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028) -> None:
super().__init__(ip, port=port)
self.api_ver = api_ver
async def asccount(self) -> dict:
"""Get data on the number of ASC devices and their info.
@@ -166,7 +167,7 @@ class BOSMinerAPI(BaseMinerAPI):
return await self.send_command("estats")
async def check(self, command: str) -> dict:
"""Check if the command command exists in BOSMiner.
"""Check if the command `command` exists in BOSMiner.
<details>
<summary>Expand</summary>

View File

@@ -13,26 +13,30 @@
# limitations under the License.
import asyncio
import re
import json
import hashlib
import binascii
import base64
import binascii
import hashlib
import json
import logging
import re
from typing import Union
import datetime
from passlib.handlers.md5_crypt import md5_crypt
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from passlib.handlers.md5_crypt import md5_crypt
from pyasic.API import BaseMinerAPI, APIError
from pyasic.API import BaseMinerAPI
from pyasic.errors import APIError
from pyasic.settings import PyasicSettings
from pyasic.misc import api_min_version
### IMPORTANT ###
# you need to change the password of the miners using the Whatsminer
# tool, then you can set them back to admin with this tool, but they
# must be changed to something else and set back to admin with this
# or the privileged API will not work using admin as the password. If
# you change the password, you can pass that to the this class as pwd,
# 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.
@@ -81,7 +85,7 @@ def _add_to_16(string: str) -> bytes:
def parse_btminer_priviledge_data(token_data: dict, data: dict):
"""Parses data returned from the BTMiner privileged API.
Parses data from the BTMiner privileged API using the the token
Parses data from the BTMiner privileged API using the token
from the API in an AES format.
Parameters:
@@ -123,6 +127,7 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
Returns:
The encrypted privileged command to be sent to the miner.
"""
logging.debug(f"(Create Prilileged Command) - Creating Privileged Command")
# add token to command
command["token"] = token_data["host_sign"]
# encode host_passwd data and get hexdigest
@@ -178,64 +183,87 @@ class BTMinerAPI(BaseMinerAPI):
def __init__(
self,
ip: str,
api_ver: str = "0.0.0",
port: int = 4028,
pwd: str = PyasicSettings().global_whatsminer_password,
):
super().__init__(ip, port)
self.admin_pwd = pwd
self.pwd = pwd
self.current_token = None
self.api_ver = api_ver
async def send_command(
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner.
Parameters:
*commands: The commands to send as a multicommand to the miner.
allow_warning: A boolean to supress APIWarnings.
"""
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# commands starting with "get_" aren't supported, but we can fake that
get_commands_data = {}
for command in list(commands):
if command.startswith("get_"):
commands.remove(command)
# send seperately and append later
try:
get_commands_data[command] = [
await self.send_command(command, allow_warning=allow_warning)
]
except APIError:
get_commands_data[command] = [{}]
command = "+".join(commands)
try:
main_data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
main_data = {command: [{}] for command in commands}
logging.debug(f"{self} - (Multicommand) - Received data")
data = dict(**main_data, **get_commands_data)
return data
async def send_privileged_command(
self,
command: str or bytes,
parameters: str or int or bool = None,
command: Union[str, bytes],
ignore_errors: bool = False,
timeout: int = 10,
**kwargs,
) -> dict:
# check if command is a string
# if its bytes its encoded and needs to be sent raw
if isinstance(command, str):
# if it is a string, put it into the standard command format
command = json.dumps({"command": command}).encode("utf-8")
logging.debug(
f"{self} - (Send Privileged Command) - {command} " + f"with args {kwargs}"
if len(kwargs) > 0
else ""
)
command = {"cmd": command, **kwargs}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
logging.debug(f"{self} - (Send Privileged Command) - Sending")
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
# handle OSError 121
except OSError as e:
if e.winerror == "121":
print("Semaphore Timeout has Expired.")
return {}
# send the command
writer.write(command)
await writer.drain()
# instantiate data
data = b""
# loop to receive all the data
try:
while True:
d = await reader.read(4096)
if not d:
break
data += d
except Exception as e:
logging.info(f"{str(self.ip)}: {e}")
data = await self._send_bytes(enc_command, timeout)
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
if command["cmd"] in ["reboot", "restart"]:
logging.info(
f"{self} - (reboot/restart) - Whatsminers currently break this. "
f"Ignoring exception. Command probably worked."
)
# FAKING IT HERE
data = b'{"STATUS": "S", "When": 1670966423, "Code": 131, "Msg": "API command OK", "Description": "Reboot"}'
else:
raise APIError("No data was returned from the API.")
if not data:
raise APIError("No data was returned from the API.")
data = self._load_api_data(data)
# close the connection
writer.close()
await writer.wait_closed()
# check if the returned data is encoded
if "enc" in data.keys():
# try to parse the encoded data
try:
data = parse_btminer_priviledge_data(self.current_token, data)
except Exception as e:
logging.info(f"{str(self.ip)}: {e}")
try:
data = parse_btminer_priviledge_data(self.current_token, data)
except Exception as e:
logging.info(f"{str(self.ip)}: {e}")
if not ignore_errors:
# if it fails to validate, it is likely an error
@@ -255,11 +283,18 @@ class BTMinerAPI(BaseMinerAPI):
An encoded token and md5 password, which are used for the privileged API.
</details>
"""
logging.debug(f"{self} - (Get Token) - Getting token")
if self.current_token:
if self.current_token[
"timestamp"
] > datetime.datetime.now() - datetime.timedelta(minutes=30):
return self.current_token
# get the token
data = await self.send_command("get_token")
# encrypt the admin password with the salt
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + "$")
pwd = _crypt(self.pwd, "$1$" + data["Msg"]["salt"] + "$")
pwd = pwd.split("$")
# take the 4th item from the pwd split
@@ -276,7 +311,11 @@ class BTMinerAPI(BaseMinerAPI):
self.current_token = {
"host_sign": host_sign,
"host_passwd_md5": host_passwd_md5,
"timestamp": datetime.datetime.now(),
}
logging.debug(
f"{self} - (Get Token) - Gathered token data: {self.current_token}"
)
return self.current_token
#### PRIVILEGED COMMANDS ####
@@ -318,46 +357,18 @@ class BTMinerAPI(BaseMinerAPI):
A dict from the API to confirm the pools were updated.
</details>
"""
# get the token and password from the miner
token_data = await self.get_token()
# parse pool data
if not pool_1:
raise APIError("No pools set.")
elif pool_2 and pool_3:
command = {
"cmd": "update_pools",
"pool1": pool_1,
"worker1": worker_1,
"passwd1": passwd_1,
"pool2": pool_2,
"worker2": worker_2,
"passwd2": passwd_2,
"pool3": pool_3,
"worker3": worker_3,
"passwd3": passwd_3,
}
elif pool_2:
command = {
"cmd": "update_pools",
"pool1": pool_1,
"worker1": worker_1,
"passwd1": passwd_1,
"pool2": pool_2,
"worker2": worker_2,
"passwd2": passwd_2,
}
else:
command = {
"cmd": "update_pools",
"pool1": pool_1,
"worker1": worker_1,
"passwd1": passwd_1,
}
# encode the command with the token data
enc_command = create_privileged_cmd(token_data, command)
# send the command
return await self.send_command(enc_command)
return await self.send_privileged_command(
"update_pools",
pool1=pool_1,
worker1=worker_1,
passwd1=passwd_1,
pool2=pool_2,
worker2=worker_2,
passwd2=passwd_2,
pool3=pool_3,
worker3=worker_3,
passwd3=passwd_3,
)
async def restart(self) -> dict:
"""Restart BTMiner using the API.
@@ -371,10 +382,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the restart.
</details>
"""
command = {"cmd": "restart_btminer"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("restart_btminer")
async def power_off(self, respbefore: bool = True) -> dict:
"""Power off the miner using the API.
@@ -391,12 +399,8 @@ class BTMinerAPI(BaseMinerAPI):
</details>
"""
if respbefore:
command = {"cmd": "power_off", "respbefore": "true"}
else:
command = {"cmd": "power_off", "respbefore": "false"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("power_off", respbefore="true")
return await self.send_privileged_command("power_off", respbefore="false")
async def power_on(self) -> dict:
"""Power on the miner using the API.
@@ -411,10 +415,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of powering on.
</details>
"""
command = {"cmd": "power_on"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("power_on")
async def reset_led(self) -> dict:
"""Reset the LED on the miner using the API.
@@ -429,16 +430,14 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of resetting the LED.
</details>
"""
command = {"cmd": "set_led", "param": "auto"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.set_led(auto=True)
async def set_led(
self,
auto: bool = True,
color: str = "red",
period: int = 2000,
duration: int = 1000,
period: int = 60,
duration: int = 20,
start: int = 0,
) -> dict:
"""Set the LED on the miner using the API.
@@ -449,6 +448,7 @@ class BTMinerAPI(BaseMinerAPI):
changing the password of the miner using the Whatsminer tool.
Parameters:
auto: Whether or not to reset the LED to auto mode.
color: The LED color to set, either 'red' or 'green'.
period: The flash cycle in ms.
duration: LED on time in the cycle in ms.
@@ -458,16 +458,11 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting the LED.
</details>
"""
command = {
"cmd": "set_led",
"color": color,
"period": period,
"duration": duration,
"start": start,
}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
if auto:
return await self.send_privileged_command("set_led", param="auto")
return await self.send_privileged_command(
"set_led", color=color, period=period, duration=duration, start=start
)
async def set_low_power(self) -> dict:
"""Set low power mode on the miner using the API.
@@ -482,10 +477,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting low power mode.
</details>
"""
command = {"cmd": "set_low_power"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("set_low_power")
async def update_firmware(self): # noqa - static
"""Not implemented."""
@@ -493,7 +485,7 @@ class BTMinerAPI(BaseMinerAPI):
# requires a file stream in bytes
return NotImplementedError
async def reboot(self) -> dict:
async def reboot(self, timeout: int = 10) -> dict:
"""Reboot the miner using the API.
<details>
<summary>Expand</summary>
@@ -503,10 +495,14 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of the reboot.
</details>
"""
command = {"cmd": "reboot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
try:
d = await asyncio.wait_for(
self.send_privileged_command("reboot"), timeout=timeout
)
except (asyncio.CancelledError, asyncio.TimeoutError):
return {}
else:
return d
async def factory_reset(self) -> dict:
"""Reset the miner to factory defaults.
@@ -518,10 +514,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of the reset.
</details>
"""
command = {"cmd": "factory_reset"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("factory_reset")
async def update_pwd(self, old_pwd: str, new_pwd: str) -> dict:
"""Update the admin user's password.
@@ -541,16 +534,21 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting the password.
"""
self.pwd = old_pwd
# check if password length is greater than 8 bytes
if len(new_pwd.encode("utf-8")) > 8:
raise APIError(
f"New password too long, the max length is 8. "
f"Password size: {len(new_pwd.encode('utf-8'))}"
)
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
try:
data = await self.send_privileged_command(
"update_pwd", old=old_pwd, new=new_pwd
)
except APIError as e:
raise e
self.pwd = new_pwd
return data
async def set_target_freq(self, percent: int) -> dict:
"""Update the target frequency.
@@ -575,10 +573,9 @@ class BTMinerAPI(BaseMinerAPI):
f"range. Please set a % between -10 and "
f"100"
)
command = {"cmd": "set_target_freq", "percent": str(percent)}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command(
"set_target_freq", percent=str(percent)
)
async def enable_fast_boot(self) -> dict:
"""Turn on fast boot.
@@ -594,10 +591,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of enabling fast boot.
</details>
"""
command = {"cmd": "enable_btminer_fast_boot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("enable_btminer_fast_boot")
async def disable_fast_boot(self) -> dict:
"""Turn off fast boot.
@@ -613,10 +607,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of disabling fast boot.
</details>
"""
command = {"cmd": "disable_btminer_fast_boot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("disable_btminer_fast_boot")
async def enable_web_pools(self) -> dict:
"""Turn on web pool updates.
@@ -632,10 +623,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of enabling web pools.
</details>
"""
command = {"cmd": "enable_web_pools"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("enable_web_pools")
async def disable_web_pools(self) -> dict:
"""Turn off web pool updates.
@@ -651,10 +639,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of disabling web pools.
</details>
"""
command = {"cmd": "disable_web_pools"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("disable_web_pools")
async def set_hostname(self, hostname: str) -> dict:
"""Set the hostname of the miner.
@@ -672,13 +657,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting the hostname.
</details>
"""
command = {"cmd": "set_hostname", "hostname": hostname}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("set_hostname", hostname=hostname)
async def set_power_pct(self, percent: int) -> dict:
"""Set the power percentage of the miner.
"""Set the power percentage of the miner based on current power. Used for temporary adjustment.
<details>
<summary>Expand</summary>
@@ -700,10 +682,7 @@ class BTMinerAPI(BaseMinerAPI):
f"range. Please set a % between 0 and "
f"100"
)
command = {"cmd": "set_power_pct", "percent": str(percent)}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("set_power_pct", percent=str(percent))
async def pre_power_on(self, complete: bool, msg: str) -> dict:
"""Configure or check status of pre power on.
@@ -734,13 +713,138 @@ class BTMinerAPI(BaseMinerAPI):
'"adjust continue"]'
)
if complete:
complete = "true"
else:
complete = "false"
command = {"cmd": "pre_power_on", "complete": complete, "msg": msg}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command(
"pre_power_on", complete="true", msg=msg
)
return await self.send_privileged_command(
"pre_power_on", complete="false", msg=msg
)
### ADDED IN V2.0.5 Whatsminer API ###
@api_min_version("2.0.5")
async def set_temp_offset(self, temp_offset: int):
"""Set the offset of miner hash board target temperature.
<details>
<summary>Expand</summary>
Set the offset of miner hash board target temperature, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
temp_offset: Target temperature offset.
Returns:
A reply informing of the status of setting temp offset.
</details>
"""
if not -30 < temp_offset < 0:
raise APIError(
f"Temp offset is outside of the allowed "
f"range. Please set a number between -30 and "
f"0."
)
return await self.send_privileged_command(
"set_temp_offset", temp_offset=temp_offset
)
@api_min_version("2.0.5")
async def adjust_power_limit(self, power_limit: int):
"""Set the upper limit of the miner's power. Cannot be higher than the ordinary power of the machine.
<details>
<summary>Expand</summary>
Set the upper limit of the miner's power, only works after
changing the password of the miner using the Whatsminer tool.
The miner will reboot after this is set.
Parameters:
power_limit: New power limit.
Returns:
A reply informing of the status of setting power limit.
</details>
"""
return await self.send_privileged_command(
"adjust_power_limit", power_limit=power_limit
)
@api_min_version("2.0.5")
async def adjust_upfreq_speed(self, upfreq_speed: int):
"""Set the upfreq speed, 0 is the normal speed, 9 is the fastest speed.
<details>
<summary>Expand</summary>
Set the upfreq speed, 0 is the normal speed, 9 is the fastest speed, only works after
changing the password of the miner using the Whatsminer tool.
The faster the speed, the greater the final hash rate and power deviation, and the stability
may be impacted. Fast boot mode cannot be used at the same time.
Parameters:
upfreq_speed: New upfreq speed.
Returns:
A reply informing of the status of setting upfreq speed.
</details>
"""
if not 0 < upfreq_speed < 9:
raise APIError(
f"Upfreq speed is outside of the allowed "
f"range. Please set a number between 0 (Normal) and "
f"9 (Fastest)."
)
return await self.send_privileged_command(
"adjust_upfreq_speed", upfreq_speed=upfreq_speed
)
@api_min_version("2.0.5")
async def set_poweroff_cool(self, poweroff_cool: bool):
"""Set whether to cool the machine when mining is stopped.
<details>
<summary>Expand</summary>
Set whether to cool the machine when mining is stopped, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
poweroff_cool: Whether to cool the miner during power off mode.
Returns:
A reply informing of the status of setting power off cooling mode.
</details>
"""
return await self.send_privileged_command(
"set_poweroff_cool", poweroff_cool=int(poweroff_cool)
)
@api_min_version("2.0.5")
async def set_fan_zero_speed(self, fan_zero_speed: bool):
"""Sets whether the fan speed supports the lowest 0 speed.
<details>
<summary>Expand</summary>
Sets whether the fan speed supports the lowest 0 speed, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
fan_zero_speed: Whether the fan is allowed to support 0 speed.
Returns:
A reply informing of the status of setting fan minimum speed.
</details>
"""
return await self.send_privileged_command(
"set_fan_zero_speed", fan_zero_speed=int(fan_zero_speed)
)
#### END privileged COMMANDS ####
@@ -866,4 +970,19 @@ class BTMinerAPI(BaseMinerAPI):
General miner info.
</details>
"""
return await self.send_command("get_miner_info")
return await self.send_command("get_miner_info", allow_warning=False)
@api_min_version("2.0.1")
async def get_error_code(self) -> dict:
"""Get a list of error codes from the miner.
<details>
<summary>Expand</summary>
Get a list of error codes from the miner. Replaced `summary` as the location of error codes with API version 2.0.4.
Returns:
A list of error codes on the miner.
</details>
"""
return await self.send_command("get_error_code", allow_warning=False)

View File

@@ -12,7 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.API import BaseMinerAPI
import logging
from pyasic.API import APIError, BaseMinerAPI
class CGMinerAPI(BaseMinerAPI):
@@ -33,8 +35,38 @@ class CGMinerAPI(BaseMinerAPI):
port: The port to reference the API on. Default is 4028.
"""
def __init__(self, ip: str, port: int = 4028):
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028):
super().__init__(ip, port)
self.api_ver = api_ver
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesn't work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
data = await self._x19_multicommand(*command.split("+"))
return data
async def _x19_multicommand(self, *commands):
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError as e:
raise APIError(e)
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
)
return data
async def version(self) -> dict:
"""Get miner version info.

View File

@@ -18,13 +18,14 @@ from pyasic.API import BaseMinerAPI
class UnknownAPI(BaseMinerAPI):
"""An abstraction of an API for a miner which is unknown.
This class is designed to try to be a intersection of as many miner APIs
This class is designed to try to be an intersection of as many miner APIs
and API commands as possible (API ⋂ API), to ensure that it can be used
with as many APIs as possible.
"""
def __init__(self, ip, port=4028):
def __init__(self, ip, api_ver: str = "0.0.0", port: int = 4028):
super().__init__(ip, port)
self.api_ver = api_ver
async def asccount(self) -> dict:
return await self.send_command("asccount")

View File

@@ -11,3 +11,45 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.API.bmminer import BMMinerAPI
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API.btminer import BTMinerAPI
from pyasic.API.cgminer import CGMinerAPI
from pyasic.API.unknown import UnknownAPI
from pyasic.config import MinerConfig
from pyasic.data import (
BraiinsOSError,
InnosiliconError,
MinerData,
WhatsminerError,
X19Error,
)
from pyasic.errors import APIError, APIWarning
from pyasic.miners import get_miner
from pyasic.miners.base import AnyMiner
from pyasic.miners.miner_factory import MinerFactory
from pyasic.miners.miner_listener import MinerListener
from pyasic.network import MinerNetwork
from pyasic.settings import PyasicSettings
__all__ = [
"BMMinerAPI",
"BOSMinerAPI",
"BTMinerAPI",
"CGMinerAPI",
"UnknownAPI",
"MinerConfig",
"MinerData",
"BraiinsOSError",
"InnosiliconError",
"WhatsminerError",
"X19Error",
"APIError",
"APIWarning",
"get_miner",
"AnyMiner",
"MinerFactory",
"MinerListener",
"MinerNetwork",
"PyasicSettings",
]

View File

@@ -12,15 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass, asdict
from typing import Literal, List
import json
import logging
import random
import string
import time
from dataclasses import asdict, dataclass, fields
from typing import Dict, List, Literal
import toml
import yaml
import json
import time
@dataclass
@@ -37,6 +38,10 @@ class _Pool:
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.
@@ -52,6 +57,19 @@ class _Pool:
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.
@@ -65,6 +83,23 @@ class _Pool:
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.
@@ -106,6 +141,10 @@ class _PoolGroup:
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(
@@ -141,6 +180,32 @@ class _PoolGroup:
pools.append(pool.as_x19(user_suffix=user_suffix))
return pools
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = {}
for idx, pool in enumerate(self.pools[:3]):
pool_data = pool.as_inno(user_suffix=user_suffix)
for key in pool_data:
pools[f"{key}{idx+1}"] = pool_data[key]
return pools
def as_wm(self, user_suffix: str = None) -> List[dict]:
"""Convert the data in this class to a list usable by a Whatsminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = []
for pool in self.pools[:3]:
pools.append(pool.as_wm(user_suffix=user_suffix))
while len(pools) < 3:
pools.append({"url": None, "user": None, "pass": None})
return pools
def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a dict usable by an Avalonminer device.
@@ -191,7 +256,7 @@ class MinerConfig:
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
temp_target: float = 70.0
temp_hot: float = 80.0
temp_dangerous: float = 10.0
temp_dangerous: float = 100.0
minimum_fans: int = None
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
@@ -207,8 +272,13 @@ class MinerConfig:
dps_shutdown_enabled: bool = None
dps_shutdown_duration: float = None
@classmethod
def fields(cls):
return fields(cls)
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 data_dict[key] is None:
@@ -217,10 +287,12 @@ class MinerConfig:
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):
@@ -230,6 +302,7 @@ class MinerConfig:
Parameters:
data: The raw config data to convert.
"""
logging.debug(f"MinerConfig - (From Raw) - Loading raw config")
pool_groups = []
for key in data.keys():
if key == "pools":
@@ -243,6 +316,10 @@ class MinerConfig:
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]:
if data[key] == 1:
self.autotuning_wattage = 0
elif key == "fan_control":
for _key in data[key].keys():
if _key == "min_fans":
@@ -287,12 +364,28 @@ class MinerConfig:
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))
@@ -308,6 +401,7 @@ class MinerConfig:
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):
@@ -316,19 +410,45 @@ class MinerConfig:
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_wm(self, user_suffix: str = None) -> Dict[str, int]:
"""Convert the data in this class to a config usable by a Whatsminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Whatsminer) - Generating Whatsminer config")
return {
"pools": self.pool_groups[0].as_wm(user_suffix=user_suffix),
"wattage": self.autotuning_wattage,
}
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Inno) - Generating Innosilicon config")
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
def as_x19(self, user_suffix: str = None) -> str:
"""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 = {
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
"bitmain-fan-ctrl": False,
"bitmain-fan-pwn": 100,
"miner-mode": 0, # Normal Mode
}
if self.autotuning_wattage == 0:
cfg["miner-mode"] = 1 # Sleep Mode
if not self.temp_mode == "auto":
cfg["bitmain-fan-ctrl"] = True
@@ -344,6 +464,7 @@ class MinerConfig:
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
@@ -354,6 +475,7 @@ class MinerConfig:
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+",

View File

@@ -12,9 +12,26 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Union
from dataclasses import dataclass, field, asdict
from datetime import datetime
import copy
import json
import logging
import time
from dataclasses import asdict, dataclass, field, fields
from datetime import datetime, timezone
from typing import List, Union
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
@dataclass
class HashBoard:
slot: int = 0
hashrate: float = 0.0
temp: int = -1
chip_temp: int = -1
chips: int = 0
expected_chips: int = 0
missing: bool = True
@dataclass
@@ -25,6 +42,9 @@ class MinerData:
ip: The IP of the miner as a str.
datetime: The time and date this data was generated.
model: The model of the miner as a str.
make: The make of the miner as a str.
api_ver: The current api version on the miner as a str.
fw_ver: The current firmware version on the miner as a str.
hostname: The network hostname of the miner as a str.
hashrate: The hashrate of the miner in TH/s as a float.
left_board_hashrate: The hashrate of the left board of the miner in TH/s as a float.
@@ -44,47 +64,56 @@ class MinerData:
fan_2: The speed of the second fan as an int.
fan_3: The speed of the third fan as an int.
fan_4: The speed of the fourth fan as an int.
fan_psu: The speed of the PSU on the fan if the miner collects it.
left_chips: The number of chips online in the left board as an int.
center_chips: The number of chips online in the left board as an int.
right_chips: The number of chips online in the left board as an int.
total_chips: The total number of chips on all boards. Calculated automatically.
ideal_chips: The ideal number of chips in the miner as an int.
percent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
nominal: The nominal amount of chips in the miner. Calculated automatically.
nominal: Whether the number of chips in the miner is nominal. Calculated automatically.
pool_split: The pool split as a str.
pool_1_url: The first pool url on the miner as a str.
pool_1_user: The first pool user on the miner as a str.
pool_2_url: The second pool url on the miner as a str.
pool_2_user: The second pool user on the miner as a str.
errors: A list of errors on the miner.
fault_light: Whether or not the fault light is on as a boolean.
efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically.
"""
ip: str
datetime: datetime = None
mac: str = "00:00:00:00:00:00"
model: str = "Unknown"
make: str = "Unknown"
api_ver: str = "Unknown"
fw_ver: str = "Unknown"
hostname: str = "Unknown"
hashrate: float = 0
left_board_hashrate: float = 0
center_board_hashrate: float = 0
right_board_hashrate: float = 0
hashboards: List[HashBoard] = field(default_factory=list)
ideal_hashboards: int = 1
left_board_hashrate: float = field(init=False)
center_board_hashrate: float = field(init=False)
right_board_hashrate: float = field(init=False)
temperature_avg: int = field(init=False)
env_temp: float = 0
left_board_temp: int = 0
left_board_chip_temp: int = 0
center_board_temp: int = 0
center_board_chip_temp: int = 0
right_board_temp: int = 0
right_board_chip_temp: int = 0
wattage: int = 0
wattage_limit: int = 0
env_temp: float = -1.0
left_board_temp: int = field(init=False)
left_board_chip_temp: int = field(init=False)
center_board_temp: int = field(init=False)
center_board_chip_temp: int = field(init=False)
right_board_temp: int = field(init=False)
right_board_chip_temp: int = field(init=False)
wattage: int = -1
wattage_limit: int = -1
fan_1: int = -1
fan_2: int = -1
fan_3: int = -1
fan_4: int = -1
left_chips: int = 0
center_chips: int = 0
right_chips: int = 0
fan_psu: int = -1
left_chips: int = field(init=False)
center_chips: int = field(init=False)
right_chips: int = field(init=False)
total_chips: int = field(init=False)
ideal_chips: int = 1
percent_ideal: float = field(init=False)
@@ -94,20 +123,212 @@ class MinerData:
pool_1_user: str = "Unknown"
pool_2_url: str = ""
pool_2_user: str = ""
errors: list = field(default_factory=list)
errors: List[
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
] = field(default_factory=list)
fault_light: Union[bool, None] = None
efficiency: int = field(init=False)
@classmethod
def fields(cls):
return [f.name for f in fields(cls)]
def __post_init__(self):
self.datetime = datetime.now()
self.datetime = datetime.now(timezone.utc).astimezone()
def __getitem__(self, item):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"{item}")
def __setitem__(self, key, value):
return setattr(self, key, value)
def __iter__(self):
return iter([item for item in self.asdict()])
def __truediv__(self, other):
return self // other
def __floordiv__(self, other):
cp = copy.deepcopy(self)
for key in self:
item = getattr(self, key)
if isinstance(item, int):
setattr(cp, key, item // other)
if isinstance(item, float):
setattr(cp, key, round(item / other, 2))
return cp
def __add__(self, other):
if not isinstance(other, MinerData):
raise TypeError("Cannot add MinerData to non MinerData type.")
cp = copy.deepcopy(self)
for key in self:
item = getattr(self, key)
other_item = getattr(other, key)
if item is None:
item = 0
if other_item is None:
other_item = 0
if isinstance(item, int):
setattr(cp, key, item + other_item)
if isinstance(item, float):
setattr(cp, key, item + other_item)
if isinstance(item, str):
setattr(cp, key, "")
if isinstance(item, list):
setattr(cp, key, item + other_item)
if isinstance(item, bool):
setattr(cp, key, item & other_item)
return cp
@property
def total_chips(self): # noqa - Skip PyCharm inspection
return self.right_chips + self.center_chips + self.left_chips
return sum([hb.chips for hb in self.hashboards])
@total_chips.setter
def total_chips(self, val):
pass
@property
def left_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3]:
return self.hashboards[0].chips
return 0
@left_chips.setter
def left_chips(self, val):
pass
@property
def center_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].chips
if len(self.hashboards) == 3:
return self.hashboards[1].chips
return 0
@center_chips.setter
def center_chips(self, val):
pass
@property
def right_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].chips
if len(self.hashboards) == 3:
return self.hashboards[2].chips
return 0
@right_chips.setter
def right_chips(self, val):
pass
@property
def left_board_hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3]:
return self.hashboards[0].hashrate
return 0
@left_board_hashrate.setter
def left_board_hashrate(self, val):
pass
@property
def center_board_hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].hashrate
if len(self.hashboards) == 3:
return self.hashboards[1].hashrate
return 0
@center_board_hashrate.setter
def center_board_hashrate(self, val):
pass
@property
def right_board_hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].hashrate
if len(self.hashboards) == 3:
return self.hashboards[2].hashrate
return 0
@right_board_hashrate.setter
def right_board_hashrate(self, val):
pass
@property
def left_board_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3]:
return self.hashboards[0].temp
return 0
@left_board_temp.setter
def left_board_temp(self, val):
pass
@property
def center_board_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].temp
if len(self.hashboards) == 3:
return self.hashboards[1].temp
return 0
@center_board_temp.setter
def center_board_temp(self, val):
pass
@property
def right_board_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].temp
if len(self.hashboards) == 3:
return self.hashboards[2].temp
return 0
@right_board_temp.setter
def right_board_temp(self, val):
pass
@property
def left_board_chip_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3]:
return self.hashboards[0].chip_temp
return 0
@left_board_chip_temp.setter
def left_board_chip_temp(self, val):
pass
@property
def center_board_chip_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].chip_temp
if len(self.hashboards) == 3:
return self.hashboards[1].chip_temp
return 0
@center_board_chip_temp.setter
def center_board_chip_temp(self, val):
pass
@property
def right_board_chip_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].chip_temp
if len(self.hashboards) == 3:
return self.hashboards[2].chip_temp
return 0
@right_board_chip_temp.setter
def right_board_chip_temp(self, val):
pass
@property
def nominal(self): # noqa - Skip PyCharm inspection
return self.ideal_chips == self.total_chips
@@ -128,13 +349,9 @@ class MinerData:
def temperature_avg(self): # noqa - Skip PyCharm inspection
total_temp = 0
temp_count = 0
for temp in [
self.left_board_chip_temp,
self.center_board_chip_temp,
self.right_board_chip_temp,
]:
if temp and not temp == 0:
total_temp += temp
for hb in self.hashboards:
if hb.temp and not hb.temp == -1:
total_temp += hb.temp
temp_count += 1
if not temp_count > 0:
return 0
@@ -144,5 +361,92 @@ class MinerData:
def temperature_avg(self, val):
pass
def asdict(self):
@property
def efficiency(self): # noqa - Skip PyCharm inspection
if self.hashrate == 0:
return 0
return round(self.wattage / self.hashrate)
@efficiency.setter
def efficiency(self, val):
pass
def asdict(self) -> dict:
"""Get this dataclass as a dictionary.
Returns:
A dictionary version of this class.
"""
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
return asdict(self)
def as_json(self) -> str:
"""Get this dataclass as JSON.
Returns:
A JSON version of this class.
"""
logging.debug(f"MinerData - (To JSON) - Dumping JSON data")
data = self.asdict()
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
return json.dumps(data)
def as_csv(self) -> str:
"""Get this dataclass as CSV.
Returns:
A CSV version of this class with no headers.
"""
logging.debug(f"MinerData - (To CSV) - Dumping CSV data")
data = self.asdict()
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
errs = []
for error in data["errors"]:
errs.append(error["error_message"])
data["errors"] = "; ".join(errs)
data_list = [str(data[item]) for item in data]
return ",".join(data_list)
def as_influxdb(self, measurement_name: str = "miner_data") -> str:
"""Get this dataclass as [influxdb line protocol](https://docs.influxdata.com/influxdb/v2.4/reference/syntax/line-protocol/).
Parameters:
measurement_name: The name of the measurement to insert into in influxdb.
Returns:
A influxdb line protocol version of this class.
"""
logging.debug(f"MinerData - (To InfluxDB) - Dumping InfluxDB data")
tag_data = [measurement_name]
field_data = []
tags = ["ip", "mac", "model", "hostname"]
for attribute in self:
if attribute in tags:
escaped_data = self[attribute].replace(" ", "\\ ")
tag_data.append(f"{attribute}={escaped_data}")
continue
if isinstance(self[attribute], str):
field_data.append(f'{attribute}="{self[attribute]}"')
continue
if isinstance(self[attribute], bool):
field_data.append(f"{attribute}={str(self[attribute]).lower()}")
continue
if isinstance(self[attribute], int):
field_data.append(f"{attribute}={self[attribute]}")
continue
if isinstance(self[attribute], float):
field_data.append(f"{attribute}={self[attribute]}")
continue
if attribute == "fault_light" and not self[attribute]:
field_data.append(f"{attribute}=false")
continue
if attribute == "errors":
for idx, item in enumerate(self[attribute]):
field_data.append(f'error_{idx+1}="{item.error_message}"')
tags_str = ",".join(tag_data)
field_str = ",".join(field_data)
timestamp = str(int(time.mktime(self.datetime.timetuple()) * 1e9))
return " ".join([tags_str, field_str, timestamp])

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 dataclasses import asdict, dataclass, fields
@dataclass
class X19Error:
"""A Dataclass to handle error codes of X19 miners.
Attributes:
error_message: The error message as a string.
error_code: The error code as an int. 0 if the message is not assigned a code.
"""
error_message: str
error_code: int = 0
@classmethod
def fields(cls):
return fields(cls)
def asdict(self):
return asdict(self)

View File

@@ -12,5 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .whatsminer import WhatsminerError
from typing import TypeVar
from .bos import BraiinsOSError
from .innosilicon import InnosiliconError
from .whatsminer import WhatsminerError
from .X19 import X19Error
MinerErrorData = TypeVar(
"MinerErrorData", WhatsminerError, BraiinsOSError, X19Error, InnosiliconError
)

View File

@@ -12,14 +12,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass, asdict
from dataclasses import asdict, dataclass, fields
@dataclass
class BraiinsOSError:
"""A Dataclass to handle error codes of BraiinsOS+ miners."""
"""A Dataclass to handle error codes of BraiinsOS+ miners.
Attributes:
error_message: The error message as a string.
error_code: The error code as an int. 0 if the message is not assigned a code.
"""
error_message: str
error_code: int = 0
@classmethod
def fields(cls):
return fields(cls)
def asdict(self):
return asdict(self)

View File

@@ -0,0 +1,69 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import asdict, dataclass, field, fields
@dataclass
class InnosiliconError:
"""A Dataclass to handle error codes of Innosilicon miners.
Attributes:
error_code: The error code as an int.
error_message: The error message as a string. Automatically found from the error code.
"""
error_code: int
error_message: str = field(init=False)
@classmethod
def fields(cls):
return fields(cls)
@property
def error_message(self): # noqa - Skip PyCharm inspection
if self.error_code in ERROR_CODES:
return ERROR_CODES[self.error_code]
return "Unknown error type."
@error_message.setter
def error_message(self, val):
pass
def asdict(self):
return asdict(self)
ERROR_CODES = {
21: "The PLUG signal of the hash board is not detected.",
22: "Power I2C communication is abnormal.",
23: "The SPI of all hash boards is blocked.",
24: "Some of the hash boards fail to connect to the SPI'.",
25: "Hashboard failed to set frequency.",
26: "Hashboard failed to set voltage.",
27: "Chip BIST test failed.",
28: "Hashboard SPI communication is abnormal.",
29: "Power I2C communication is abnormal.",
30: "Pool connection failed.",
31: "Individual chips are damaged.",
32: "Over temperature protection.",
33: "Hashboard fault.",
34: "The data cables are not connected in the correct order.",
35: "No power output.",
36: "Hashboard fault.",
37: "Control board and/or hashboard do not match.",
40: "Power output is abnormal.",
41: "Power output is abnormal.",
42: "Hashboard fault.",
}

View File

@@ -12,21 +12,69 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass, field, asdict
from dataclasses import asdict, dataclass, field, fields
@dataclass
class WhatsminerError:
"""A Dataclass to handle error codes of Whatsminers."""
"""A Dataclass to handle error codes of Whatsminers.
Attributes:
error_code: The error code as an int.
error_message: The error message as a string. Automatically found from the error code.
"""
error_code: int
error_message: str = field(init=False)
@classmethod
def fields(cls):
return fields(cls)
@property
def error_message(self): # noqa - Skip PyCharm inspection
if self.error_code in ERROR_CODES:
return ERROR_CODES[self.error_code]
return "Unknown error type."
if len(str(self.error_code)) > 3 and str(self.error_code).startswith("55"):
# 55 error code base has chip numbers, so the format is
# 55 -> board num len 1 -> chip num len 3
err_type = 55
err_subtype = int(str(self.error_code)[2:3])
err_value = int(str(self.error_code)[3:])
else:
err_type = int(str(self.error_code)[:-2])
err_subtype = int(str(self.error_code)[-2:-1])
err_value = int(str(self.error_code)[-1:])
try:
select_err_type = ERROR_CODES[err_type]
if err_subtype in select_err_type:
select_err_subtype = select_err_type[err_subtype]
if err_value in select_err_subtype:
return select_err_subtype[err_value]
elif "n" in select_err_subtype:
return select_err_subtype[
"n" # noqa: picks up `select_err_subtype["n"]` as not being numeric?
].replace(
"{n}", str(err_value)
)
else:
return "Unknown error type."
elif "n" in select_err_type:
select_err_subtype = select_err_type[
"n" # noqa: picks up `select_err_subtype["n"]` as not being numeric?
]
if err_value in select_err_subtype:
return select_err_subtype[err_value]
elif "c" in select_err_subtype:
return (
select_err_subtype["c"]
.replace( # noqa: picks up `select_err_subtype["n"]` as not being numeric?
"{n}", str(err_subtype)
)
.replace("{c}", str(err_value))
)
else:
return "Unknown error type."
except KeyError:
return "Unknown error type."
@error_message.setter
def error_message(self, val):
@@ -37,130 +85,160 @@ class WhatsminerError:
ERROR_CODES = {
110: "Intake fan speed error.",
111: "Exhaust fan speed error.",
120: "Intake fan speed error. Fan speed deviates by more than 2000.",
121: "Exhaust fan speed error. Fan speed deviates by more than 2000.",
130: "Intake fan speed error. Fan speed deviates by more than 3000.",
131: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
140: "Fan speed too high.",
200: "Power probing error. No power found.",
201: "Power supply and configuration file don't match.",
202: "Power output voltage error.",
203: "Power protecting due to high environment temperature.",
204: "Power current protecting due to high environment temperature.",
205: "Power current error.",
206: "Power input low voltage error.",
207: "Power input current protecting due to bad power input.",
210: "Power error.",
213: "Power input voltage and current do not match power output.",
216: "Power remained unchanged for a long time.",
217: "Power set enable error.",
218: "Power input voltage is lower than 230V for high power mode.",
233: "Power output high temperature protection error.",
234: "Power output high temperature protection error.",
235: "Power output high temperature protection error.",
236: "Power output high current protection error.",
237: "Power output high current protection error.",
238: "Power output high current protection error.",
239: "Power output high voltage protection error.",
240: "Power output low voltage protection error.",
241: "Power output current imbalance error.",
243: "Power input high temperature protection error.",
244: "Power input high temperature protection error.",
245: "Power input high temperature protection error.",
246: "Power input high voltage protection error.",
247: "Power input high voltage protection error.",
248: "Power input high current protection error.",
249: "Power input high current protection error.",
250: "Power input low voltage protection error.",
251: "Power input low voltage protection error.",
253: "Power supply fan error.",
254: "Power supply fan error.",
255: "Power output high power protection error.",
256: "Power output high power protection error.",
257: "Input over current protection of power supply on primary side.",
263: "Power communication warning.",
264: "Power communication error.",
267: "Power watchdog protection.",
268: "Power output high current protection.",
269: "Power input high current protection.",
270: "Power input high voltage protection.",
271: "Power input low voltage protection.",
272: "Excessive power supply output warning.",
273: "Power input too high warning.",
274: "Power fan warning.",
275: "Power high temperature warning.",
300: "Right board temperature sensor detection error.",
301: "Center board temperature sensor detection error.",
302: "Left board temperature sensor detection error.",
320: "Right board temperature reading error.",
321: "Center board temperature reading error.",
322: "Left board temperature reading error.",
329: "Control board temperature sensor communication error.",
350: "Right board temperature protecting.",
351: "Center board temperature protecting.",
352: "Left board temperature protecting.",
360: "Hashboard high temperature error.",
410: "Right board eeprom detection error.",
411: "Center board eeprom detection error.",
412: "Left board eeprom detection error.",
420: "Right board eeprom parsing error.",
421: "Center board eeprom parsing error.",
422: "Left board eeprom parsing error.",
430: "Right board chip bin type error.",
431: "Center board chip bin type error.",
432: "Left board chip bin type error.",
440: "Right board eeprom chip number X error.",
441: "Center board eeprom chip number X error.",
442: "Left board eeprom chip number X error.",
450: "Right board eeprom xfer error.",
451: "Center board eeprom xfer error.",
452: "Left board eeprom xfer error.",
510: "Right board miner type error.",
511: "Center board miner type error.",
512: "Left board miner type error.",
520: "Right board bin type error.",
521: "Center board bin type error.",
522: "Left board bin type error.",
530: "Right board not found.",
531: "Center board not found.",
532: "Left board not found.",
540: "Right board error reading chip id.",
541: "Center board error reading chip id.",
542: "Left board error reading chip id.",
550: "Right board has bad chips.",
551: "Center board has bad chips.",
552: "Left board has bad chips.",
560: "Right board loss of balance error.",
561: "Center board loss of balance error.",
562: "Left board loss of balance error.",
600: "Environment temperature is too high.",
610: "Environment temperature is too high for high performance mode.",
701: "Control board no support chip.",
710: "Control board rebooted as an exception.",
712: "Control board rebooted as an exception.",
800: "CGMiner checksum error.",
801: "System monitor checksum error.",
802: "Remote daemon checksum error.",
2010: "All pools are disabled.",
2020: "Pool 0 connection failed.",
2021: "Pool 1 connection failed.",
2022: "Pool 2 connection failed.",
2030: "High rejection rate on pool.",
2040: "The pool does not support asicboost mode.",
2310: "Hashrate is too low.",
2320: "Hashrate is too low.",
2340: "Hashrate loss is too high.",
2350: "Hashrate loss is too high.",
5070: "Right hashboard water velocity is abnormal.",
5071: "Center hashboard water velocity is abnormal.",
5072: "Left hashboard water velocity is abnormal.",
5110: "Right hashboard frequency up timeout.",
5111: "Center hashboard frequency up timeout.",
5112: "Left hashboard frequency up timeout.",
8410: "Software version error.",
100001: "/antiv/signature illegal.",
100002: "/antiv/dig/init.d illegal.",
100003: "/antiv/dig/pf_partial.dig illegal.",
1: { # Fan error
1: { # Fan speed error of 1000+
0: "Intake fan speed error.",
1: "Exhaust fan speed error.",
},
2: { # Fan speed error of 2000+
0: "Intake fan speed error. Fan speed deviates by more than 2000.",
1: "Exhaust fan speed error. Fan speed deviates by more than 2000.",
},
3: { # Fan speed error of 3000+
0: "Intake fan speed error. Fan speed deviates by more than 3000.",
1: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
},
4: {0: "Fan speed too high."}, # High speed
},
2: { # Power error
0: {
0: "Power probing error. No power found.",
1: "Power supply and configuration file don't match.",
2: "Power output voltage error.",
3: "Power protecting due to high environment temperature.",
4: "Power current protecting due to high environment temperature.",
5: "Power current error.",
6: "Power input low voltage error.",
7: "Power input current protecting due to bad power input.",
},
1: {
0: "Power error.",
3: "Power input voltage and current do not match power output.",
6: "Power remained unchanged for a long time.",
7: "Power set enable error.",
8: "Power input voltage is lower than 230V for high power mode.",
},
3: {
3: "Power output high temperature protection error.",
4: "Power output high temperature protection error.",
5: "Power output high temperature protection error.",
6: "Power output high current protection error.",
7: "Power output high current protection error.",
8: "Power output high current protection error.",
9: "Power output high voltage protection error.",
},
4: {
0: "Power output low voltage protection error.",
1: "Power output current imbalance error.",
3: "Power input high temperature protection error.",
4: "Power input high temperature protection error.",
5: "Power input high temperature protection error.",
6: "Power input high voltage protection error.",
7: "Power input high voltage protection error.",
8: "Power input high current protection error.",
9: "Power input high current protection error.",
},
5: {
0: "Power input low voltage protection error.",
1: "Power input low voltage protection error.",
3: "Power supply fan error.",
4: "Power supply fan error.",
5: "Power output high power protection error.",
6: "Power output high power protection error.",
7: "Input over current protection of power supply on primary side.",
},
6: {
3: "Power communication warning.",
4: "Power communication error.",
7: "Power watchdog protection.",
8: "Power output high current protection.",
9: "Power input high current protection.",
},
7: {
0: "Power input high voltage protection.",
1: "Power input low voltage protection.",
2: "Excessive power supply output warning.",
3: "Power input too high warning.",
4: "Power fan warning.",
5: "Power high temperature warning.",
},
},
3: { # temperature error
0: { # sensor detection error
"n": "Slot {n} temperature sensor detection error."
},
2: { # temperature reading error
"n": "Slot {n} temperature reading error.",
9: "Control board temperature sensor communication error.",
},
5: {"n": "Slot {n} temperature protecting."}, # temperature protection
6: {0: "Hashboard high temperature error."}, # high temp
},
4: { # EEPROM error
1: {"n": "Slot {n} eeprom detection error."}, # EEPROM detection error
2: {"n": "Slot {n} eeprom parsing error."}, # EEPROM parsing error
3: {"n": "Slot {n} chip bin type error."}, # chip bin error
4: {"n": "Slot {n} eeprom chip number X error."}, # EEPROM chip number error
5: {"n": "Slot {n} eeprom xfer error."}, # EEPROM xfer error
},
5: { # hashboard error
1: {"n": "Slot {n} miner type error."}, # board miner type error
2: {"n": "Slot {n} bin type error."}, # chip bin type error
3: {"n": "Slot {n} not found."}, # board not found error
4: {"n": "Slot {n} error reading chip id."}, # reading chip id error
5: {"n": "Slot {n} has bad chips."}, # board has bad chips error
6: {"n": "Slot {n} loss of balance error."}, # loss of balance error
},
6: { # env temp error
0: {0: "Environment temperature is too high."}, # normal env temp error
1: { # high power env temp error
0: "Environment temperature is too high for high performance mode."
},
},
7: { # control board error
0: {1: "Control board no support chip."},
1: {
0: "Control board rebooted as an exception.",
2: "Control board rebooted as an exception.",
},
},
8: { # checksum error
0: {
0: "CGMiner checksum error.",
1: "System monitor checksum error.",
2: "Remote daemon checksum error.",
}
},
20: { # pool error
1: {0: "All pools are disabled."}, # all disabled error
2: {"n": "Pool {n} connection failed."}, # pool connection failed error
3: {0: "High rejection rate on pool."}, # rejection rate error
4: { # asicboost not supported error
0: "The pool does not support asicboost mode."
},
},
23: { # hashrate error
1: {0: "Hashrate is too low."},
2: {0: "Hashrate is too low."},
3: {0: "Hashrate loss is too high."},
4: {0: "Hashrate loss is too high."},
},
50: { # water velocity error
7: {"n": "Slot {n} water velocity is abnormal."}, # abnormal water velocity
9: {"n": "Slot {n} chip temp calibration check no balance."},
},
51: { # frequency error
7: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
},
55: {"n": {"c": "Slot {n} chip {c} has been reset."}},
84: {
1: {0: "Software version error."},
},
1000: {
0: {
1: "/antiv/signature illegal.",
2: "/antiv/dig/init.d illegal.",
3: "/antiv/dig/pf_partial.dig illegal.",
},
},
}

41
pyasic/errors/__init__.py Normal file
View File

@@ -0,0 +1,41 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class APIError(Exception):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Incorrect API parameters."
class APIWarning(Warning):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Incorrect API parameters."

View File

@@ -13,6 +13,7 @@
# limitations under the License.
import logging
from pyasic.settings import PyasicSettings

View File

@@ -12,105 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncssh
import logging
import ipaddress
from typing import Union
from pyasic.data import MinerData
from pyasic.miners.base import AnyMiner, BaseMiner
from pyasic.miners.miner_factory import MinerFactory
class BaseMiner:
def __init__(self, *args) -> None:
self.ip = None
self.uname = "root"
self.pwd = "admin"
self.api = None
self.api_type = None
self.model = None
self.light = None
self.hostname = None
self.nominal_chips = 1
self.version = None
self.fan_count = 2
self.config = None
def __repr__(self):
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
def __lt__(self, other):
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
def __gt__(self, other):
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username=self.uname,
password=self.pwd,
server_host_key_algs=["ssh-rsa"],
)
return conn
except asyncssh.misc.PermissionDenied:
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username="root",
password="admin",
server_host_key_algs=["ssh-rsa"],
)
return conn
except Exception as e:
raise e
except OSError as e:
logging.warning(f"Connection refused: {self}")
raise e
except Exception as e:
raise e
async def fault_light_on(self) -> bool:
return False
async def fault_light_off(self) -> bool:
return False
async def send_file(self, src, dest):
async with (await self._get_ssh_connection()) as conn:
await asyncssh.scp(src, (conn, dest))
async def check_light(self):
return self.light
async def get_board_info(self):
return None
async def get_config(self):
return None
async def get_hostname(self):
return None
async def get_model(self):
return None
async def reboot(self):
return False
async def restart_backend(self):
return False
async def send_config(self, *args, **kwargs):
return None
async def get_mac(self):
return None
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))
# abstracted version of get miner that is easier to access
async def get_miner(ip: Union[ipaddress.ip_address, str]) -> AnyMiner:
return await MinerFactory().get_miner(ip)

View File

@@ -17,3 +17,4 @@ from .bosminer import BOSMiner
from .btminer import BTMiner
from .cgminer import CGMiner
from .hiveon import Hiveon
from .cgminer_avalon import CGMinerAvalon

View File

@@ -14,96 +14,42 @@
import ipaddress
import logging
from typing import Union
from typing import List, Union, Tuple, Optional
from collections import namedtuple
import asyncssh
from pyasic.API.bmminer import BMMinerAPI
from pyasic.miners import BaseMiner
from pyasic.data import MinerData
from pyasic.config import MinerConfig
from pyasic.data import HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
class BMMiner(BaseMiner):
"""Base handler for BMMiner based miners."""
def __init__(self, ip: str) -> None:
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip)
self.ip = ipaddress.ip_address(ip)
self.api = BMMinerAPI(ip)
self.api = BMMinerAPI(ip, api_ver)
self.api_type = "BMMiner"
self.api_ver = api_ver
self.uname = "root"
self.pwd = "admin"
async def get_model(self) -> Union[str, None]:
"""Get miner model.
Returns:
Miner model or None.
"""
# check if model is cached
if self.model:
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
# get devdetails data
version_data = await self.api.devdetails()
# if we get data back, parse it for model
if version_data:
# handle Antminer BMMiner as a base
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
# if we don't get devdetails, log a failed attempt
logging.warning(f"Failed to get model for miner: {self}")
return None
async def get_hostname(self) -> str:
"""Get miner hostname.
Returns:
The hostname of the miner as a string or "?"
"""
if self.hostname:
return self.hostname
try:
# open an ssh connection
async with (await self._get_ssh_connection()) as conn:
# if we get the connection, check hostname
if conn is not None:
# get output of the hostname file
data = await conn.run("cat /proc/sys/kernel/hostname")
host = data.stdout.strip()
# return hostname data
logging.debug(f"Found hostname for {self.ip}: {host}")
self.hostname = host
return self.hostname
else:
# return ? if we fail to get hostname with no ssh connection
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
except Exception:
# return ? if we fail to get hostname with an exception
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
async def send_ssh_command(self, cmd: str) -> Union[str, None]:
"""Send a command to the miner over ssh.
Parameters:
cmd: The command to run.
Returns:
Result of the command or None.
"""
async def send_ssh_command(self, cmd: str) -> Optional[str]:
result = None
try:
conn = await self._get_ssh_connection()
except (asyncssh.Error, OSError):
return None
# open an ssh connection
async with (await self._get_ssh_connection()) as conn:
async with conn:
# 3 retries
for i in range(3):
try:
@@ -122,31 +68,17 @@ class BMMiner(BaseMiner):
# return the result, either command output or None
return result
async def get_config(self) -> Union[list, None]:
"""Get the pool configuration of the miner.
Returns:
Pool config data or None.
"""
async def get_config(self) -> MinerConfig:
# get pool data
pools = await self.api.pools()
pool_data = []
try:
pools = await self.api.pools()
except APIError:
return self.config
# ensure we got pool data
if not pools:
return
# parse all the pools
for pool in pools["POOLS"]:
pool_data.append({"url": pool["URL"], "user": pool["User"], "pwd": "123"})
return pool_data
self.config = MinerConfig().from_api(pools["POOLS"])
return self.config
async def reboot(self) -> bool:
"""Reboot the miner.
Returns:
The result of rebooting the miner.
"""
logging.debug(f"{self}: Sending reboot command.")
_ret = await self.send_ssh_command("reboot")
logging.debug(f"{self}: Reboot command completed.")
@@ -154,59 +86,118 @@ class BMMiner(BaseMiner):
return True
return False
async def get_data(self) -> MinerData:
"""Get data from the miner.
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
"""
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
async def fault_light_off(self) -> bool:
return False
board_offset = -1
fan_offset = -1
async def fault_light_on(self) -> bool:
return False
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
async def restart_backend(self) -> bool:
return False
if model:
data.model = model
async def stop_mining(self) -> bool:
return False
if hostname:
data.hostname = hostname
async def resume_mining(self) -> bool:
return False
if mac:
data.mac = mac
async def set_power_limit(self, wattage: int) -> bool:
return False
data.fault_light = await self.check_light()
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
miner_data = None
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True
)
if miner_data:
break
async def get_mac(self) -> str:
return "00:00:00:00:00:00"
if not miner_data:
return data
async def get_model(self, api_devdetails: dict = None) -> Optional[str]:
if self.model:
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
summary = miner_data.get("summary")[0]
pools = miner_data.get("pools")[0]
stats = miner_data.get("stats")[0]
if not api_devdetails:
try:
api_devdetails = await self.api.devdetails()
except APIError:
pass
if summary:
hr = summary.get("SUMMARY")
if hr:
if len(hr) > 0:
hr = hr[0].get("GHS av")
if hr:
data.hashrate = round(hr / 1000, 2)
if api_devdetails:
try:
self.model = api_devdetails["DEVDETAILS"][0]["Model"].replace(
"Antminer ", ""
)
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
except (TypeError, IndexError, KeyError):
pass
if stats:
boards = stats.get("STATS")
if boards:
if len(boards) > 0:
logging.warning(f"Failed to get model for miner: {self}")
return None
async def get_version(
self, api_version: dict = None
) -> Tuple[Optional[str], Optional[str]]:
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
# Check to see if the version info is already cached
if self.api_ver and self.fw_ver:
return miner_version(self.api_ver, self.fw_ver)
if not api_version:
try:
api_version = await self.api.version()
except APIError:
pass
if api_version:
try:
self.api_ver = api_version["VERSION"][0]["API"]
except (KeyError, IndexError):
pass
try:
self.fw_ver = api_version["VERSION"][0]["CompileTime"]
except (KeyError, IndexError):
pass
return miner_version(self.api_ver, self.fw_ver)
async def get_hostname(self) -> Optional[str]:
hn = await self.send_ssh_command("cat /proc/sys/kernel/hostname")
if hn:
self.hostname = hn
return self.hostname
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
# get hr from API
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return round(float(api_summary["SUMMARY"][0]["GHS 5s"] / 1000), 2)
except (IndexError, KeyError, ValueError, TypeError):
pass
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
hashboards = []
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
board_offset = -1
boards = api_stats["STATS"]
if len(boards) > 1:
for board_num in range(1, 16, 5):
for _b_num in range(5):
b = boards[1].get(f"chain_acn{board_num + _b_num}")
@@ -216,93 +207,215 @@ class BMMiner(BaseMiner):
if board_offset == -1:
board_offset = 1
data.left_chips = boards[1].get(f"chain_acn{board_offset}")
data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
data.left_board_hashrate = round(
float(boards[1].get(f"chain_rate{board_offset}")) / 1000, 2
)
data.center_board_hashrate = round(
float(boards[1].get(f"chain_rate{board_offset+1}")) / 1000, 2
)
data.right_board_hashrate = round(
float(boards[1].get(f"chain_rate{board_offset+2}")) / 1000, 2
)
if stats:
temp = stats.get("STATS")
if temp:
if len(temp) > 1:
for fan_num in range(1, 8, 4):
for _f_num in range(4):
f = temp[1].get(f"fan{fan_num + _f_num}")
if f and not f == 0 and fan_offset == -1:
fan_offset = fan_num
if fan_offset == -1:
fan_offset = 1
for fan in range(self.fan_count):
setattr(
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
for i in range(board_offset, board_offset + self.ideal_hashboards):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
)
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
env_temp_list = []
for item in range(3):
board_temp = temp[1].get(f"temp{item + board_offset}")
chip_temp = temp[1].get(f"temp2_{item + board_offset}")
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
setattr(data, f"{board_map[item]}_temp", board_temp)
if f"temp_pcb{item}" in temp[1].keys():
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
if not env_temp == 0:
env_temp_list.append(int(env_temp))
data.env_temp = sum(env_temp_list) / len(env_temp_list)
chip_temp = boards[1].get(f"temp{i}")
if chip_temp:
hashboard.chip_temp = round(chip_temp)
if pools:
pool_1 = None
pool_2 = None
pool_1_user = None
pool_2_user = None
pool_1_quota = 1
pool_2_quota = 1
quota = 0
for pool in pools.get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
pool_1_quota = pool["Quota"]
elif not pool_2_user:
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if not pool.get("User") == pool_1_user:
if not pool_2_user == pool.get("User"):
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if pool_2_user and not pool_2_user == pool_1_user:
quota = f"{pool_1_quota}/{pool_2_quota}"
temp = boards[1].get(f"temp2_{i}")
if temp:
hashboard.temp = round(temp)
if pool_1:
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
hashrate = boards[1].get(f"chain_rate{i}")
if hashrate:
hashboard.hashrate = round(float(hashrate) / 1000, 2)
chips = boards[1].get(f"chain_acn{i}")
if chips:
hashboard.chips = chips
hashboard.missing = False
if (not chips) or (not chips > 0):
hashboard.missing = True
hashboards.append(hashboard)
except (IndexError, KeyError, ValueError, TypeError):
pass
return hashboards
async def get_env_temp(self) -> Optional[float]:
return None
async def get_wattage(self) -> Optional[int]:
return None
async def get_wattage_limit(self) -> Optional[int]:
return None
async def get_fans(
self, api_stats: dict = None
) -> Tuple[
Tuple[Optional[int], Optional[int], Optional[int], Optional[int]],
Tuple[Optional[int]],
]:
fan_speeds = namedtuple("FanSpeeds", "fan_1 fan_2 fan_3 fan_4")
psu_fan_speeds = namedtuple("PSUFanSpeeds", "psu_fan")
miner_fan_speeds = namedtuple("MinerFans", "fan_speeds psu_fan_speeds")
psu_fans = psu_fan_speeds(None)
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
fans_data = [None, None, None, None]
if api_stats:
try:
fan_offset = -1
for fan_num in range(1, 8, 4):
for _f_num in range(4):
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}")
if f and not f == 0 and fan_offset == -1:
fan_offset = fan_num
if fan_offset == -1:
fan_offset = 1
for fan in range(self.fan_count):
fans_data[fan] = api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
except (KeyError, IndexError):
pass
fans = fan_speeds(*fans_data)
return miner_fan_speeds(fans, psu_fans)
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
try:
pools = {}
for i, pool in enumerate(api_pools["POOLS"]):
pools[f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["User"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_errors(self) -> List[MinerErrorData]:
return []
async def get_fault_light(self) -> bool:
return False
async def _get_data(self, allow_warning: bool) -> dict:
miner_data = None
for i in range(PyasicSettings().miner_get_data_retries):
try:
miner_data = await self.api.multicommand(
"summary",
"pools",
"version",
"devdetails",
"stats",
allow_warning=allow_warning,
)
data.pool_1_url = pool_1
except APIError:
try:
miner_data = await self.api.multicommand(
"summary",
"version",
"pools",
"stats",
allow_warning=allow_warning,
)
except APIError:
pass
if miner_data:
break
if miner_data:
summary = miner_data.get("summary")
if summary:
summary = summary[0]
pools = miner_data.get("pools")
if pools:
pools = pools[0]
version = miner_data.get("version")
if version:
version = version[0]
devdetails = miner_data.get("devdetails")
if devdetails:
devdetails = devdetails[0]
stats = miner_data.get("stats")
if stats:
stats = stats[0]
else:
summary, pools, devdetails, version, stats = (None for _ in range(5))
if pool_1_user:
data.pool_1_user = pool_1_user
data = { # noqa - Ignore dictionary could be re-written
# ip - Done at start
# datetime - Done auto
"mac": await self.get_mac(),
"model": await self.get_model(api_devdetails=devdetails),
# make - Done at start
"api_ver": None, # - Done at end
"fw_ver": None, # Done at end.
"hostname": await self.get_hostname(),
"hashrate": await self.get_hashrate(api_summary=summary),
"hashboards": await self.get_hashboards(api_stats=stats),
# ideal_hashboards - Done at start
"env_temp": await self.get_env_temp(),
"wattage": await self.get_wattage(),
"wattage_limit": await self.get_wattage_limit(),
"fan_1": None, # - Done at end
"fan_2": None, # - Done at end
"fan_3": None, # - Done at end
"fan_4": None, # - Done at end
"fan_psu": None, # - Done at end
# ideal_chips - Done at start
"pool_split": None, # - Done at end
"pool_1_url": None, # - Done at end
"pool_1_user": None, # - Done at end
"pool_2_url": None, # - Done at end
"pool_2_user": None, # - Done at end
"errors": await self.get_errors(),
"fault_light": await self.get_fault_light(),
}
if pool_2:
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
data["api_ver"], data["fw_ver"] = await self.get_version(api_version=version)
fan_data = await self.get_fans()
if fan_data:
data["fan_1"] = fan_data.fan_speeds.fan_1 # noqa
data["fan_2"] = fan_data.fan_speeds.fan_2 # noqa
data["fan_3"] = fan_data.fan_speeds.fan_3 # noqa
data["fan_4"] = fan_data.fan_speeds.fan_4 # noqa
if pool_2_user:
data.pool_2_user = pool_2_user
data["fan_psu"] = fan_data.psu_fan_speeds.psu_fan # noqa
if quota:
data.pool_split = str(quota)
pools_data = await self.get_pools(api_pools=pools)
if pools_data:
data["pool_1_url"] = pools_data[0]["pool_1_url"]
data["pool_1_user"] = pools_data[0]["pool_1_user"]
if len(pools_data) > 1:
data["pool_2_url"] = pools_data[1]["pool_2_url"]
data["pool_2_user"] = pools_data[1]["pool_2_user"]
data[
"pool_split"
] = f"{pools_data[0]['quota']}/{pools_data[1]['quota']}"
else:
try:
data["pool_2_url"] = pools_data[0]["pool_2_url"]
data["pool_2_user"] = pools_data[0]["pool_2_user"]
data["quota"] = "0"
except KeyError:
pass
return data

File diff suppressed because it is too large Load Diff

View File

@@ -12,42 +12,49 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import ipaddress
import logging
from collections import namedtuple
from typing import List, Tuple, Optional
from typing import Union
import asyncssh
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.miners import BaseMiner
from pyasic.config import MinerConfig
from pyasic.data import MinerData, HashBoard
from pyasic.data.error_codes import MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
class BOSMinerOld(BaseMiner):
def __init__(self, ip: str) -> None:
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip)
self.ip = ipaddress.ip_address(ip)
self.api = BOSMinerAPI(ip)
self.api = BOSMinerAPI(ip, api_ver)
self.api_type = "BOSMiner"
self.uname = "root"
self.pwd = "admin"
async def send_ssh_command(self, cmd: str) -> str or None:
"""Send a command to the miner over ssh.
:return: Result of the command or None.
"""
async def send_ssh_command(self, cmd: str) -> Optional[str]:
result = None
try:
conn = await self._get_ssh_connection()
except (asyncssh.Error, OSError):
return None
# open an ssh connection
async with (await self._get_ssh_connection()) as conn:
async with conn:
# 3 retries
for i in range(3):
try:
# run the command and get the result
result = await conn.run(cmd)
if result.stdout:
result = result.stdout
result = result.stdout
except Exception as e:
if e == "SSH connection closed":
return "Update completed."
# if the command fails, log it
logging.warning(f"{self} command {cmd} error: {e}")
@@ -56,8 +63,122 @@ class BOSMinerOld(BaseMiner):
return
continue
# return the result, either command output or None
return str(result)
return result
async def update_to_plus(self):
result = await self.send_ssh_command("opkg update && opkg install bos_plus")
return result
async def check_light(self) -> bool:
return False
async def fault_light_on(self) -> bool:
return False
async def fault_light_off(self) -> bool:
return False
async def get_config(self) -> None:
return None
async def reboot(self) -> bool:
return False
async def restart_backend(self) -> bool:
return False
async def stop_mining(self) -> bool:
return False
async def resume_mining(self) -> bool:
return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def set_power_limit(self, wattage: int) -> bool:
return False
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_mac(self) -> Optional[str]:
return None
async def get_model(self) -> str:
return "S9"
async def get_version(self) -> Tuple[Optional[str], Optional[str]]:
return None, None
async def get_hostname(self) -> Optional[str]:
return None
async def get_hashrate(self) -> Optional[float]:
return None
async def get_hashboards(self) -> List[HashBoard]:
return []
async def get_env_temp(self) -> Optional[float]:
return None
async def get_wattage(self) -> Optional[int]:
return None
async def get_wattage_limit(self) -> Optional[int]:
return None
async def get_fans(
self,
) -> Tuple[
Tuple[Optional[int], Optional[int], Optional[int], Optional[int]],
Tuple[Optional[int]],
]:
fan_speeds = namedtuple("FanSpeeds", "fan_1 fan_2 fan_3 fan_4")
psu_fan_speeds = namedtuple("PSUFanSpeeds", "psu_fan")
miner_fan_speeds = namedtuple("MinerFans", "fan_speeds psu_fan_speeds")
fans = fan_speeds(None, None, None, None)
psu_fans = psu_fan_speeds(None)
return miner_fan_speeds(fans, psu_fans)
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
try:
pools = {}
for i, pool in enumerate(api_pools["POOLS"]):
pools[f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["User"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_errors(self) -> List[MinerErrorData]:
return []
async def get_fault_light(self) -> bool:
return False
async def _get_data(self, allow_warning: bool) -> dict:
return {}
async def get_data(self, allow_warning: bool = False) -> MinerData:
return MinerData(ip=str(self.ip))

View File

@@ -14,260 +14,606 @@
import ipaddress
import logging
from typing import Union
from typing import List, Union, Tuple, Optional
from collections import namedtuple
from pyasic.API.btminer import BTMinerAPI
from pyasic.miners import BaseMiner
from pyasic.API import APIError
from pyasic.data import MinerData
from pyasic.data.error_codes import WhatsminerError
from pyasic.config import MinerConfig
from pyasic.data import HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData, WhatsminerError
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
class BTMiner(BaseMiner):
def __init__(self, ip: str) -> None:
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip)
self.ip = ipaddress.ip_address(ip)
self.api = BTMinerAPI(ip)
self.api = BTMinerAPI(ip, api_ver)
self.api_type = "BTMiner"
self.api_ver = api_ver
async def get_model(self) -> Union[str, None]:
"""Get miner model.
async def _reset_api_pwd_to_admin(self, pwd: str):
try:
data = await self.api.update_pwd(pwd, "admin")
except APIError:
return False
if data:
if "Code" in data.keys():
if data["Code"] == 131:
return True
return False
Returns:
Miner model or None.
"""
async def fault_light_off(self) -> bool:
try:
data = await self.api.set_led(auto=True)
except APIError:
return False
if data:
if "Code" in data.keys():
if data["Code"] == 131:
self.light = False
return True
return False
async def fault_light_on(self) -> bool:
try:
data = await self.api.set_led(auto=False)
await self.api.set_led(
auto=False, color="green", start=0, period=1, duration=0
)
except APIError:
return False
if data:
if "Code" in data.keys():
if data["Code"] == 131:
self.light = True
return True
return False
async def reboot(self) -> bool:
try:
data = await self.api.reboot()
except APIError:
return False
if data.get("Msg"):
if data["Msg"] == "API command OK":
return True
return False
async def restart_backend(self) -> bool:
try:
data = await self.api.restart()
except APIError:
return False
if data.get("Msg"):
if data["Msg"] == "API command OK":
return True
return False
async def stop_mining(self) -> bool:
try:
data = await self.api.power_off(respbefore=True)
except APIError:
return False
if data.get("Msg"):
if data["Msg"] == "API command OK":
return True
return False
async def resume_mining(self) -> bool:
try:
data = await self.api.power_on()
except APIError:
return False
if data.get("Msg"):
if data["Msg"] == "API command OK":
return True
return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
conf = config.as_wm(user_suffix=user_suffix)
pools_conf = conf["pools"]
try:
await self.api.update_pools(
pools_conf[0]["url"],
pools_conf[0]["user"],
pools_conf[0]["pass"],
pools_conf[1]["url"],
pools_conf[1]["user"],
pools_conf[1]["pass"],
pools_conf[2]["url"],
pools_conf[2]["user"],
pools_conf[2]["pass"],
)
except APIError:
pass
try:
await self.api.adjust_power_limit(conf["wattage"])
except APIError:
# cannot set wattage
pass
async def get_config(self) -> MinerConfig:
pools = None
summary = None
cfg = MinerConfig()
try:
data = await self.api.multicommand("pools", "summary")
pools = data["pools"][0]
summary = data["summary"][0]
except APIError as e:
logging.warning(e)
if pools:
if "POOLS" in pools:
cfg = cfg.from_api(pools["POOLS"])
if summary:
if "SUMMARY" in summary:
if wattage := summary["SUMMARY"][0].get("Power Limit"):
cfg.autotuning_wattage = wattage
return cfg
async def set_power_limit(self, wattage: int) -> bool:
try:
await self.api.adjust_power_limit(wattage)
except Exception as e:
logging.warning(f"{self} set_power_limit: {e}")
return False
else:
return True
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_mac(
self, api_summary: dict = None, api_miner_info: dict = None
) -> Optional[str]:
if not api_miner_info:
try:
api_miner_info = await self.api.get_miner_info()
except APIError:
pass
if api_miner_info:
try:
mac = api_miner_info["Msg"]["mac"]
return str(mac).upper()
except KeyError:
pass
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
mac = api_summary["SUMMARY"][0]["MAC"]
return str(mac).upper()
except (KeyError, IndexError):
pass
async def get_model(self, api_devdetails: dict = None) -> Optional[str]:
if self.model:
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
version_data = await self.api.devdetails()
if version_data:
self.model = version_data["DEVDETAILS"][0]["Model"].split("V")[0]
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
if not api_devdetails:
try:
api_devdetails = await self.api.devdetails()
except APIError:
pass
if api_devdetails:
try:
self.model = api_devdetails["DEVDETAILS"][0]["Model"].split("V")[0]
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
except (TypeError, IndexError, KeyError):
pass
logging.warning(f"Failed to get model for miner: {self}")
return None
async def get_hostname(self) -> Union[str, None]:
"""Get miner hostname.
async def get_version(
self, api_version: dict = None, api_summary: dict = None
) -> Tuple[Optional[str], Optional[str]]:
# check if version is cached
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
# Check to see if the version info is already cached
if self.api_ver and self.fw_ver:
return miner_version(self.api_ver, self.fw_ver)
Returns:
The hostname of the miner as a string or None.
"""
if self.hostname:
return self.hostname
try:
host_data = await self.api.get_miner_info()
if host_data:
host = host_data["Msg"]["hostname"]
logging.debug(f"Found hostname for {self.ip}: {host}")
self.hostname = host
return self.hostname
except APIError:
logging.info(f"Failed to get hostname for miner: {self}")
return None
except Exception:
logging.warning(f"Failed to get hostname for miner: {self}")
return None
async def get_mac(self) -> str:
"""Get the mac address of the miner.
Returns:
The mac address of the miner as a string.
"""
mac = ""
data = await self.api.summary()
if data:
if data.get("SUMMARY"):
if len(data["SUMMARY"]) > 0:
_mac = data["SUMMARY"][0].get("MAC")
if _mac:
mac = _mac
if mac == "":
if not api_version:
try:
data = await self.api.get_miner_info()
if data:
if "Msg" in data.keys():
if "mac" in data["Msg"].keys():
mac = data["Msg"]["mac"]
api_version = await self.api.get_version()
except APIError:
pass
return str(mac).upper()
if api_version:
if "Code" in api_version.keys():
if api_version["Code"] == 131:
try:
api_ver = api_version["Msg"]
if not isinstance(api_ver, str):
api_ver = api_ver["api_ver"]
self.api_ver = api_ver.replace(
"whatsminer v", ""
)
self.fw_ver = api_version["Msg"]["fw_ver"]
except (KeyError, TypeError):
pass
else:
self.api.api_ver = self.api_ver
return miner_version(self.api_ver, self.fw_ver)
async def get_data(self) -> MinerData:
"""Get data from the miner.
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
"""
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
if api_summary:
try:
self.fw_ver = api_summary["SUMMARY"][0]["Firmware Version"].replace("'", "")
except (KeyError, IndexError):
pass
mac = None
return miner_version(self.api_ver, self.fw_ver)
try:
model = await self.get_model()
except APIError:
logging.info(f"Failed to get model: {self}")
model = None
data.model = "Whatsminer"
async def get_hostname(self, api_miner_info: dict = None) -> Optional[str]:
if self.hostname:
return self.hostname
try:
hostname = await self.get_hostname()
except APIError:
logging.info(f"Failed to get hostname: {self}")
hostname = None
data.hostname = "Whatsminer"
if not api_miner_info:
try:
api_miner_info = await self.api.get_miner_info()
except APIError:
return None # only one way to get this
if model:
data.model = model
if api_miner_info:
try:
self.hostname = api_miner_info["Msg"]["hostname"]
except KeyError:
return None
if hostname:
data.hostname = hostname
return self.hostname
data.fault_light = await self.check_light()
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
# get hr from API
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return round(float(api_summary["SUMMARY"][0]["MHS 1m"] / 1000000), 2)
except (KeyError, IndexError):
pass
async def get_hashboards(self, api_devs: dict = None) -> List[HashBoard]:
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
]
if not api_devs:
try:
api_devs = await self.api.devs()
except APIError:
pass
if api_devs:
try:
for board in api_devs["DEVS"]:
if len(hashboards) < board["ASC"] + 1:
hashboards.append(
HashBoard(
slot=board["ASC"], expected_chips=self.nominal_chips
)
)
self.ideal_hashboards += 1
hashboards[board["ASC"]].chip_temp = round(board["Chip Temp Avg"])
hashboards[board["ASC"]].temp = round(board["Temperature"])
hashboards[board["ASC"]].hashrate = round(
float(board["MHS 1m"] / 1000000), 2
)
hashboards[board["ASC"]].chips = board["Effective Chips"]
hashboards[board["ASC"]].missing = False
except (KeyError, IndexError):
pass
return hashboards
async def get_env_temp(self, api_summary: dict = None) -> Optional[float]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return api_summary["SUMMARY"][0]["Env Temp"]
except (KeyError, IndexError):
pass
async def get_wattage(self, api_summary: dict = None) -> Optional[int]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return api_summary["SUMMARY"][0]["Power"]
except (KeyError, IndexError):
pass
async def get_wattage_limit(self, api_summary: dict = None) -> Optional[int]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return api_summary["SUMMARY"][0]["Power Limit"]
except (KeyError, IndexError):
pass
async def get_fans(
self, api_summary: dict = None, api_psu: dict = None
) -> Tuple[
Tuple[Optional[int], Optional[int], Optional[int], Optional[int]],
Tuple[Optional[int]],
]:
fan_speeds = namedtuple("FanSpeeds", "fan_1 fan_2 fan_3 fan_4")
psu_fan_speeds = namedtuple("PSUFanSpeeds", "psu_fan")
miner_fan_speeds = namedtuple("MinerFans", "fan_speeds psu_fan_speeds")
fans = fan_speeds(None, None, None, None)
psu_fans = psu_fan_speeds(None)
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
if self.fan_count > 0:
fans = fan_speeds(
api_summary["SUMMARY"][0]["Fan Speed In"],
api_summary["SUMMARY"][0]["Fan Speed Out"],
None,
None,
)
psu_fans = psu_fan_speeds(
int(api_summary["SUMMARY"][0]["Power Fanspeed"])
)
except (KeyError, IndexError):
pass
if not psu_fans[0]:
if not api_psu:
try:
api_psu = await self.api.get_psu()
except APIError:
pass
if api_psu:
try:
psu_fans = psu_fan_speeds(int(api_psu["Msg"]["fan_speed"]))
except (KeyError, TypeError):
pass
return miner_fan_speeds(fans, psu_fans)
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
try:
pools = {}
for i, pool in enumerate(api_pools["POOLS"]):
pools[f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["User"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_errors(
self, api_summary: dict = None, api_error_codes: dict = None
) -> List[MinerErrorData]:
errors = []
if not api_summary and not api_error_codes:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
for i in range(api_summary["SUMMARY"][0]["Error Code Count"]):
errors.append(
WhatsminerError(
error_code=api_summary["SUMMARY"][0][f"Error Code {i}"]
)
)
except (KeyError, IndexError, ValueError, TypeError):
pass
if not api_error_codes:
try:
api_error_codes = await self.api.get_error_code()
except APIError:
pass
if api_error_codes:
for err in api_error_codes["Msg"]["error_code"]:
if isinstance(err, dict):
for code in err:
errors.append(WhatsminerError(error_code=int(code)))
else:
errors.append(WhatsminerError(error_code=int(err)))
return errors
async def get_fault_light(self, api_miner_info: dict = None) -> bool:
data = None
if not api_miner_info:
try:
api_miner_info = await self.api.get_miner_info()
except APIError:
if not self.light:
self.light = False
if api_miner_info:
try:
self.light = api_miner_info["Msg"]["ledstat"] == "auto"
except KeyError:
pass
return self.light if self.light else False
async def _get_data(self, allow_warning: bool) -> dict:
miner_data = None
for i in range(PyasicSettings().miner_get_data_retries):
try:
miner_data = await self.api.multicommand("summary", "devs", "pools")
if miner_data:
break
miner_data = await self.api.multicommand(
"summary",
"get_version",
"pools",
"devdetails",
"devs",
"get_psu",
"get_miner_info",
"get_error_code",
allow_warning=allow_warning,
)
except APIError:
pass
if miner_data:
break
if miner_data:
summary = miner_data.get("summary")
if summary:
summary = summary[0]
version = miner_data.get("get_version")
if version:
version = version[0]
pools = miner_data.get("pools")
if pools:
pools = pools[0]
devdetails = miner_data.get("devdetails")
if devdetails:
devdetails = devdetails[0]
devs = miner_data.get("devs")
if devs:
devs = devs[0]
psu = miner_data.get("get_psu")
if psu:
psu = psu[0]
miner_info = miner_data.get("get_miner_info")
if miner_info:
miner_info = miner_info[0]
error_codes = miner_data.get("get_error_codes")
if error_codes:
error_codes = error_codes[0]
else:
summary, version, pools, devdetails, devs, psu, miner_info, error_codes = (
None for _ in range(8)
)
if not miner_data:
return data
data = { # noqa - Ignore dictionary could be re-written
# ip - Done at start
# datetime - Done auto
"mac": await self.get_mac(api_summary=summary, api_miner_info=miner_info),
"model": await self.get_model(api_devdetails=devdetails),
# make - Done at start
"api_ver": None, # - Done at end
"fw_ver": None, # - Done at end
"hostname": await self.get_hostname(api_miner_info=miner_info),
"hashrate": await self.get_hashrate(api_summary=summary),
"hashboards": await self.get_hashboards(api_devs=devs),
# ideal_hashboards - Done at start
"env_temp": await self.get_env_temp(api_summary=summary),
"wattage": await self.get_wattage(api_summary=summary),
"wattage_limit": await self.get_wattage_limit(api_summary=summary),
"fan_1": None, # - Done at end
"fan_2": None, # - Done at end
"fan_3": None, # - Done at end
"fan_4": None, # - Done at end
"fan_psu": None, # - Done at end
# ideal_chips - Done at start
"pool_split": None, # - Done at end
"pool_1_url": None, # - Done at end
"pool_1_user": None, # - Done at end
"pool_2_url": None, # - Done at end
"pool_2_user": None, # - Done at end
"errors": await self.get_errors(
api_summary=summary, api_error_codes=error_codes
),
"fault_light": await self.get_fault_light(api_miner_info=miner_info),
}
summary = miner_data.get("summary")[0]
devs = miner_data.get("devs")[0]
pools = miner_data.get("pools")[0]
data["api_ver"], data["fw_ver"] = await self.get_version(api_version=version)
fan_data = await self.get_fans()
if summary:
summary_data = summary.get("SUMMARY")
if summary_data:
if len(summary_data) > 0:
wattage_limit = None
if summary_data[0].get("MAC"):
mac = summary_data[0]["MAC"]
if fan_data:
data["fan_1"] = fan_data.fan_speeds.fan_1 # noqa
data["fan_2"] = fan_data.fan_speeds.fan_2 # noqa
data["fan_3"] = fan_data.fan_speeds.fan_3 # noqa
data["fan_4"] = fan_data.fan_speeds.fan_4 # noqa
if summary_data[0].get("Env Temp"):
data.env_temp = summary_data[0]["Env Temp"]
data["fan_psu"] = fan_data.psu_fan_speeds.psu_fan # noqa
if summary_data[0].get("Power Limit"):
wattage_limit = summary_data[0]["Power Limit"]
pools_data = await self.get_pools(api_pools=pools)
data.fan_1 = summary_data[0]["Fan Speed In"]
data.fan_2 = summary_data[0]["Fan Speed Out"]
hr = summary_data[0].get("MHS 1m")
if hr:
data.hashrate = round(hr / 1000000, 2)
wattage = summary_data[0].get("Power")
if wattage:
data.wattage = round(wattage)
if not wattage_limit:
wattage_limit = round(wattage)
data.wattage_limit = wattage_limit
if summary_data[0].get("Error Code Count"):
for i in range(summary_data[0]["Error Code Count"]):
if summary_data[0].get(f"Error Code {i}"):
data.errors.append(
WhatsminerError(
error_code=summary_data[0][f"Error Code {i}"]
)
)
if devs:
temp_data = devs.get("DEVS")
if temp_data:
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
for board in temp_data:
_id = board["ASC"]
chip_temp = round(board["Chip Temp Avg"])
board_temp = round(board["Temperature"])
hashrate = round(board["MHS 1m"] / 1000000, 2)
setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
setattr(data, f"{board_map[_id]}_temp", board_temp)
setattr(data, f"{board_map[_id]}_hashrate", hashrate)
if devs:
boards = devs.get("DEVS")
if boards:
if len(boards) > 0:
board_map = {0: "left_chips", 1: "center_chips", 2: "right_chips"}
if "ID" in boards[0].keys():
id_key = "ID"
else:
id_key = "ASC"
offset = boards[0][id_key]
for board in boards:
_id = board[id_key] - offset
chips = board["Effective Chips"]
setattr(data, board_map[_id], chips)
if pools:
pool_1 = None
pool_2 = None
pool_1_user = None
pool_2_user = None
pool_1_quota = 1
pool_2_quota = 1
quota = 0
for pool in pools.get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
pool_1_quota = pool["Quota"]
elif not pool_2_user:
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if not pool.get("User") == pool_1_user:
if not pool_2_user == pool.get("User"):
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if pool_2_user and not pool_2_user == pool_1_user:
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_1_url = pool_1
if pool_1_user:
data.pool_1_user = pool_1_user
if pool_2:
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
if pool_2_user:
data.pool_2_user = pool_2_user
if quota:
data.pool_split = str(quota)
if not mac:
try:
mac = await self.get_mac()
except APIError:
logging.info(f"Failed to get mac: {self}")
mac = None
if mac:
data.mac = mac
if pools_data:
data["pool_1_url"] = pools_data[0]["pool_1_url"]
data["pool_1_user"] = pools_data[0]["pool_1_user"]
if len(pools_data) > 1:
data["pool_2_url"] = pools_data[1]["pool_2_url"]
data["pool_2_user"] = pools_data[1]["pool_2_user"]
data[
"pool_split"
] = f"{pools_data[0]['quota']}/{pools_data[1]['quota']}"
else:
try:
data["pool_2_url"] = pools_data[0]["pool_2_url"]
data["pool_2_user"] = pools_data[0]["pool_2_user"]
data["quota"] = "0"
except KeyError:
pass
return data

View File

@@ -14,85 +14,57 @@
import ipaddress
import logging
from typing import Union
from typing import List, Union, Tuple, Optional
from collections import namedtuple
import asyncssh
from pyasic.API.cgminer import CGMinerAPI
from pyasic.miners import BaseMiner
from pyasic.API import APIError
from pyasic.data import MinerData
from pyasic.config import MinerConfig
from pyasic.data import HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
class CGMiner(BaseMiner):
def __init__(self, ip: str) -> None:
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip)
self.ip = ipaddress.ip_address(ip)
self.api = CGMinerAPI(ip)
self.api = CGMinerAPI(ip, api_ver)
self.api_ver = api_ver
self.api_type = "CGMiner"
self.uname = "root"
self.pwd = "admin"
self.config = None
async def get_model(self) -> Union[str, None]:
"""Get miner model.
Returns:
Miner model or None.
"""
if self.model:
return self.model
try:
version_data = await self.api.devdetails()
except APIError:
return None
if version_data:
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
return self.model
return None
async def get_hostname(self) -> Union[str, None]:
"""Get miner hostname.
Returns:
The hostname of the miner as a string or "?"
"""
if self.hostname:
return self.hostname
try:
async with (await self._get_ssh_connection()) as conn:
if conn is not None:
data = await conn.run("cat /proc/sys/kernel/hostname")
host = data.stdout.strip()
self.hostname = host
return self.hostname
else:
return None
except Exception:
return None
async def send_ssh_command(self, cmd: str) -> Union[str, None]:
"""Send a command to the miner over ssh.
Parameters:
cmd: The command to run.
Returns:
Result of the command or None.
"""
async def send_ssh_command(self, cmd: str) -> Optional[str]:
result = None
async with (await self._get_ssh_connection()) as conn:
try:
conn = await self._get_ssh_connection()
except (asyncssh.Error, OSError):
return None
# open an ssh connection
async with conn:
# 3 retries
for i in range(3):
try:
# run the command and get the result
result = await conn.run(cmd)
result = result.stdout
except Exception as e:
print(f"{cmd} error: {e}")
# if the command fails, log it
logging.warning(f"{self} command {cmd} error: {e}")
# on the 3rd retry, return None
if i == 3:
return
continue
# return the result, either command output or None
return result
async def restart_backend(self) -> bool:
@@ -103,107 +75,177 @@ class CGMiner(BaseMiner):
"""Restart cgminer hashing process."""
commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"]
commands = ";".join(commands)
_ret = await self.send_ssh_command(commands)
if isinstance(_ret, str):
return True
try:
_ret = await self.send_ssh_command(commands)
except (asyncssh.Error, OSError):
return False
else:
if isinstance(_ret, str):
return True
return False
async def reboot(self) -> bool:
"""Reboots power to the physical miner."""
logging.debug(f"{self}: Sending reboot command.")
_ret = await self.send_ssh_command("reboot")
logging.debug(f"{self}: Reboot command completed.")
if isinstance(_ret, str):
return True
try:
_ret = await self.send_ssh_command("reboot")
except (asyncssh.Error, OSError):
return False
else:
logging.debug(f"{self}: Reboot command completed.")
if isinstance(_ret, str):
return True
return False
async def start_cgminer(self) -> None:
"""Start cgminer hashing process."""
commands = [
"mkdir -p /etc/tmp/",
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
"crontab -u root /etc/tmp/root",
"/usr/bin/cgminer-monitor >/dev/null 2>&1",
]
commands = ";".join(commands)
await self.send_ssh_command(commands)
async def resume_mining(self) -> bool:
try:
commands = [
"mkdir -p /etc/tmp/",
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
"crontab -u root /etc/tmp/root",
"/usr/bin/cgminer-monitor >/dev/null 2>&1",
]
commands = ";".join(commands)
await self.send_ssh_command(commands)
except (asyncssh.Error, OSError):
return False
else:
return True
async def stop_cgminer(self) -> None:
"""Restart cgminer hashing process."""
commands = [
"mkdir -p /etc/tmp/",
'echo "" > /etc/tmp/root',
"crontab -u root /etc/tmp/root",
"killall cgminer",
]
commands = ";".join(commands)
await self.send_ssh_command(commands)
async def stop_mining(self) -> bool:
try:
commands = [
"mkdir -p /etc/tmp/",
'echo "" > /etc/tmp/root',
"crontab -u root /etc/tmp/root",
"killall cgminer",
]
commands = ";".join(commands)
await self.send_ssh_command(commands)
except (asyncssh.Error, OSError):
return False
else:
return True
async def get_config(self) -> str:
"""Gets the config for the miner and sets it as `self.config`.
async def get_config(self, api_pools: dict = None) -> MinerConfig:
# get pool data
try:
api_pools = await self.api.pools()
except APIError:
pass
Returns:
The config from `self.config`.
"""
async with (await self._get_ssh_connection()) as conn:
command = "cat /etc/config/cgminer"
result = await conn.run(command, check=True)
self.config = result.stdout
if api_pools:
self.config = MinerConfig().from_api(api_pools["POOLS"])
return self.config
async def get_data(self) -> MinerData:
"""Get data from the miner.
async def fault_light_off(self) -> bool:
return False
Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
"""
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
async def fault_light_on(self) -> bool:
return False
board_offset = -1
fan_offset = -1
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
async def set_power_limit(self, wattage: int) -> bool:
return False
if model:
data.model = model
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
if hostname:
data.hostname = hostname
async def get_mac(self) -> Optional[str]:
return None
if mac:
data.mac = mac
async def get_model(self, api_devdetails: dict = None) -> Optional[str]:
if self.model:
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
data.fault_light = await self.check_light()
if not api_devdetails:
try:
api_devdetails = await self.api.devdetails()
except APIError:
pass
miner_data = None
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True
)
if miner_data:
break
if api_devdetails:
try:
self.model = api_devdetails["DEVDETAILS"][0]["Model"].replace(
"Antminer ", ""
)
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
except (TypeError, IndexError, KeyError):
pass
if not miner_data:
return data
logging.warning(f"Failed to get model for miner: {self}")
return None
summary = miner_data.get("summary")[0]
pools = miner_data.get("pools")[0]
stats = miner_data.get("stats")[0]
async def get_version(
self, api_version: dict = None
) -> Tuple[Optional[str], Optional[str]]:
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
# Check to see if the version info is already cached
if self.api_ver and self.fw_ver:
return miner_version(self.api_ver, self.fw_ver)
if summary:
hr = summary.get("SUMMARY")
if hr:
if len(hr) > 0:
hr = hr[0].get("GHS av")
if hr:
data.hashrate = round(hr / 1000, 2)
if not api_version:
try:
api_version = await self.api.version()
except APIError:
pass
if stats:
boards = stats.get("STATS")
if boards:
if len(boards) > 0:
if api_version:
try:
self.api_ver = api_version["VERSION"][0]["API"]
except (KeyError, IndexError):
pass
try:
self.fw_ver = api_version["VERSION"][0]["CGMiner"]
except (KeyError, IndexError):
pass
return miner_version(self.api_ver, self.fw_ver)
async def get_hostname(self) -> Optional[str]:
try:
hn = await self.send_ssh_command("cat /proc/sys/kernel/hostname")
except (asyncssh.Error, OSError):
return None
if hn:
self.hostname = hn
return self.hostname
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
# get hr from API
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return round(
float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2
)
except (IndexError, KeyError, ValueError, TypeError):
pass
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
hashboards = []
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
board_offset = -1
boards = api_stats["STATS"]
if len(boards) > 1:
for board_num in range(1, 16, 5):
for _b_num in range(5):
b = boards[1].get(f"chain_acn{board_num + _b_num}")
@@ -213,96 +255,216 @@ class CGMiner(BaseMiner):
if board_offset == -1:
board_offset = 1
data.left_chips = boards[1].get(f"chain_acn{board_offset}")
data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
data.left_board_hashrate = round(
float(boards[1].get(f"chain_rate{board_offset}")) / 1000, 2
)
data.center_board_hashrate = round(
float(boards[1].get(f"chain_rate{board_offset+1}")) / 1000, 2
)
data.right_board_hashrate = round(
float(boards[1].get(f"chain_rate{board_offset+2}")) / 1000, 2
)
if stats:
temp = stats.get("STATS")
if temp:
if len(temp) > 1:
for fan_num in range(1, 8, 4):
for _f_num in range(4):
f = temp[1].get(f"fan{fan_num + _f_num}")
if f and not f == 0 and fan_offset == -1:
fan_offset = fan_num
if fan_offset == -1:
fan_offset = 1
for fan in range(self.fan_count):
setattr(
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
for i in range(board_offset, board_offset + self.ideal_hashboards):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
)
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
env_temp_list = []
for item in range(3):
board_temp = temp[1].get(f"temp{item + board_offset}")
chip_temp = temp[1].get(f"temp2_{item + board_offset}")
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
setattr(data, f"{board_map[item]}_temp", board_temp)
if f"temp_pcb{item}" in temp[1].keys():
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
if not env_temp == 0:
env_temp_list.append(int(env_temp))
data.env_temp = sum(env_temp_list) / len(env_temp_list)
chip_temp = boards[1].get(f"temp{i}")
if chip_temp:
hashboard.chip_temp = round(chip_temp)
if pools:
pool_1 = None
pool_2 = None
pool_1_user = None
pool_2_user = None
pool_1_quota = 1
pool_2_quota = 1
quota = 0
for pool in pools.get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
if pool.get("Quota"):
pool_2_quota = pool.get("Quota")
elif not pool_2_user:
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
if pool.get("Quota"):
pool_2_quota = pool.get("Quota")
if not pool.get("User") == pool_1_user:
if not pool_2_user == pool.get("User"):
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
if pool.get("Quota"):
pool_2_quota = pool.get("Quota")
if pool_2_user and not pool_2_user == pool_1_user:
quota = f"{pool_1_quota}/{pool_2_quota}"
temp = boards[1].get(f"temp2_{i}")
if temp:
hashboard.temp = round(temp)
if pool_1:
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
hashrate = boards[1].get(f"chain_rate{i}")
if hashrate:
hashboard.hashrate = round(float(hashrate) / 1000, 2)
chips = boards[1].get(f"chain_acn{i}")
if chips:
hashboard.chips = chips
hashboard.missing = False
if (not chips) or (not chips > 0):
hashboard.missing = True
hashboards.append(hashboard)
except (IndexError, KeyError, ValueError, TypeError):
pass
return hashboards
async def get_env_temp(self) -> Optional[float]:
return None
async def get_wattage(self) -> Optional[int]:
return None
async def get_wattage_limit(self) -> Optional[int]:
return None
async def get_fans(
self, api_stats: dict = None
) -> Tuple[
Tuple[Optional[int], Optional[int], Optional[int], Optional[int]],
Tuple[Optional[int]],
]:
fan_speeds = namedtuple("FanSpeeds", "fan_1 fan_2 fan_3 fan_4")
psu_fan_speeds = namedtuple("PSUFanSpeeds", "psu_fan")
miner_fan_speeds = namedtuple("MinerFans", "fan_speeds psu_fan_speeds")
psu_fans = psu_fan_speeds(None)
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
fans_data = [None, None, None, None]
if api_stats:
try:
fan_offset = -1
for fan_num in range(1, 8, 4):
for _f_num in range(4):
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}")
if f and not f == 0 and fan_offset == -1:
fan_offset = fan_num
if fan_offset == -1:
fan_offset = 1
for fan in range(self.fan_count):
fans_data[fan] = api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
except (KeyError, IndexError):
pass
fans = fan_speeds(*fans_data)
return miner_fan_speeds(fans, psu_fans)
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
try:
pools = {}
for i, pool in enumerate(api_pools["POOLS"]):
pools[f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["User"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_errors(self) -> List[MinerErrorData]:
return []
async def get_fault_light(self) -> bool:
return False
async def _get_data(self, allow_warning: bool) -> dict:
miner_data = None
for i in range(PyasicSettings().miner_get_data_retries):
try:
miner_data = await self.api.multicommand(
"summary",
"pools",
"devdetails",
"stats",
allow_warning=allow_warning,
)
data.pool_1_url = pool_1
except APIError:
try:
miner_data = await self.api.multicommand(
"summary",
"pools",
"version",
"stats",
allow_warning=allow_warning,
)
except APIError:
pass
if miner_data:
break
if miner_data:
summary = miner_data.get("summary")
if summary:
summary = summary[0]
pools = miner_data.get("pools")
if pools:
pools = pools[0]
version = miner_data.get("version")
if version:
version = version[0]
devdetails = miner_data.get("devdetails")
if devdetails:
devdetails = devdetails[0]
stats = miner_data.get("stats")
if stats:
stats = stats[0]
else:
summary, pools, devdetails, version, stats = (None for _ in range(5))
if pool_1_user:
data.pool_1_user = pool_1_user
data = { # noqa - Ignore dictionary could be re-written
# ip - Done at start
# datetime - Done auto
"mac": await self.get_mac(),
"model": await self.get_model(api_devdetails=devdetails),
# make - Done at start
"api_ver": None, # - Done at end
"fw_ver": None, # - Done at end
"hostname": await self.get_hostname(),
"hashrate": await self.get_hashrate(api_summary=summary),
"hashboards": await self.get_hashboards(api_stats=stats),
# ideal_hashboards - Done at start
"env_temp": await self.get_env_temp(),
"wattage": await self.get_wattage(),
"wattage_limit": await self.get_wattage_limit(),
"fan_1": None, # - Done at end
"fan_2": None, # - Done at end
"fan_3": None, # - Done at end
"fan_4": None, # - Done at end
"fan_psu": None, # - Done at end
# ideal_chips - Done at start
"pool_split": None, # - Done at end
"pool_1_url": None, # - Done at end
"pool_1_user": None, # - Done at end
"pool_2_url": None, # - Done at end
"pool_2_user": None, # - Done at end
"errors": await self.get_errors(),
"fault_light": await self.get_fault_light(),
}
if pool_2:
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
data["api_ver"], data["fw_ver"] = await self.get_version(api_version=version)
fan_data = await self.get_fans()
if pool_2_user:
data.pool_2_user = pool_2_user
if fan_data:
data["fan_1"] = fan_data.fan_speeds.fan_1 # noqa
data["fan_2"] = fan_data.fan_speeds.fan_2 # noqa
data["fan_3"] = fan_data.fan_speeds.fan_3 # noqa
data["fan_4"] = fan_data.fan_speeds.fan_4 # noqa
if quota:
data.pool_split = str(quota)
data["fan_psu"] = fan_data.psu_fan_speeds.psu_fan # noqa
pools_data = await self.get_pools(api_pools=pools)
if pools_data:
data["pool_1_url"] = pools_data[0]["pool_1_url"]
data["pool_1_user"] = pools_data[0]["pool_1_user"]
if len(pools_data) > 1:
data["pool_2_url"] = pools_data[1]["pool_2_url"]
data["pool_2_user"] = pools_data[1]["pool_2_user"]
data[
"pool_split"
] = f"{pools_data[0]['quota']}/{pools_data[1]['quota']}"
else:
try:
data["pool_2_url"] = pools_data[0]["pool_2_url"]
data["pool_2_user"] = pools_data[0]["pool_2_user"]
data["quota"] = "0"
except KeyError:
pass
return data

View File

@@ -0,0 +1,378 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ipaddress
import logging
from typing import List, Union, Tuple, Optional
from collections import namedtuple
import re
from pyasic.API.cgminer import CGMinerAPI
from pyasic.config import MinerConfig
from pyasic.data import HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
from pyasic.miners._backends import CGMiner
class CGMinerAvalon(CGMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver)
self.ip = ip
async def fault_light_on(self) -> bool:
try:
data = await self.api.ascset(0, "led", "1-1")
except APIError:
return False
if data["STATUS"][0]["Msg"] == "ASC 0 set OK":
return True
return False
async def fault_light_off(self) -> bool:
try:
data = await self.api.ascset(0, "led", "1-0")
except APIError:
return False
if data["STATUS"][0]["Msg"] == "ASC 0 set OK":
return True
return False
async def reboot(self) -> bool:
try:
data = await self.api.restart()
except APIError:
return False
try:
if data["STATUS"] == "RESTART":
return True
except KeyError:
return False
return False
async def stop_mining(self) -> bool:
return False
async def resume_mining(self) -> bool:
return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
return None
logging.debug(f"{self}: Sending config.") # noqa - This doesnt work...
conf = config.as_avalon(user_suffix=user_suffix)
try:
data = await self.api.ascset(
0, "setpool", f"root,root,{conf}"
) # this should work but doesn't
except APIError:
pass
# return data
@staticmethod
def parse_stats(stats):
_stats_items = re.findall(".+?\[*?]", stats)
stats_items = []
stats_dict = {}
for item in _stats_items:
if ":" in item:
data = item.replace("]", "").split("[")
data_list = [i.split(": ") for i in data[1].strip().split(", ")]
data_dict = {}
for key, val in [tuple(item) for item in data_list]:
data_dict[key] = val
raw_data = [data[0].strip(), data_dict]
else:
raw_data = [
value
for value in item.replace("[", " ")
.replace("]", " ")
.split(" ")[:-1]
if value != ""
]
if len(raw_data) == 1:
raw_data.append("")
if raw_data[0] == "":
raw_data = raw_data[1:]
if len(raw_data) == 2:
stats_dict[raw_data[0]] = raw_data[1]
else:
stats_dict[raw_data[0]] = raw_data[1:]
stats_items.append(raw_data)
return stats_dict
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_mac(self, api_version: dict = None) -> Optional[str]:
if not api_version:
try:
api_version = await self.api.version()
except APIError:
pass
if api_version:
try:
base_mac = api_version["VERSION"][0]["MAC"]
base_mac = base_mac.upper()
mac = ":".join(
[base_mac[i : (i + 2)] for i in range(0, len(base_mac), 2)]
)
return mac
except (KeyError, ValueError):
pass
async def get_hostname(self, mac: str = None) -> Optional[str]:
if not mac:
mac = await self.get_mac()
if mac:
return f"Avalon{mac.replace(':', '')[-6:]}"
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return round(float(api_summary["SUMMARY"][0]["MHS 1m"] / 1000000), 2)
except (KeyError, IndexError, ValueError, TypeError):
pass
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
]
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
stats_data = api_stats[0].get("STATS")
if stats_data:
for key in stats_data[0].keys():
if key.startswith("MM ID"):
raw_data = self.parse_stats(stats_data[0][key])
for board in range(self.ideal_hashboards):
chip_temp = raw_data.get("MTmax")
if chip_temp:
hashboards[board].chip_temp = chip_temp[board]
temp = raw_data.get("MTavg")
if temp:
hashboards[board].temp = temp[board]
chips = raw_data.get(f"PVT_T{board}")
if chips:
hashboards[board].chips = len(
[item for item in chips if not item == "0"]
)
except (IndexError, KeyError, ValueError, TypeError):
pass
return hashboards
async def get_env_temp(self) -> Optional[float]:
return None
async def get_wattage(self) -> Optional[int]:
return None
async def get_wattage_limit(self) -> Optional[int]:
return None
async def get_fans(
self, api_stats: dict = None
) -> Tuple[
Tuple[Optional[int], Optional[int], Optional[int], Optional[int]],
Tuple[Optional[int]],
]:
fan_speeds = namedtuple("FanSpeeds", "fan_1 fan_2 fan_3 fan_4")
psu_fan_speeds = namedtuple("PSUFanSpeeds", "psu_fan")
miner_fan_speeds = namedtuple("MinerFans", "fan_speeds psu_fan_speeds")
psu_fans = psu_fan_speeds(None)
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
fans_data = [None, None, None, None]
if api_stats:
try:
stats_data = api_stats[0].get("STATS")
if stats_data:
for key in stats_data[0].keys():
if key.startswith("MM ID"):
raw_data = self.parse_stats(stats_data[0][key])
for fan in range(self.fan_count):
fans_data[fan] = int(raw_data[f"Fan{fan + 1}"])
except (KeyError, IndexError, ValueError, TypeError):
pass
fans = fan_speeds(*fans_data)
return miner_fan_speeds(fans, psu_fans)
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
try:
pools = {}
for i, pool in enumerate(api_pools["POOLS"]):
pools[f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["User"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_errors(self) -> List[MinerErrorData]:
return []
async def get_fault_light(self) -> bool:
if self.light:
return self.light
try:
data = await self.api.ascset(0, "led", "1-255")
except APIError:
return False
if data["STATUS"][0]["Msg"] == "ASC 0 set info: LED[1]":
return True
return False
async def _get_data(self, allow_warning: bool) -> dict:
miner_data = None
for i in range(PyasicSettings().miner_get_data_retries):
try:
miner_data = await self.api.multicommand(
"summary",
"pools",
"version",
"devdetails",
"stats",
allow_warning=allow_warning,
)
except APIError:
pass
if miner_data:
break
if miner_data:
summary = miner_data.get("summary")
if summary:
summary = summary[0]
pools = miner_data.get("pools")
if pools:
pools = pools[0]
version = miner_data.get("version")
if version:
version = version[0]
devdetails = miner_data.get("devdetails")
if devdetails:
devdetails = devdetails[0]
stats = miner_data.get("stats")
if stats:
stats = stats[0]
else:
summary, pools, devdetails, version, stats = (None for _ in range(5))
data = { # noqa - Ignore dictionary could be re-written
# ip - Done at start
# datetime - Done auto
"mac": await self.get_mac(),
"model": await self.get_model(api_devdetails=devdetails),
# make - Done at start
"api_ver": None, # - Done at end
"fw_ver": None, # - Done at end
"hostname": await self.get_hostname(),
"hashrate": await self.get_hashrate(api_summary=summary),
"hashboards": await self.get_hashboards(api_stats=stats),
# ideal_hashboards - Done at start
"env_temp": await self.get_env_temp(),
"wattage": await self.get_wattage(),
"wattage_limit": await self.get_wattage_limit(),
"fan_1": None, # - Done at end
"fan_2": None, # - Done at end
"fan_3": None, # - Done at end
"fan_4": None, # - Done at end
"fan_psu": None, # - Done at end
# ideal_chips - Done at start
"pool_split": None, # - Done at end
"pool_1_url": None, # - Done at end
"pool_1_user": None, # - Done at end
"pool_2_url": None, # - Done at end
"pool_2_user": None, # - Done at end
"errors": await self.get_errors(),
"fault_light": await self.get_fault_light(),
}
data["api_ver"], data["fw_ver"] = await self.get_version(api_version=version)
fan_data = await self.get_fans()
if fan_data:
data["fan_1"] = fan_data.fan_speeds.fan_1 # noqa
data["fan_2"] = fan_data.fan_speeds.fan_2 # noqa
data["fan_3"] = fan_data.fan_speeds.fan_3 # noqa
data["fan_4"] = fan_data.fan_speeds.fan_4 # noqa
data["fan_psu"] = fan_data.psu_fan_speeds.psu_fan # noqa
pools_data = await self.get_pools(api_pools=pools)
if pools_data:
data["pool_1_url"] = pools_data[0]["pool_1_url"]
data["pool_1_user"] = pools_data[0]["pool_1_user"]
if len(pools_data) > 1:
data["pool_2_url"] = pools_data[1]["pool_2_url"]
data["pool_2_user"] = pools_data[1]["pool_2_user"]
data[
"pool_split"
] = f"{pools_data[0]['quota']}/{pools_data[1]['quota']}"
else:
try:
data["pool_2_url"] = pools_data[0]["pool_2_url"]
data["pool_2_user"] = pools_data[0]["pool_2_user"]
data["quota"] = "0"
except KeyError:
pass
return data

View File

@@ -12,51 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners._backends import BMMiner
import ipaddress
from pyasic.miners._backends import BMMiner
class Hiveon(BMMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver)
self.ip = ipaddress.ip_address(ip)
self.api_type = "Hiveon"
self.uname = "root"
self.pwd = "admin"
async def get_board_info(self) -> dict:
"""Gets data on each board and chain in the miner."""
board_stats = await self.api.stats()
stats = board_stats["STATS"][1]
boards = {}
board_chains = {0: [2, 9, 10], 1: [3, 11, 12], 2: [4, 13, 14]}
for idx, board in enumerate(board_chains):
boards[board] = []
for chain in board_chains[board]:
count = stats[f"chain_acn{chain}"]
chips = stats[f"chain_acs{chain}"].replace(" ", "")
if not count == 18 or "x" in chips:
nominal = False
else:
nominal = True
boards[board].append(
{
"chain": chain,
"chip_count": count,
"chip_status": chips,
"nominal": nominal,
}
)
return boards
async def get_bad_boards(self) -> dict:
"""Checks for and provides list of non working boards."""
boards = await self.get_board_info()
bad_boards = {}
for board in boards.keys():
for chain in boards[board]:
if not chain["chip_count"] == 18 or "x" in chain["chip_status"]:
if board not in bad_boards.keys():
bad_boards[board] = []
bad_boards[board].append(chain)
return bad_boards

View File

@@ -14,4 +14,5 @@
from .antminer import *
from .avalonminer import *
from .innosilicon import *
from .whatsminer import *

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S17(BaseMiner):
class S17(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S17Plus(BaseMiner):
class S17Plus(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S17Pro(BaseMiner):
class S17Pro(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S17e(BaseMiner):
class S17e(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class T17(BaseMiner):
class T17(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class T17Plus(BaseMiner):
class T17Plus(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class T17e(BaseMiner):
class T17e(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -16,7 +16,6 @@ from .S17 import S17
from .S17_Plus import S17Plus
from .S17_Pro import S17Pro
from .S17e import S17e
from .T17 import T17
from .T17_Plus import T17Plus
from .T17e import T17e

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S19(BaseMiner):
class S19(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S19Pro(BaseMiner):
class S19Pro(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

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

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S19a(BaseMiner):
class S19a(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

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

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S19j(BaseMiner):
class S19j(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S19jPro(BaseMiner):
class S19jPro(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class T19(BaseMiner):
class T19(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -14,10 +14,9 @@
from .S19 import S19
from .S19_Pro import S19Pro
from .S19_XP import S19XP
from .S19a import S19a
from .S19a_Pro import S19aPro
from .S19j import S19j
from .S19j_Pro import S19jPro
from .S19a import S19a
from .T19 import T19

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S9(BaseMiner):
class S9(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class S9i(BaseMiner):
class S9i(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,13 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AntMiner
class T9(BaseMiner):
class T9(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "T9"
self.nominal_chips = 57
self.nominal_chips = 54
self.fan_count = 2

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon1026(BaseMiner):
class Avalon1026(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon1047(BaseMiner):
class Avalon1047(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon1066(BaseMiner):
class Avalon1066(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,13 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon721(BaseMiner):
class Avalon721(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "Avalon 721"
self.chip_count = 18 # This miner has 4 boards totaling 72
self.fan_count = 1 # also only 1 fan
self.ideal_hashboards = 4
self.chip_count = 18
self.fan_count = 1

View File

@@ -12,13 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon741(BaseMiner):
class Avalon741(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "Avalon 741"
self.chip_count = 22 # This miner has 4 boards totaling 88
self.fan_count = 1 # also only 1 fan
self.ideal_hashboards = 4
self.chip_count = 22
self.fan_count = 1

View File

@@ -12,13 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon761(BaseMiner):
class Avalon761(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "Avalon 761"
self.chip_count = 18 # This miner has 4 boards totaling 72
self.fan_count = 1 # also only 1 fan
self.ideal_hashboards = 4
self.chip_count = 18
self.fan_count = 1

View File

@@ -12,13 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon821(BaseMiner):
class Avalon821(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "Avalon 821"
self.chip_count = 26 # This miner has 4 boards totaling 104
self.fan_count = 1 # also only 1 fan
self.ideal_hashboards = 4
self.chip_count = 26
self.fan_count = 1

View File

@@ -12,13 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon841(BaseMiner):
class Avalon841(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "Avalon 841"
self.chip_count = 26 # This miner has 4 boards totaling 104
self.fan_count = 1 # also only 1 fan
self.ideal_hashboards = 4
self.chip_count = 26
self.fan_count = 1

View File

@@ -12,13 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon851(BaseMiner):
class Avalon851(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "Avalon 851"
self.chip_count = 26 # This miner has 4 boards totaling 104
self.fan_count = 1 # also only 1 fan
self.ideal_hashboards = 4
self.chip_count = 26
self.fan_count = 1

View File

@@ -12,13 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import AvalonMiner
class Avalon921(BaseMiner):
class Avalon921(AvalonMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "Avalon 921"
self.chip_count = 26 # This miner has 4 boards totaling 104
self.fan_count = 1 # also only 1 fan
self.ideal_hashboards = 4
self.chip_count = 26
self.fan_count = 1

View File

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

View File

@@ -12,7 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
if __name__ == "__main__":
unittest.main()
from .T3H_Plus import InnosiliconT3HPlus

View File

@@ -0,0 +1,15 @@
# 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 .T3X import *

View File

@@ -0,0 +1,39 @@
# 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.base import BaseMiner
class WhatsMiner(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self):
super().__init__()
self.make = "WhatsMiner"
class AntMiner(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self):
super().__init__()
self.make = "AntMiner"
class AvalonMiner(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self):
super().__init__()
self.make = "AvalonMiner"
class InnosiliconMiner(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self):
super().__init__()
self.make = "Innosilicon"

View File

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

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M20S(BaseMiner):
class M20S(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -24,19 +24,19 @@ class M20S(BaseMiner):
self.fan_count = 2
class M20SV10(BaseMiner):
class M20SV10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20S"
self.model = "M20S V10"
self.nominal_chips = 105
self.fan_count = 2
class M20SV20(BaseMiner):
class M20SV20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20S"
self.model = "M20S V20"
self.nominal_chips = 111
self.fan_count = 2

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M20SPlus(BaseMiner):
class M20SPlus(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M21(BaseMiner):
class M21(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M21S(BaseMiner):
class M21S(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -24,7 +24,7 @@ class M21S(BaseMiner):
self.fan_count = 2
class M21SV60(BaseMiner):
class M21SV60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -33,7 +33,7 @@ class M21SV60(BaseMiner):
self.fan_count = 2
class M21SV20(BaseMiner):
class M21SV20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M21SPlus(BaseMiner):
class M21SPlus(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .M20 import M20, M20V10
from .M20S import M20S, M20SV10, M20SV20
from .M20S_Plus import M20SPlus
from .M21 import M21
from .M21S import M21S, M21SV20, M21SV60
from .M21S_Plus import M21SPlus

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M30S(BaseMiner):
class M30S(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -24,7 +24,7 @@ class M30S(BaseMiner):
self.fan_count = 2
class M30SV50(BaseMiner):
class M30SV50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -33,7 +33,7 @@ class M30SV50(BaseMiner):
self.fan_count = 2
class M30SVG20(BaseMiner):
class M30SVG20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -42,7 +42,7 @@ class M30SVG20(BaseMiner):
self.fan_count = 2
class M30SVE20(BaseMiner):
class M30SVE20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -51,10 +51,19 @@ class M30SVE20(BaseMiner):
self.fan_count = 2
class M30SVE10(BaseMiner):
class M30SVE10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VE10"
self.nominal_chips = 105
self.fan_count = 2
class M30SVG10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VG10"
self.nominal_chips = 66
self.fan_count = 2

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M30SPlus(BaseMiner):
class M30SPlus(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -24,7 +24,7 @@ class M30SPlus(BaseMiner):
self.fan_count = 2
class M30SPlusVG60(BaseMiner):
class M30SPlusVG60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -33,7 +33,16 @@ class M30SPlusVG60(BaseMiner):
self.fan_count = 2
class M30SPlusVE40(BaseMiner):
class M30SPlusVG40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VG40"
self.nominal_chips = 105
self.fan_count = 2
class M30SPlusVE40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -42,10 +51,26 @@ class M30SPlusVE40(BaseMiner):
self.fan_count = 2
class M30SPlusVF20(BaseMiner):
class M30SPlusVF20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VF20"
self.nominal_chips = 111
self.fan_count = 2
class M30SPlusVH30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VH30"
self.nominal_chips = 70
self.fan_count = 2
class M30SPlusVH60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VH60"
self.nominal_chips = 66
self.fan_count = 2

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M30SPlusPlus(BaseMiner):
class M30SPlusPlus(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -24,19 +24,28 @@ class M30SPlusPlus(BaseMiner):
self.fan_count = 2
class M30SPlusPlusVG30(BaseMiner):
class M30SPlusPlusVG30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ V30"
self.model = "M30S++ VG30"
self.nominal_chips = 111
self.fan_count = 2
class M30SPlusPlusVG40(BaseMiner):
class M30SPlusPlusVG40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ V40"
self.model = "M30S++ VG40"
self.nominal_chips = 117
self.fan_count = 2
class M30SPlusPlusVH60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH60"
self.nominal_chips = 78
self.fan_count = 2

View File

@@ -12,13 +12,49 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M31S(BaseMiner):
class M31S(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S"
# TODO: Add chip count for this miner (per board) - self.nominal_chips
self.nominal_chips = 111
self.fan_count = 2
class M31SV10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V10"
self.nominal_chips = 105
self.fan_count = 2
class M31SV20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V20"
self.nominal_chips = 111
self.fan_count = 2
class M31SV60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V60"
self.nominal_chips = 105
self.fan_count = 2
class M31SV70(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V70"
self.nominal_chips = 111
self.fan_count = 2

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M31SPlus(BaseMiner):
class M31SPlus(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -24,10 +24,55 @@ class M31SPlus(BaseMiner):
self.fan_count = 2
class M31SPlusVE20(BaseMiner):
class M31SPlusVE20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VE20"
self.nominal_chips = 78
self.fan_count = 2
class M31SPlusV30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V30"
self.nominal_chips = 117
self.fan_count = 2
class M31SPlusV40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V40"
self.nominal_chips = 123
self.fan_count = 2
class M31SPlusV60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V60"
self.nominal_chips = 156
self.fan_count = 2
class M31SPlusV80(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V80"
self.nominal_chips = 129
self.fan_count = 2
class M31SPlusV90(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V90"
self.nominal_chips = 117
self.fan_count = 2

View File

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

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners._types.makes import WhatsMiner
class M32S(BaseMiner):
class M32S(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

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._types.makes import WhatsMiner
class M34SPlus(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M34S+"
self.ideal_hashboards = 4
self.nominal_chips = 116
self.fan_count = 0
class M34SPlusVE10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M34S+ VE10"
self.ideal_hashboards = 4
self.nominal_chips = 116
self.fan_count = 0

View File

@@ -12,11 +12,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .M30S import M30S, M30SVE10, M30SVE20, M30SVG20, M30SV50
from .M30S_Plus import M30SPlus, M30SPlusVG60, M30SPlusVE40, M30SPlusVF20
from .M30S_Plus_Plus import M30SPlusPlus, M30SPlusPlusVG30, M30SPlusPlusVG40
from .M31S import M31S
from .M31S_Plus import M31SPlus, M31SPlusVE20
from .M30S import M30S, M30SV50, M30SVE10, M30SVE20, M30SVG20, M30SVG10
from .M30S_Plus import M30SPlus, M30SPlusVE40, M30SPlusVF20, M30SPlusVG60, M30SPlusVG40, M30SPlusVH30, M30SPlusVH60
from .M30S_Plus_Plus import (
M30SPlusPlus,
M30SPlusPlusVG30,
M30SPlusPlusVG40,
M30SPlusPlusVH60,
)
from .M31S import M31S, M31SV10, M31SV20, M31SV60, M31SV70
from .M31S_Plus import (
M31SPlus,
M31SPlusV30,
M31SPlusV40,
M31SPlusV60,
M31SPlusV80,
M31SPlusV90,
M31SPlusVE20,
)
from .M32 import M32, M32V20
from .M32S import M32S
from .M34S_Plus import M34SPlus, M34SPlusVE10

View File

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

View File

@@ -0,0 +1,15 @@
# 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 .M50 import M50, M50VH50

View File

@@ -14,3 +14,4 @@
from .M2X import *
from .M3X import *
from .M5X import *

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