Compare commits

..

248 Commits

Author SHA1 Message Date
UpstreamData
b32649435d version: bump version number. 2023-02-09 15:48:49 -07:00
UpstreamData
c0096126df bug: Reverse check for whatsminer fault light as it was backwards. 2023-02-09 15:47:11 -07:00
UpstreamData
d632360932 version: bump version number. 2023-02-09 10:37:19 -07:00
UpstreamData
400001fa38 version: bump version number. 2023-02-07 13:41:06 -07:00
UpstreamData
4ff32a8081 feature: allow get_data to pick which data to gather in the first place to allow for speed optimizations. 2023-02-07 13:40:09 -07:00
UpstreamData
33b4ae2f2f version: bump version number. 2023-01-31 09:56:16 -07:00
UpstreamData
62194bd627 bug: add chip counts for M31S+ V30, V40, and V100. 2023-01-31 09:54:35 -07:00
UpstreamData
83bb2950fa version: bump version number. 2023-01-30 15:02:49 -07:00
UpstreamData
262dee3cfd bug: update whatsminer fan counts for water cooled racks. 2023-01-30 15:01:42 -07:00
UpstreamData
7ccf6ed610 bug: update whatsminer error codes. 2023-01-30 14:49:25 -07:00
UpstreamData
21b189f5a8 version: bump version number. 2023-01-30 13:46:32 -07:00
UpstreamData
d8d8a050ce docs: update docs to reflect new whatsminers. 2023-01-30 13:45:50 -07:00
UpstreamData
b9ca810903 feature: Add every whatsminer type known to man (or at least microBT). 2023-01-30 13:07:35 -07:00
Upstream Data
6ad750d3e9 small fix to BTMiner error codes to make things a bit more efficient. 2023-01-27 22:14:51 -07:00
UpstreamData
8423b64825 docs: update documentation. 2023-01-27 09:51:58 -07:00
UpstreamData
742ddef227 version: bump version number. 2023-01-27 09:42:01 -07:00
UpstreamData
85d7f0abfb feature: add nominal_hashrate to miner data 2023-01-27 09:41:25 -07:00
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
247 changed files with 13081 additions and 4225 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__.:

1
.gitignore vendored
View File

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

View File

@@ -3,13 +3,18 @@ repos:
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- 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

View File

@@ -7,63 +7,46 @@
[![Read the Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![GitHub](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
## Documentation
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/)
## 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-dev.txt``` on Windows or ```pip3 install -r requirements-dev.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```, and you will have to install `pre-commit` (`pip install pre-commit`).
Finally, initialize pre-commit hooks with `pre-commit 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
@@ -84,26 +67,19 @@ 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()
@@ -117,25 +93,24 @@ 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__":
@@ -148,19 +123,13 @@ 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")

View File

@@ -23,3 +23,11 @@
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

@@ -6,3 +6,10 @@
options:
show_root_heading: false
heading_level: 4
## HashBoard Data
::: pyasic.data.HashBoard
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -101,3 +101,174 @@ async def gather_miner_data(): # define async scan function to allow awaiting
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

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

@@ -4,70 +4,507 @@
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
##### pyasic currently supports the following miners and subtypes:
* 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:
* [M20][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20]:
* [V10][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20V10]
* [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>M2X Series:</summary>
<ul>
<details>
<summary><a href='../whatsminer/M2X/#M20'>M20</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M20V10'>M20V10</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M20S'>M20S</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M20SV10'>M20SV10</a></li>
<li><a href='../whatsminer/M2X/#M20SV20'>M20SV20</a></li>
<li><a href='../whatsminer/M2X/#M20SV30'>M20SV30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M20S_1'>M20S+</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M20S_1V30'>M20S+V30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M21'>M21</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M21V10'>M21V10</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M21S'>M21S</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M21SV20'>M21SV20</a></li>
<li><a href='../whatsminer/M2X/#M21SV60'>M21SV60</a></li>
<li><a href='../whatsminer/M2X/#M21SV70'>M21SV70</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M21S_1'>M21S+</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M21S_1V20'>M21S+V20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M2X/#M29'>M29</a></summary>
<ul>
<li><a href='../whatsminer/M2X/#M29V10'>M29V10</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>M3X Series:</summary>
<ul>
<details>
<summary><a href='../whatsminer/M3X/#M30'>M30</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M30V10'>M30V10</a></li>
<li><a href='../whatsminer/M3X/#M30V20'>M30V20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M30S'>M30S</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M30SV10'>M30SV10</a></li>
<li><a href='../whatsminer/M3X/#M30SV20'>M30SV20</a></li>
<li><a href='../whatsminer/M3X/#M30SV30'>M30SV30</a></li>
<li><a href='../whatsminer/M3X/#M30SV40'>M30SV40</a></li>
<li><a href='../whatsminer/M3X/#M30SV50'>M30SV50</a></li>
<li><a href='../whatsminer/M3X/#M30SV60'>M30SV60</a></li>
<li><a href='../whatsminer/M3X/#M30SV70'>M30SV70</a></li>
<li><a href='../whatsminer/M3X/#M30SV80'>M30SV80</a></li>
<li><a href='../whatsminer/M3X/#M30SVE10'>M30SVE10</a></li>
<li><a href='../whatsminer/M3X/#M30SVE20'>M30SVE20</a></li>
<li><a href='../whatsminer/M3X/#M30SVE30'>M30SVE30</a></li>
<li><a href='../whatsminer/M3X/#M30SVE40'>M30SVE40</a></li>
<li><a href='../whatsminer/M3X/#M30SVE50'>M30SVE50</a></li>
<li><a href='../whatsminer/M3X/#M30SVE60'>M30SVE60</a></li>
<li><a href='../whatsminer/M3X/#M30SVE70'>M30SVE70</a></li>
<li><a href='../whatsminer/M3X/#M30SVF10'>M30SVF10</a></li>
<li><a href='../whatsminer/M3X/#M30SVF20'>M30SVF20</a></li>
<li><a href='../whatsminer/M3X/#M30SVF30'>M30SVF30</a></li>
<li><a href='../whatsminer/M3X/#M30SVG10'>M30SVG10</a></li>
<li><a href='../whatsminer/M3X/#M30SVG20'>M30SVG20</a></li>
<li><a href='../whatsminer/M3X/#M30SVG30'>M30SVG30</a></li>
<li><a href='../whatsminer/M3X/#M30SVG40'>M30SVG40</a></li>
<li><a href='../whatsminer/M3X/#M30SVH10'>M30SVH10</a></li>
<li><a href='../whatsminer/M3X/#M30SVH20'>M30SVH20</a></li>
<li><a href='../whatsminer/M3X/#M30SVH30'>M30SVH30</a></li>
<li><a href='../whatsminer/M3X/#M30SVH40'>M30SVH40</a></li>
<li><a href='../whatsminer/M3X/#M30SVH50'>M30SVH50</a></li>
<li><a href='../whatsminer/M3X/#M30SVH60'>M30SVH60</a></li>
<li><a href='../whatsminer/M3X/#M30SVI20'>M30SVI20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M30S_1'>M30S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M30S_1V10'>M30S+V10</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V20'>M30S+V20</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V30'>M30S+V30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V40'>M30S+V40</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V50'>M30S+V50</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V60'>M30S+V60</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V70'>M30S+V70</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V80'>M30S+V80</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V90'>M30S+V90</a></li>
<li><a href='../whatsminer/M3X/#M30S_1V100'>M30S+V100</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE30'>M30S+VE30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE40'>M30S+VE40</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE50'>M30S+VE50</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE60'>M30S+VE60</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE70'>M30S+VE70</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE80'>M30S+VE80</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE90'>M30S+VE90</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VE100'>M30S+VE100</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VF20'>M30S+VF20</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VF30'>M30S+VF30</a></li>
<li><a href='../whatsminer/M3X/#M36S_1VG30'>M36S+VG30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VG30'>M30S+VG30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VG40'>M30S+VG40</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VG50'>M30S+VG50</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VG60'>M30S+VG60</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH10'>M30S+VH10</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH20'>M30S+VH20</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH30'>M30S+VH30</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH40'>M30S+VH40</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH50'>M30S+VH50</a></li>
<li><a href='../whatsminer/M3X/#M30S_1VH60'>M30S+VH60</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M30S_2'>M30S++</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M30S_2V10'>M30S++V10</a></li>
<li><a href='../whatsminer/M3X/#M30S_2V20'>M30S++V20</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VE30'>M30S++VE30</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VE40'>M30S++VE40</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VE50'>M30S++VE50</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VF40'>M30S++VF40</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VG30'>M30S++VG30</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VG40'>M30S++VG40</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VG50'>M30S++VG50</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH10'>M30S++VH10</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH20'>M30S++VH20</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH30'>M30S++VH30</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH40'>M30S++VH40</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH50'>M30S++VH50</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH60'>M30S++VH60</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH70'>M30S++VH70</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH80'>M30S++VH80</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH90'>M30S++VH90</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VH100'>M30S++VH100</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VJ20'>M30S++VJ20</a></li>
<li><a href='../whatsminer/M3X/#M30S_2VJ30'>M30S++VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31'>M31</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31V10'>M31V10</a></li>
<li><a href='../whatsminer/M3X/#M31V20'>M31V20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31S'>M31S</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31SV10'>M31SV10</a></li>
<li><a href='../whatsminer/M3X/#M31SV20'>M31SV20</a></li>
<li><a href='../whatsminer/M3X/#M31SV30'>M31SV30</a></li>
<li><a href='../whatsminer/M3X/#M31SV40'>M31SV40</a></li>
<li><a href='../whatsminer/M3X/#M31SV50'>M31SV50</a></li>
<li><a href='../whatsminer/M3X/#M31SV60'>M31SV60</a></li>
<li><a href='../whatsminer/M3X/#M31SV70'>M31SV70</a></li>
<li><a href='../whatsminer/M3X/#M31SV80'>M31SV80</a></li>
<li><a href='../whatsminer/M3X/#M31SV90'>M31SV90</a></li>
<li><a href='../whatsminer/M3X/#M31SVE10'>M31SVE10</a></li>
<li><a href='../whatsminer/M3X/#M31SVE20'>M31SVE20</a></li>
<li><a href='../whatsminer/M3X/#M31SVE30'>M31SVE30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31SE'>M31SE</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31SEV10'>M31SEV10</a></li>
<li><a href='../whatsminer/M3X/#M31SEV20'>M31SEV20</a></li>
<li><a href='../whatsminer/M3X/#M31SEV30'>M31SEV30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31H'>M31H</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31HV40'>M31HV40</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M31S_1'>M31S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M31S_1V10'>M31S+V10</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V20'>M31S+V20</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V30'>M31S+V30</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V40'>M31S+V40</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V50'>M31S+V50</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V60'>M31S+V60</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V80'>M31S+V80</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V90'>M31S+V90</a></li>
<li><a href='../whatsminer/M3X/#M31S_1V100'>M31S+V100</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE10'>M31S+VE10</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE20'>M31S+VE20</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE30'>M31S+VE30</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE40'>M31S+VE40</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE50'>M31S+VE50</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE60'>M31S+VE60</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VE80'>M31S+VE80</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VF20'>M31S+VF20</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VF30'>M31S+VF30</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VG20'>M31S+VG20</a></li>
<li><a href='../whatsminer/M3X/#M31S_1VG30'>M31S+VG30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M32'>M32</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M32V10'>M32V10</a></li>
<li><a href='../whatsminer/M3X/#M32V20'>M32V20</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M33'>M33</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M33V10'>M33V10</a></li>
<li><a href='../whatsminer/M3X/#M33V20'>M33V20</a></li>
<li><a href='../whatsminer/M3X/#M33V30'>M33V30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M33S'>M33S</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M33SVG30'>M33SVG30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M33S_1'>M33S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M33S_1VH20'>M33S+VH20</a></li>
<li><a href='../whatsminer/M3X/#M33S_1VH30'>M33S+VH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M33S_2'>M33S++</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M33S_2VH20'>M33S++VH20</a></li>
<li><a href='../whatsminer/M3X/#M33S_2VH30'>M33S++VH30</a></li>
<li><a href='../whatsminer/M3X/#M33S_2VG40'>M33S++VG40</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M34S_1'>M34S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M34S_1VE10'>M34S+VE10</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M36S'>M36S</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M36SVE10'>M36SVE10</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M36S_1'>M36S+</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M36S_1VG30'>M36S+VG30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M36S_2'>M36S++</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M36S_2VH30'>M36S++VH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M3X/#M39'>M39</a></summary>
<ul>
<li><a href='../whatsminer/M3X/#M39V20'>M39V20</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>M5X Series:</summary>
<ul>
<details>
<summary><a href='../whatsminer/M5X/#M50'>M50</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M50VG30'>M50VG30</a></li>
<li><a href='../whatsminer/M5X/#M50VH10'>M50VH10</a></li>
<li><a href='../whatsminer/M5X/#M50VH20'>M50VH20</a></li>
<li><a href='../whatsminer/M5X/#M50VH30'>M50VH30</a></li>
<li><a href='../whatsminer/M5X/#M50VH40'>M50VH40</a></li>
<li><a href='../whatsminer/M5X/#M50VH50'>M50VH50</a></li>
<li><a href='../whatsminer/M5X/#M50VH60'>M50VH60</a></li>
<li><a href='../whatsminer/M5X/#M50VH70'>M50VH70</a></li>
<li><a href='../whatsminer/M5X/#M50VH80'>M50VH80</a></li>
<li><a href='../whatsminer/M5X/#M50VJ10'>M50VJ10</a></li>
<li><a href='../whatsminer/M5X/#M50VJ20'>M50VJ20</a></li>
<li><a href='../whatsminer/M5X/#M50VJ30'>M50VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M50S'>M50S</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M50SVJ10'>M50SVJ10</a></li>
<li><a href='../whatsminer/M5X/#M50SVJ20'>M50SVJ20</a></li>
<li><a href='../whatsminer/M5X/#M50SVJ30'>M50SVJ30</a></li>
<li><a href='../whatsminer/M5X/#M50SVH10'>M50SVH10</a></li>
<li><a href='../whatsminer/M5X/#M50SVH20'>M50SVH20</a></li>
<li><a href='../whatsminer/M5X/#M50SVH30'>M50SVH30</a></li>
<li><a href='../whatsminer/M5X/#M50SVH40'>M50SVH40</a></li>
<li><a href='../whatsminer/M5X/#M50SVH50'>M50SVH50</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M50S_1'>M50S+</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M50S_1VH30'>M50S+VH30</a></li>
<li><a href='../whatsminer/M5X/#M50S_1VH40'>M50S+VH40</a></li>
<li><a href='../whatsminer/M5X/#M50S_1VJ30'>M50S+VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M53'>M53</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M53VH30'>M53VH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M53S'>M53S</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M53SVH30'>M53SVH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M53S_1'>M53S+</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M53S_1VJ30'>M53S+VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M56'>M56</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M56VH30'>M56VH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M56S'>M56S</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M56SVH30'>M56SVH30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M56S_1'>M56S+</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M56S_1VJ30'>M56S+VJ30</a></li>
</ul>
</details>
<details>
<summary><a href='../whatsminer/M5X/#M59'>M59</a></summary>
<ul>
<li><a href='../whatsminer/M5X/#M59VH30'>M59VH30</a></li>
</ul>
</details>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Antminers:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19/#s19">S19</a></li>
<li><a href="../antminer/X19/#s19-pro">S19 Pro</a></li>
<li><a href="../antminer/X19/#s19a">S19a</a></li>
<li><a href="../antminer/X19/#s19j">S19j</a></li>
<li><a href="../antminer/X19/#s19j-pro">S19j Pro</a></li>
<li><a href="../antminer/X19/#s19-xp">S19 XP</a></li>
<li><a href="../antminer/X19/#t19">T19</a></li>
</ul>
</details>
<details>
<summary>X17 Series:</summary>
<ul>
<li><a href="../antminer/X17/#s17">S17</a></li>
<li><a href="../antminer/X17/#s17_1">S17+</a></li>
<li><a href="../antminer/X17/#s17-pro">S17 Pro</a></li>
<li><a href="../antminer/X17/#s17e">S17e</a></li>
<li><a href="../antminer/X17/#t17">T17</a></li>
<li><a href="../antminer/X17/#t17_1">T17+</a></li>
<li><a href="../antminer/X17/#t17e">T17e</a></li>
</ul>
</details>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9/#s9">S9</a></li>
<li><a href="../antminer/X9/#s9i">S9i</a></li>
<li><a href="../antminer/X9/#t9">T9</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Avalonminers:</summary>
<ul>
<details>
<summary>A7X Series:</summary>
<ul>
<li><a href="../avalonminer/A7X/#a721">A721</a></li>
<li><a href="../avalonminer/A7X/#a741">A741</a></li>
<li><a href="../avalonminer/A7X/#a761">A761</a></li>
</ul>
</details>
<details>
<summary>A8X Series:</summary>
<ul>
<li><a href="../avalonminer/A8X/#a821">A821</a></li>
<li><a href="../avalonminer/A8X/#a841">A841</a></li>
<li><a href="../avalonminer/A8X/#a851">A851</a></li>
</ul>
</details>
<details>
<summary>A9X Series:</summary>
<ul>
<li><a href="../avalonminer/A9X/#a921">A921</a></li>
</ul>
</details>
<details>
<summary>A10X Series:</summary>
<ul>
<li><a href="../avalonminer/A10X/#a1026">A1026</a></li>
<li><a href="../avalonminer/A10X/#a1047">A1047</a></li>
<li><a href="../avalonminer/A10X/#a1066">A1066</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Innosilicon Miners:</summary>
<ul>
<details>
<summary>T3X Series:</summary>
<ul>
<li><a href="../innosilicon/T3X/#t3h">T3H+</a></li>
</ul>
</details>
</ul>
</details>

View File

@@ -1,14 +1,6 @@
# 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
@@ -17,14 +9,6 @@
show_root_heading: false
heading_level: 4
## M20S
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S
handler: python
options:
show_root_heading: false
heading_level: 4
## M20SV10
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV10
@@ -41,26 +25,25 @@
show_root_heading: false
heading_level: 4
## M20S+
## M20SV30
::: pyasic.miners.whatsminer.btminer.M2X.M20S_Plus.BTMinerM20SPlus
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV30
handler: python
options:
show_root_heading: false
heading_level: 4
## M21
## M20S+V30
::: pyasic.miners.whatsminer.btminer.M2X.M21.BTMinerM21
::: pyasic.miners.whatsminer.btminer.M2X.M20S_Plus.BTMinerM20SPlusV30
handler: python
options:
show_root_heading: false
heading_level: 4
## M21V10
## M21S
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21S
::: pyasic.miners.whatsminer.btminer.M2X.M21.BTMinerM21V10
handler: python
options:
show_root_heading: false
@@ -82,9 +65,25 @@
show_root_heading: false
heading_level: 4
## M21S+
## M21SV70
::: pyasic.miners.whatsminer.btminer.M2X.M21S_Plus.BTMinerM21SPlus
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV70
handler: python
options:
show_root_heading: false
heading_level: 4
## M21S+V20
::: pyasic.miners.whatsminer.btminer.M2X.M21S_Plus.BTMinerM21SPlusV20
handler: python
options:
show_root_heading: false
heading_level: 4
## M29V10
::: pyasic.miners.whatsminer.btminer.M2X.M29.BTMinerM29V10
handler: python
options:
show_root_heading: false

File diff suppressed because it is too large Load Diff

View File

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

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,14 +21,14 @@ 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"
- Error Codes: "data/error_codes.md"
- Config:
- Miner Config: "config/miner_config.md"
- Advanced:
- Miner APIs:

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:
@@ -60,6 +35,86 @@ class BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls)
def __repr__(self):
return f"{self.__class__.__name__}: {str(self.ip)}"
async def send_command(
self,
command: Union[str, bytes],
parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
allow_warning: bool = True,
) -> dict:
"""Send an API command to the miner and return the result.
Parameters:
command: The command to sent to the miner.
parameters: Any additional parameters to be sent with the command.
ignore_errors: Whether to raise APIError when the command returns an error.
allow_warning: Whether to warn if the command fails.
Returns:
The return data from the API command parsed from JSON into a dict.
"""
logging.debug(
f"{self} - (Send Privileged Command) - {command} "
+ f"with args {parameters}"
if parameters
else ""
)
# create the command
cmd = {"command": command}
if parameters:
cmd["parameter"] = parameters
# send the command
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
data = self._load_api_data(data)
# check for if the user wants to allow errors to return
if not ignore_errors:
# validate the command succeeded
validation = self._validate_command_output(data)
if not validation[0]:
if allow_warning:
logging.warning(
f"{self.ip}: API Command Error: {command}: {validation[1]}"
)
raise APIError(validation[1])
logging.debug(f"{self} - (Send Command) - Received data.")
return data
# Privileged command handler, only used by whatsminers, defined here for consistency.
async def send_privileged_command(self, *args, **kwargs) -> dict:
return await self.send_command(*args, **kwargs)
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner.
Parameters:
*commands: The commands to send as a multicommand to the miner.
ignore_errors: Whether to raise APIError when the command returns an error.
allow_warning: A boolean to supress APIWarnings.
"""
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# standard format doesn't work for X19
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
return {command: [{}] for command in commands}
logging.debug(f"{self} - (Multicommand) - Received data")
return data
@property
def commands(self) -> list:
return self.get_commands()
def get_commands(self) -> list:
"""Get a list of command accessible to a specific type of API on the miner.
@@ -71,9 +126,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 [
@@ -97,108 +153,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:
@@ -229,32 +227,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:
pass
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,27 +13,29 @@
# limitations under the License.
import asyncio
import re
import json
import hashlib
import binascii
import base64
import binascii
import datetime
import hashlib
import json
import logging
import re
from typing import Union
from passlib.handlers.md5_crypt import md5_crypt
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from 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.misc import api_min_version
from pyasic.settings import PyasicSettings
### 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.
@@ -82,7 +84,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:
@@ -124,6 +126,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
@@ -179,64 +182,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: Union[str, bytes],
parameters: Union[str, int, bool] = None,
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
@@ -256,11 +282,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
@@ -277,7 +310,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 ####
@@ -319,46 +356,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.
@@ -372,10 +381,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.
@@ -392,12 +398,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.
@@ -408,14 +410,10 @@ class BTMinerAPI(BaseMinerAPI):
the password of the miner using the Whatsminer tool.
Returns:
A reply informing of the status of powering on.
</details>
"""
command = {"cmd": "power_on"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("power_on")
async def reset_led(self) -> dict:
"""Reset the LED on the miner using the API.
@@ -426,20 +424,17 @@ class BTMinerAPI(BaseMinerAPI):
changing the password of the miner using the Whatsminer tool.
Returns:
A reply informing of the status of resetting the LED.
</details>
"""
command = {"cmd": "set_led", "param": "auto"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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.
@@ -450,25 +445,20 @@ 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.
start: LED on time offset in the cycle in ms.
Returns:
A reply informing of the status of setting the LED.
</details>
"""
command = {
"cmd": "set_led",
"color": color,
"period": period,
"duration": duration,
"start": start,
}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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.
@@ -479,14 +469,10 @@ class BTMinerAPI(BaseMinerAPI):
changing the password of the miner using the Whatsminer tool.
Returns:
A reply informing of the status of setting low power mode.
</details>
"""
command = {"cmd": "set_low_power"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("set_low_power")
async def update_firmware(self): # noqa - static
"""Not implemented."""
@@ -494,20 +480,23 @@ 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>
Returns:
A reply informing of the status of the reboot.
</details>
"""
command = {"cmd": "reboot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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.
@@ -515,14 +504,10 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
A reply informing of the status of the reset.
</details>
"""
command = {"cmd": "factory_reset"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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.
@@ -539,19 +524,23 @@ class BTMinerAPI(BaseMinerAPI):
old_pwd: The old admin password.
new_pwd: The new password to set.
Returns:
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.
@@ -566,7 +555,6 @@ class BTMinerAPI(BaseMinerAPI):
Parameters:
percent: The frequency % to set.
Returns:
A reply informing of the status of setting the frequency.
</details>
"""
@@ -576,10 +564,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.
@@ -591,14 +578,10 @@ class BTMinerAPI(BaseMinerAPI):
the miner using the Whatsminer tool.
Returns:
A reply informing of the status of enabling fast boot.
</details>
"""
command = {"cmd": "enable_btminer_fast_boot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("enable_btminer_fast_boot")
async def disable_fast_boot(self) -> dict:
"""Turn off fast boot.
@@ -610,14 +593,10 @@ class BTMinerAPI(BaseMinerAPI):
the miner using the Whatsminer tool.
Returns:
A reply informing of the status of disabling fast boot.
</details>
"""
command = {"cmd": "disable_btminer_fast_boot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("disable_btminer_fast_boot")
async def enable_web_pools(self) -> dict:
"""Turn on web pool updates.
@@ -629,14 +608,10 @@ class BTMinerAPI(BaseMinerAPI):
password of the miner using the Whatsminer tool.
Returns:
A reply informing of the status of enabling web pools.
</details>
"""
command = {"cmd": "enable_web_pools"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("enable_web_pools")
async def disable_web_pools(self) -> dict:
"""Turn off web pool updates.
@@ -648,14 +623,10 @@ class BTMinerAPI(BaseMinerAPI):
password of the miner using the Whatsminer tool.
Returns:
A reply informing of the status of disabling web pools.
</details>
"""
command = {"cmd": "disable_web_pools"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("disable_web_pools")
async def set_hostname(self, hostname: str) -> dict:
"""Set the hostname of the miner.
@@ -669,17 +640,13 @@ class BTMinerAPI(BaseMinerAPI):
Parameters:
hostname: The new hostname to use.
Returns:
A reply informing of the status of setting the hostname.
</details>
"""
command = {"cmd": "set_hostname", "hostname": hostname}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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>
@@ -690,7 +657,6 @@ class BTMinerAPI(BaseMinerAPI):
Parameters:
percent: The power percentage to set.
Returns:
A reply informing of the status of setting the power percentage.
</details>
"""
@@ -701,10 +667,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.
@@ -722,7 +685,6 @@ class BTMinerAPI(BaseMinerAPI):
* `adjust complete`
* `adjust continue`
Returns:
A reply informing of the status of pre power on.
</details>
"""
@@ -735,13 +697,133 @@ 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) -> dict:
"""Set the offset of miner hash board target temperature.
<details>
<summary>Expand</summary>
Set the offset of miner hash board target temperature, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
temp_offset: Target temperature offset.
Returns:
A reply informing of the status of setting temp offset.
</details>
"""
if not -30 < temp_offset < 0:
raise APIError(
f"Temp offset is outside of the allowed "
f"range. Please set a number between -30 and "
f"0."
)
return await self.send_privileged_command(
"set_temp_offset", temp_offset=temp_offset
)
@api_min_version("2.0.5")
async def adjust_power_limit(self, power_limit: int) -> dict:
"""Set the upper limit of the miner's power. Cannot be higher than the ordinary power of the machine.
<details>
<summary>Expand</summary>
Set the upper limit of the miner's power, only works after
changing the password of the miner using the Whatsminer tool.
The miner will reboot after this is set.
Parameters:
power_limit: New power limit.
Returns:
A reply informing of the status of setting power limit.
</details>
"""
return await self.send_privileged_command(
"adjust_power_limit", power_limit=power_limit
)
@api_min_version("2.0.5")
async def adjust_upfreq_speed(self, upfreq_speed: int) -> dict:
"""Set the upfreq speed, 0 is the normal speed, 9 is the fastest speed.
<details>
<summary>Expand</summary>
Set the upfreq speed, 0 is the normal speed, 9 is the fastest speed, only works after
changing the password of the miner using the Whatsminer tool.
The faster the speed, the greater the final hash rate and power deviation, and the stability
may be impacted. Fast boot mode cannot be used at the same time.
Parameters:
upfreq_speed: New upfreq speed.
Returns:
A reply informing of the status of setting upfreq speed.
</details>
"""
if not 0 < upfreq_speed < 9:
raise APIError(
f"Upfreq speed is outside of the allowed "
f"range. Please set a number between 0 (Normal) and "
f"9 (Fastest)."
)
return await self.send_privileged_command(
"adjust_upfreq_speed", upfreq_speed=upfreq_speed
)
@api_min_version("2.0.5")
async def set_poweroff_cool(self, poweroff_cool: bool) -> dict:
"""Set whether to cool the machine when mining is stopped.
<details>
<summary>Expand</summary>
Set whether to cool the machine when mining is stopped, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
poweroff_cool: Whether to cool the miner during power off mode.
Returns:
A reply informing of the status of setting power off cooling mode.
</details>
"""
return await self.send_privileged_command(
"set_poweroff_cool", poweroff_cool=int(poweroff_cool)
)
@api_min_version("2.0.5")
async def set_fan_zero_speed(self, fan_zero_speed: bool) -> dict:
"""Sets whether the fan speed supports the lowest 0 speed.
<details>
<summary>Expand</summary>
Sets whether the fan speed supports the lowest 0 speed, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
fan_zero_speed: Whether the fan is allowed to support 0 speed.
Returns:
A reply informing of the status of setting fan minimum speed.
</details>
"""
return await self.send_privileged_command(
"set_fan_zero_speed", fan_zero_speed=int(fan_zero_speed)
)
#### END privileged COMMANDS ####
@@ -751,7 +833,6 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
Summary status of the miner.
</details>
"""
@@ -763,7 +844,6 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
Pool status of the miner.
</details>
"""
@@ -775,7 +855,6 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
Data on each PGA/ASC with their details.
</details>
"""
@@ -787,7 +866,6 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
Data on each PGA/ASC with their details.
</details>
"""
@@ -799,7 +877,6 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
Data on all devices with their static details.
</details>
"""
@@ -811,7 +888,6 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
Data on the PSU and power information.
</details>
"""
@@ -827,7 +903,6 @@ class BTMinerAPI(BaseMinerAPI):
with the other miner APIs.
Returns:
Version data for the miner.
</details>
"""
@@ -839,7 +914,6 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
Version data for the miner.
</details>
"""
@@ -851,7 +925,6 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
BTMiner status and firmware version.
</details>
"""
@@ -863,8 +936,21 @@ class BTMinerAPI(BaseMinerAPI):
<summary>Expand</summary>
Returns:
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:
pass
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":
@@ -288,6 +365,12 @@ class MinerConfig:
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")
@@ -302,6 +385,7 @@ class MinerConfig:
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))
@@ -317,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):
@@ -325,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
@@ -353,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
@@ -363,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,11 +12,49 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Union, List
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 X19Error, WhatsminerError, BraiinsOSError
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
@dataclass
class HashBoard:
"""A Dataclass to standardize hashboard data.
Attributes:
slot: The slot of the board as an int.
hashrate: The hashrate of the board in TH/s as a float.
temp: The temperature of the PCB as an int.
chip_temp: The temperature of the chips as an int.
chips: The chip count of the board as an int.
expected_chips: The ideal chip count of the board as an int.
missing: Whether the board is returned from the miners data as a bool.
"""
slot: int = 0
hashrate: float = 0.0
temp: int = -1
chip_temp: int = -1
chips: int = 0
expected_chips: int = 0
missing: bool = True
@dataclass
class Fan:
"""A Dataclass to standardize fan data.
Attributes:
speed: The speed of the fan.
"""
speed: int = -1
@dataclass
@@ -27,8 +65,12 @@ 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.
nominal_hashrate: The factory nominal hashrate of the miner in TH/s as a float.
left_board_hashrate: The hashrate of the left board of the miner in TH/s as a float.
center_board_hashrate: The hashrate of the center board of the miner in TH/s as a float.
right_board_hashrate: The hashrate of the right board of the miner in TH/s as a float.
@@ -46,13 +88,14 @@ 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.
@@ -60,34 +103,43 @@ class MinerData:
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
nominal_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
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
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
fans: List[Fan] = field(default_factory=list)
fan_1: int = field(init=False)
fan_2: int = field(init=False)
fan_3: int = field(init=False)
fan_4: int = field(init=False)
fan_psu: int = -1
left_chips: int = field(init=False)
center_chips: int = field(init=False)
right_chips: int = field(init=False)
total_chips: int = field(init=False)
ideal_chips: int = 1
percent_ideal: float = field(init=False)
@@ -97,22 +149,248 @@ class MinerData:
pool_1_user: str = "Unknown"
pool_2_url: str = ""
pool_2_user: str = ""
errors: List[Union[WhatsminerError, BraiinsOSError, X19Error]] = 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 fan_1(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 0:
return self.fans[0].speed
@fan_1.setter
def fan_1(self, val):
pass
@property
def fan_2(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 1:
return self.fans[1].speed
@fan_2.setter
def fan_2(self, val):
pass
@property
def fan_3(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 2:
return self.fans[2].speed
@fan_3.setter
def fan_3(self, val):
pass
@property
def fan_4(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 3:
return self.fans[3].speed
@fan_4.setter
def fan_4(self, val):
pass
@property
def total_chips(self): # noqa - Skip PyCharm inspection
return 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
@@ -133,13 +411,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
@@ -149,5 +423,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

@@ -12,7 +12,7 @@
# 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
@@ -21,9 +21,15 @@ class X19Error:
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,6 +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,7 +12,7 @@
# 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
@@ -21,9 +21,15 @@ class BraiinsOSError:
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,7 +12,9 @@
# 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
C_N_CODES = ["52", "53", "54", "55"]
@dataclass
@@ -27,11 +29,52 @@ class WhatsminerError:
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)[:2] in C_N_CODES:
# 55 error code base has chip numbers, so the format is
# 55 -> board num len 1 -> chip num len 3
err_type = 55
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):
@@ -42,130 +85,251 @@ 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
0: {0: "Fan unknown."},
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.",
8: "Power power error.",
9: "Power voltage offset error.",
},
1: {
0: "Power error.",
1: "Power iout error, please reboot.",
2: "Power vout error, reach vout border. Border: [1150, 1500]",
3: "Power input voltage and current do not match power output.",
4: "Power pin did not change.",
5: "Power vout set error.",
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
8: {
0: "Humidity sensor not found.",
1: "Humidity sensor read error.",
2: "Humidity sensor read error.",
3: "Humidity sensor protecting.",
},
},
4: { # EEPROM error
0: {0: "Eeprom unknown error."},
1: {"n": "Slot {n} eeprom detection error."}, # EEPROM detection error
2: {"n": "Slot {n} eeprom parsing error."}, # EEPROM parsing error
3: {"n": "Slot {n} chip bin type error."}, # chip bin error
4: {"n": "Slot {n} eeprom chip number X error."}, # EEPROM chip number error
5: {"n": "Slot {n} eeprom xfer error."}, # EEPROM xfer error
},
5: { # hashboard error
0: {0: "Board unknown error."},
1: {"n": "Slot {n} miner type error."}, # board miner type error
2: {"n": "Slot {n} bin type error."}, # chip bin type error
3: {"n": "Slot {n} not found."}, # board not found error
4: {"n": "Slot {n} error reading chip id."}, # reading chip id error
5: {"n": "Slot {n} has bad chips."}, # board has bad chips error
6: {"n": "Slot {n} loss of balance error."}, # loss of balance error
7: {"n": "Slot {n} xfer error chip."}, # xfer error
8: {"n": "Slot {n} reset error."}, # reset error
9: {"n": "Slot {n} frequency too low."}, # freq error
},
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: {0: "MAC address invalid", 1: "Control board no support chip."},
1: {
0: "Control board rebooted as an exception.",
1: "Control board rebooted as exception and cpufreq reduced, please upgrade the firmware",
2: "Control board rebooted as an exception.",
},
},
8: { # checksum error
0: {
0: "CGMiner checksum error.",
1: "System monitor checksum error.",
2: "Remote daemon checksum error.",
}
},
9: {0: {1: "Power rate error."}}, # power rate error
20: { # pool error
1: {0: "All pools are disabled."}, # all disabled error
2: {"n": "Pool {n} connection failed."}, # pool connection failed error
3: {0: "High rejection rate on pool."}, # rejection rate error
4: { # asicboost not supported error
0: "The pool does not support asicboost mode."
},
},
21: {1: {"n": "Slot {n} factory test step failed."}},
23: { # hashrate error
1: {0: "Hashrate is too low."},
2: {0: "Hashrate is too low."},
3: {0: "Hashrate loss is too high."},
4: {0: "Hashrate loss is too high."},
5: {0: "Hashrate loss."},
},
50: { # water velocity error/voltage error
1: {"n": "Slot {n} chip voltage too low."},
2: {"n": "Slot {n} chip voltage changed."},
3: {"n": "Slot {n} chip temperature difference is too large."},
4: {"n": "Slot {n} chip hottest temperature difference is too large."},
7: {"n": "Slot {n} water velocity is abnormal."}, # abnormal water velocity
8: {0: "Chip temp calibration failed, please restore factory settings."},
9: {"n": "Slot {n} chip temp calibration check no balance."},
},
51: { # frequency error
1: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
7: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
},
52: {"n": {"c": "Slot {n} chip {c} error nonce."}},
53: {"n": {"c": "Slot {n} chip {c} too few nonce."}},
54: {"n": {"c": "Slot {n} chip {c} temp protected."}},
55: {"n": {"c": "Slot {n} chip {c} has been reset."}},
80: {
0: {0: "The tool version is too low, please update."},
1: {0: "Low freq."},
2: {0: "Low hashrate."},
3: {5: "High env temp."},
},
81: {
0: {0: "Chip data error."},
},
82: {
0: {0: "Power version error."},
1: {0: "Miner type error."},
2: {0: "Version info error."},
},
83: {
0: {0: "Empty level error."},
},
84: {
0: {0: "Old firmware."},
1: {0: "Software version error."},
},
85: {
"n": {
0: "Hashrate substandard L{n}.",
1: "Power consumption substandard L{n}.",
2: "Fan speed substandard L{n}.",
3: "Fan speed substandard L{n}.",
4: "Voltage substandard L{n}.",
},
},
86: {
0: {0: "Missing product serial #."},
1: {0: "Missing product type."},
2: {
0: "Missing miner serial #.",
1: "Wrong miner serial # length.",
},
3: {
0: "Missing power serial #.",
1: "Wrong power serial #.",
2: "Fault miner serial #.",
},
4: {
0: "Missing power model.",
1: "Wrong power model name.",
2: "Wrong power model vout.",
3: "Wrong power model rate.",
4: "Wrong power model format.",
},
5: {0: "Wrong hash board struct."},
6: {0: "Wrong miner cooling type."},
7: {0: "Missing PCB serial #."},
},
87: {0: {0: "Miner power mismatch."}},
99: {9: {9: "Miner unknown error."}},
1000: {
0: {
0: "Security library error, please upgrade firmware",
1: "/antiv/signature illegal.",
2: "/antiv/dig/init.d illegal.",
3: "/antiv/dig/pf_partial.dig illegal.",
},
},
1001: {0: {0: "Security BTMiner removed, please upgrade firmware."}},
1100: {
0: {
0: "Security illegal file, please upgrade firmware.",
1: "Security virus 0001 is removed, please upgrade firmware.",
}
},
}

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,126 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncssh
import logging
import ipaddress
from abc import ABC, abstractmethod
from typing import Union
from pyasic.data import MinerData
from pyasic.config import MinerConfig
from pyasic.miners.base import AnyMiner, BaseMiner
from pyasic.miners.miner_factory import MinerFactory
class BaseMiner(ABC):
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 __new__(cls, *args, **kwargs):
if cls is BaseMiner:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls)
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
@abstractmethod
async def fault_light_on(self) -> bool:
pass
@abstractmethod
async def fault_light_off(self) -> bool:
pass
# async def send_file(self, src, dest):
# async with (await self._get_ssh_connection()) as conn:
# await asyncssh.scp(src, (conn, dest))
@abstractmethod
async def check_light(self) -> bool:
pass
# @abstractmethod
async def get_board_info(self):
return None
@abstractmethod
async def get_config(self) -> MinerConfig:
pass
@abstractmethod
async def get_hostname(self) -> str:
pass
@abstractmethod
async def get_model(self) -> str:
pass
@abstractmethod
async def reboot(self) -> bool:
pass
@abstractmethod
async def restart_backend(self) -> bool:
pass
async def send_config(self, *args, **kwargs):
return None
@abstractmethod
async def get_mac(self) -> str:
pass
@abstractmethod
async def get_errors(self) -> list:
pass
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

@@ -16,4 +16,5 @@ from .bmminer import BMMiner
from .bosminer import BOSMiner
from .btminer import BTMiner
from .cgminer import CGMiner
from .cgminer_avalon import CGMinerAvalon
from .hiveon import Hiveon

View File

@@ -14,96 +14,42 @@
import ipaddress
import logging
from typing import Union
from collections import namedtuple
from typing import List, Optional, Tuple, Union
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 Fan, 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,10 +86,8 @@ class BMMiner(BaseMiner):
return True
return False
async def check_light(self) -> bool:
if not self.light:
self.light = False
return self.light
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def fault_light_off(self) -> bool:
return False
@@ -165,73 +95,134 @@ class BMMiner(BaseMiner):
async def fault_light_on(self) -> bool:
return False
async def get_errors(self) -> list:
return []
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 set_power_limit(self, wattage: int) -> bool:
return False
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_mac(self) -> str:
return "00:00:00:00:00:00"
async def restart_backend(self) -> bool:
return False
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
async def get_data(self) -> MinerData:
"""Get data from the miner.
if not api_devdetails:
try:
api_devdetails = await self.api.devdetails()
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_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
board_offset = -1
fan_offset = -1
logging.warning(f"Failed to get model for miner: {self}")
return None
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
errors = await self.get_errors()
async def get_api_ver(self, api_version: dict = None) -> Optional[str]:
# Check to see if the version info is already cached
if self.api_ver:
return self.api_ver
if model:
data.model = model
if not api_version:
try:
api_version = await self.api.version()
except APIError:
pass
if hostname:
data.hostname = hostname
if api_version:
try:
self.api_ver = api_version["VERSION"][0]["API"]
except (KeyError, IndexError):
pass
if mac:
data.mac = mac
return self.api_ver
if errors:
for error in errors:
data.errors.append(error)
async def get_fw_ver(self, api_version: dict = None) -> Optional[str]:
# Check to see if the version info is already cached
if self.fw_ver:
return self.fw_ver
data.fault_light = await self.check_light()
if not api_version:
try:
api_version = await self.api.version()
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_version:
try:
self.fw_ver = api_version["VERSION"][0]["CompileTime"]
except (KeyError, IndexError):
pass
if not miner_data:
return data
return self.fw_ver
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]]:
# check if version is cached
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
return miner_version(
api_ver=await self.get_api_ver(api_version),
fw_ver=await self.get_fw_ver(api_version=api_version),
)
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)
async def get_fan_psu(self):
return None
if stats:
boards = stats.get("STATS")
if boards:
if len(boards) > 0:
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}")
@@ -241,94 +232,124 @@ 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))
if not env_temp_list == []:
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://", ""
)
data.pool_1_url = pool_1
hashrate = boards[1].get(f"chain_rate{i}")
if hashrate:
hashboard.hashrate = round(float(hashrate) / 1000, 2)
if pool_1_user:
data.pool_1_user = pool_1_user
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
if pool_2:
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
return hashboards
if pool_2_user:
data.pool_2_user = pool_2_user
async def get_env_temp(self) -> Optional[float]:
return None
if quota:
data.pool_split = str(quota)
async def get_wattage(self) -> Optional[int]:
return None
return data
async def get_wattage_limit(self) -> Optional[int]:
return None
async def get_fans(self, api_stats: dict = None) -> List[Fan]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
fans_data = [None, None, None, None]
if api_stats:
try:
fan_offset = -1
for fan_num in range(1, 8, 4):
for _f_num in range(4):
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}")
if f and not f == 0 and fan_offset == -1:
fan_offset = fan_num
if fan_offset == -1:
fan_offset = 1
for fan in range(self.fan_count):
fans_data[fan] = api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
except (KeyError, IndexError):
pass
fans = [Fan(speed=d) if d else Fan() for d in fans_data]
return fans
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
try:
pools = {}
for i, pool in enumerate(api_pools["POOLS"]):
pools[f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["User"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_errors(self) -> List[MinerErrorData]:
return []
async def get_fault_light(self) -> bool:
return False
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
# X19 method, not sure compatibility
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
try:
rate_unit = api_stats["STATS"][1]["rate_unit"]
except KeyError:
rate_unit = "GH"
if rate_unit == "GH":
return round(ideal_rate / 1000, 2)
if rate_unit == "MH":
return round(ideal_rate / 1000000, 2)
else:
return round(ideal_rate, 2)
except (KeyError, IndexError):
pass

File diff suppressed because it is too large Load Diff

View File

@@ -12,43 +12,48 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import ipaddress
from typing import Union
import logging
from collections import namedtuple
from typing import List, Optional, Tuple, Union
import asyncssh
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.miners import BaseMiner
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
class BOSMinerOld(BaseMiner):
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) -> Union[str, 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}")
@@ -57,7 +62,7 @@ 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")
@@ -75,20 +80,103 @@ class BOSMinerOld(BaseMiner):
async def get_config(self) -> None:
return None
async def get_errors(self) -> list:
return []
async def get_hostname(self) -> str:
return "?"
async def get_mac(self) -> str:
return "00:00:00:00:00:00"
async def get_model(self) -> str:
return "S9"
async def reboot(self) -> bool:
return False
async def restart_backend(self) -> bool:
return False
async def stop_mining(self) -> bool:
return False
async def resume_mining(self) -> bool:
return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def set_power_limit(self, wattage: int) -> bool:
return False
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_mac(self) -> Optional[str]:
return None
async def get_model(self) -> str:
return "S9"
async def get_version(self) -> Tuple[Optional[str], Optional[str]]:
return None, None
async def get_hostname(self) -> Optional[str]:
return None
async def get_hashrate(self) -> Optional[float]:
return None
async def get_hashboards(self) -> List[HashBoard]:
return []
async def get_env_temp(self) -> Optional[float]:
return None
async def get_wattage(self) -> Optional[int]:
return None
async def get_wattage_limit(self) -> Optional[int]:
return None
async def get_fans(
self,
) -> List[Fan]:
return [Fan(), Fan(), Fan(), Fan()]
async def get_fan_psu(self) -> Optional[int]:
return None
async def get_api_ver(self) -> Optional[str]:
return None
async def get_fw_ver(self) -> Optional[str]:
return None
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
try:
pools = {}
for i, pool in enumerate(api_pools["POOLS"]):
pools[f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["User"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_errors(self) -> List[MinerErrorData]:
return []
async def get_fault_light(self) -> bool:
return False
async def get_nominal_hashrate(self) -> Optional[float]:
return None
async def get_data(self, allow_warning: bool = False) -> MinerData:
return MinerData(ip=str(self.ip))

View File

@@ -14,295 +14,537 @@
import ipaddress
import logging
from typing import Union
from collections import namedtuple
from typing import List, Optional, Tuple, Union
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 Fan, 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.
Returns:
Miner model or None.
"""
if self.model:
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
version_data = await self.api.devdetails()
if version_data:
self.model = version_data["DEVDETAILS"][0]["Model"].split("V")[0]
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
logging.warning(f"Failed to get model for miner: {self}")
return None
async def get_hostname(self) -> Union[str, None]:
"""Get miner hostname.
Returns:
The hostname of the miner as a string or None.
"""
if self.hostname:
return self.hostname
async def _reset_api_pwd_to_admin(self, pwd: str):
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
data = await self.api.update_pwd(pwd, "admin")
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()
return False
if data:
if data.get("SUMMARY"):
if len(data["SUMMARY"]) > 0:
_mac = data["SUMMARY"][0].get("MAC")
if _mac:
mac = _mac
if mac == "":
try:
data = await self.api.get_miner_info()
if data:
if "Msg" in data.keys():
if "mac" in data["Msg"].keys():
mac = data["Msg"]["mac"]
except APIError:
pass
return str(mac).upper()
async def check_light(self) -> bool:
if not self.light:
self.light = False
return self.light
if "Code" in data.keys():
if data["Code"] == 131:
return True
return False
async def fault_light_off(self) -> bool:
try:
data = await self.api.set_led(auto=True)
except APIError:
return False
if data:
if "Code" in data.keys():
if data["Code"] == 131:
self.light = False
return True
return False
async def fault_light_on(self) -> bool:
try:
data = await self.api.set_led(auto=False)
await self.api.set_led(
auto=False, color="green", start=0, period=1, duration=0
)
except APIError:
return False
if data:
if "Code" in data.keys():
if data["Code"] == 131:
self.light = True
return True
return False
async def get_errors(self) -> list:
return []
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:
pools = await self.api.pools()
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.keys():
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 get_data(self) -> MinerData:
"""Get data from the miner.
Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
"""
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
mac = None
async def set_power_limit(self, wattage: int) -> bool:
try:
model = await self.get_model()
except APIError:
logging.info(f"Failed to get model: {self}")
model = None
data.model = "Whatsminer"
await self.api.adjust_power_limit(wattage)
except Exception as e:
logging.warning(f"{self} set_power_limit: {e}")
return False
else:
return True
try:
hostname = await self.get_hostname()
except APIError:
logging.info(f"Failed to get hostname: {self}")
hostname = None
data.hostname = "Whatsminer"
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
if model:
data.model = model
if hostname:
data.hostname = hostname
data.fault_light = await self.check_light()
miner_data = None
for i in range(PyasicSettings().miner_get_data_retries):
async def get_mac(
self, api_summary: dict = None, api_get_miner_info: dict = None
) -> Optional[str]:
if not api_get_miner_info:
try:
miner_data = await self.api.multicommand("summary", "devs", "pools")
if miner_data:
break
api_get_miner_info = await self.api.get_miner_info()
except APIError:
pass
if not miner_data:
return data
summary = miner_data.get("summary")[0]
devs = miner_data.get("devs")[0]
pools = miner_data.get("pools")[0]
if summary:
summary_data = summary.get("SUMMARY")
if summary_data:
if len(summary_data) > 0:
wattage_limit = None
if summary_data[0].get("MAC"):
mac = summary_data[0]["MAC"]
if summary_data[0].get("Env Temp"):
data.env_temp = summary_data[0]["Env Temp"]
if summary_data[0].get("Power Limit"):
wattage_limit = summary_data[0]["Power Limit"]
data.fan_1 = summary_data[0]["Fan Speed In"]
data.fan_2 = summary_data[0]["Fan Speed Out"]
hr = summary_data[0].get("MHS 1m")
if hr:
data.hashrate = round(hr / 1000000, 2)
wattage = summary_data[0].get("Power")
if wattage:
data.wattage = round(wattage)
if not wattage_limit:
wattage_limit = round(wattage)
data.wattage_limit = wattage_limit
if summary_data[0].get("Error Code Count"):
for i in range(summary_data[0]["Error Code Count"]):
if summary_data[0].get(f"Error Code {i}"):
data.errors.append(
WhatsminerError(
error_code=summary_data[0][f"Error Code {i}"]
)
)
if devs:
temp_data = devs.get("DEVS")
if temp_data:
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
for board in temp_data:
_id = board["ASC"]
chip_temp = round(board["Chip Temp Avg"])
board_temp = round(board["Temperature"])
hashrate = round(board["MHS 1m"] / 1000000, 2)
setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
setattr(data, f"{board_map[_id]}_temp", board_temp)
setattr(data, f"{board_map[_id]}_hashrate", hashrate)
if devs:
boards = devs.get("DEVS")
if boards:
if len(boards) > 0:
board_map = {0: "left_chips", 1: "center_chips", 2: "right_chips"}
if "ID" in boards[0].keys():
id_key = "ID"
else:
id_key = "ASC"
offset = boards[0][id_key]
for board in boards:
_id = board[id_key] - offset
chips = board["Effective Chips"]
setattr(data, board_map[_id], chips)
if pools:
pool_1 = None
pool_2 = None
pool_1_user = None
pool_2_user = None
pool_1_quota = 1
pool_2_quota = 1
quota = 0
for pool in pools.get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
pool_1_quota = pool["Quota"]
elif not pool_2_user:
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if not pool.get("User") == pool_1_user:
if not pool_2_user == pool.get("User"):
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if pool_2_user and not pool_2_user == pool_1_user:
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_1_url = pool_1
if pool_1_user:
data.pool_1_user = pool_1_user
if pool_2:
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
if pool_2_user:
data.pool_2_user = pool_2_user
if quota:
data.pool_split = str(quota)
if not mac:
if api_get_miner_info:
try:
mac = await self.get_mac()
mac = api_get_miner_info["Msg"]["mac"]
return str(mac).upper()
except KeyError:
pass
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
logging.info(f"Failed to get mac: {self}")
mac = None
pass
if mac:
data.mac = mac
if api_summary:
try:
mac = api_summary["SUMMARY"][0]["MAC"]
return str(mac).upper()
except (KeyError, IndexError):
pass
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
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_version(
self, api_get_version: dict = None, api_summary: dict = None
) -> Tuple[Optional[str], Optional[str]]:
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
api_ver = await self.get_api_ver(api_get_version=api_get_version)
fw_ver = await self.get_fw_ver(
api_get_version=api_get_version, api_summary=api_summary
)
return miner_version(api_ver, fw_ver)
async def get_api_ver(self, api_get_version: dict = None) -> Optional[str]:
# Check to see if the version info is already cached
if self.api_ver:
return self.api_ver
if not api_get_version:
try:
api_get_version = await self.api.get_version()
except APIError:
pass
if api_get_version:
if "Code" in api_get_version.keys():
if api_get_version["Code"] == 131:
try:
api_ver = api_get_version["Msg"]
if not isinstance(api_ver, str):
api_ver = api_ver["api_ver"]
self.api_ver = api_ver.replace("whatsminer v", "")
except (KeyError, TypeError):
pass
else:
self.api.api_ver = self.api_ver
return self.api_ver
return self.api_ver
async def get_fw_ver(
self, api_get_version: dict = None, api_summary: dict = None
) -> Optional[str]:
# Check to see if the version info is already cached
if self.fw_ver:
return self.fw_ver
if not api_get_version:
try:
api_get_version = await self.api.get_version()
except APIError:
pass
if api_get_version:
if "Code" in api_get_version.keys():
if api_get_version["Code"] == 131:
try:
self.fw_ver = api_get_version["Msg"]["fw_ver"]
except (KeyError, TypeError):
pass
else:
return self.fw_ver
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
self.fw_ver = api_summary["SUMMARY"][0]["Firmware Version"].replace(
"'", ""
)
except (KeyError, IndexError):
pass
return self.fw_ver
async def get_hostname(self, api_get_miner_info: dict = None) -> Optional[str]:
if self.hostname:
return self.hostname
if not api_get_miner_info:
try:
api_get_miner_info = await self.api.get_miner_info()
except APIError:
return None # only one way to get this
if api_get_miner_info:
try:
self.hostname = api_get_miner_info["Msg"]["hostname"]
except KeyError:
return None
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]["MHS 1m"] / 1000000), 2)
except (KeyError, IndexError):
pass
async def get_hashboards(self, api_devs: dict = None) -> List[HashBoard]:
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
]
if not api_devs:
try:
api_devs = await self.api.devs()
except APIError:
pass
if api_devs:
try:
for board in api_devs["DEVS"]:
if len(hashboards) < board["ASC"] + 1:
hashboards.append(
HashBoard(
slot=board["ASC"], expected_chips=self.nominal_chips
)
)
self.ideal_hashboards += 1
hashboards[board["ASC"]].chip_temp = round(board["Chip Temp Avg"])
hashboards[board["ASC"]].temp = round(board["Temperature"])
hashboards[board["ASC"]].hashrate = round(
float(board["MHS 1m"] / 1000000), 2
)
hashboards[board["ASC"]].chips = board["Effective Chips"]
hashboards[board["ASC"]].missing = False
except (KeyError, IndexError):
pass
return hashboards
async def get_env_temp(self, api_summary: dict = None) -> Optional[float]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return api_summary["SUMMARY"][0]["Env Temp"]
except (KeyError, IndexError):
pass
async def get_wattage(self, api_summary: dict = None) -> Optional[int]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return api_summary["SUMMARY"][0]["Power"]
except (KeyError, IndexError):
pass
async def get_wattage_limit(self, api_summary: dict = None) -> Optional[int]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return api_summary["SUMMARY"][0]["Power Limit"]
except (KeyError, IndexError):
pass
async def get_fans(
self, api_summary: dict = None, api_get_psu: dict = None
) -> List[Fan]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
fans = [Fan(), Fan(), Fan(), Fan()]
if api_summary:
try:
if self.fan_count > 0:
fans = [
Fan(api_summary["SUMMARY"][0]["Fan Speed In"]),
Fan(api_summary["SUMMARY"][0]["Fan Speed Out"]),
Fan(),
Fan(),
]
except (KeyError, IndexError):
pass
return fans
async def get_fan_psu(
self, api_summary: dict = None, api_get_psu: dict = None
) -> Optional[int]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return int(api_summary["SUMMARY"][0]["Power Fanspeed"])
except (KeyError, IndexError):
pass
if not api_get_psu:
try:
api_get_psu = await self.api.get_psu()
except APIError:
pass
if api_get_psu:
try:
return int(api_get_psu["Msg"]["fan_speed"])
except (KeyError, TypeError):
pass
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
try:
pools = {}
for i, pool in enumerate(api_pools["POOLS"]):
pools[f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["User"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_errors(
self, api_summary: dict = None, api_get_error_code: dict = None
) -> List[MinerErrorData]:
errors = []
if not api_summary and not api_get_error_code:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
for i in range(api_summary["SUMMARY"][0]["Error Code Count"]):
err = api_summary["SUMMARY"][0].get(f"Error Code {i}")
if err:
errors.append(WhatsminerError(error_code=err))
except (KeyError, IndexError, ValueError, TypeError):
pass
if not api_get_error_code:
try:
api_get_error_code = await self.api.get_error_code()
except APIError:
pass
if api_get_error_code:
for err in api_get_error_code["Msg"]["error_code"]:
if isinstance(err, dict):
for code in err:
errors.append(WhatsminerError(error_code=int(code)))
else:
errors.append(WhatsminerError(error_code=int(err)))
return errors
async def get_nominal_hashrate(self, api_summary: dict = None):
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
nominal_hashrate = api_summary["SUMMARY"][0]["Factory GHS"]
if nominal_hashrate:
return round(nominal_hashrate / 1000, 2)
except (KeyError, IndexError):
pass
async def get_fault_light(self, api_get_miner_info: dict = None) -> bool:
data = None
if not api_get_miner_info:
try:
api_get_miner_info = await self.api.get_miner_info()
except APIError:
if not self.light:
self.light = False
if api_get_miner_info:
try:
self.light = not (api_get_miner_info["Msg"]["ledstat"] == "auto")
except KeyError:
pass
return self.light if self.light else False

View File

@@ -14,85 +14,57 @@
import ipaddress
import logging
from typing import Union
from collections import namedtuple
from typing import List, Optional, Tuple, Union
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 Fan, 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,124 +75,196 @@ 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 check_light(self) -> bool:
if not self.light:
self.light = False
return self.light
async def fault_light_off(self) -> bool:
return False
async def fault_light_on(self) -> bool:
return False
async def get_errors(self) -> list:
return []
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def get_mac(self) -> str:
return "00:00:00:00:00:00"
async def set_power_limit(self, wattage: int) -> bool:
return False
async def get_data(self) -> MinerData:
"""Get data from the miner.
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
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 get_mac(self) -> Optional[str]:
return None
board_offset = -1
fan_offset = -1
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
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
if not api_devdetails:
try:
api_devdetails = await self.api.devdetails()
except APIError:
pass
if model:
data.model = model
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 hostname:
data.hostname = hostname
logging.warning(f"Failed to get model for miner: {self}")
return None
if mac:
data.mac = mac
async def get_version(
self, api_version: dict = None
) -> Tuple[Optional[str], Optional[str]]:
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
return miner_version(
api_ver=await self.get_api_ver(api_version=api_version),
fw_ver=await self.get_fw_ver(api_version=api_version),
)
data.fault_light = await self.check_light()
async def get_api_ver(self, api_version: dict = None) -> Optional[str]:
if self.api_ver:
return self.api_ver
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 not api_version:
try:
api_version = await self.api.version()
except APIError:
pass
if not miner_data:
return data
if api_version:
try:
self.api_ver = api_version["VERSION"][0]["API"]
except (KeyError, IndexError):
pass
summary = miner_data.get("summary")[0]
pools = miner_data.get("pools")[0]
stats = miner_data.get("stats")[0]
return self.api_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)
async def get_fw_ver(self, api_version: dict = None) -> Optional[str]:
if self.fw_ver:
return self.fw_ver
if stats:
boards = stats.get("STATS")
if boards:
if len(boards) > 0:
if not api_version:
try:
api_version = await self.api.version()
except APIError:
pass
if api_version:
try:
self.fw_ver = api_version["VERSION"][0]["CGMiner"]
except (KeyError, IndexError):
pass
return self.fw_ver
async def get_hostname(self) -> Optional[str]:
try:
hn = await self.send_ssh_command("cat /proc/sys/kernel/hostname")
except (asyncssh.Error, OSError):
return None
if hn:
self.hostname = hn
return self.hostname
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
# get hr from API
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return round(
float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2
)
except (IndexError, KeyError, ValueError, TypeError):
pass
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
hashboards = []
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
board_offset = -1
boards = api_stats["STATS"]
if len(boards) > 1:
for board_num in range(1, 16, 5):
for _b_num in range(5):
b = boards[1].get(f"chain_acn{board_num + _b_num}")
@@ -230,96 +274,127 @@ 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://", ""
)
data.pool_1_url = pool_1
hashrate = boards[1].get(f"chain_rate{i}")
if hashrate:
hashboard.hashrate = round(float(hashrate) / 1000, 2)
if pool_1_user:
data.pool_1_user = pool_1_user
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
if pool_2:
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
return hashboards
if pool_2_user:
data.pool_2_user = pool_2_user
async def get_env_temp(self) -> Optional[float]:
return None
if quota:
data.pool_split = str(quota)
async def get_wattage(self) -> Optional[int]:
return None
return data
async def get_wattage_limit(self) -> Optional[int]:
return None
async def get_fans(self, api_stats: dict = None) -> List[Fan]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
fans_data = [Fan(), Fan(), Fan(), Fan()]
if api_stats:
try:
fan_offset = -1
for fan_num in range(1, 8, 4):
for _f_num in range(4):
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}")
if f and not f == 0 and fan_offset == -1:
fan_offset = fan_num
if fan_offset == -1:
fan_offset = 1
for fan in range(self.fan_count):
fans_data[fan] = Fan(
api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
)
except (KeyError, IndexError):
pass
return fans_data
async def get_fan_psu(self) -> Optional[int]:
return None
async def get_pools(self, api_pools: dict = None) -> List[dict]:
groups = []
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
try:
pools = {}
for i, pool in enumerate(api_pools["POOLS"]):
pools[f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["User"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_errors(self) -> List[MinerErrorData]:
return []
async def get_fault_light(self) -> bool:
return False
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
# X19 method, not sure compatibility
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
try:
rate_unit = api_stats["STATS"][1]["rate_unit"]
except KeyError:
rate_unit = "GH"
if rate_unit == "GH":
return round(ideal_rate / 1000, 2)
if rate_unit == "MH":
return round(ideal_rate / 1000000, 2)
else:
return round(ideal_rate, 2)
except (KeyError, IndexError):
pass

View File

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

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,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 T9(BaseMiner):
class T9(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 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

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

@@ -12,19 +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 M20(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20"
self.nominal_chips = 70
self.fan_count = 2
class M20V10(BaseMiner):
class M20V10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip

View File

@@ -12,19 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M20S(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20S"
self.nominal_chips = 66
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
@@ -33,10 +26,22 @@ class M20SV10(BaseMiner):
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 V20"
self.nominal_chips = 111
self.fan_count = 2
class M20SV30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20S V30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M20SV30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

View File

@@ -12,13 +12,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M20SPlus(BaseMiner):
class M20SPlusV30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20S+"
self.nominal_chips = 66
self.model = "M20S+ V30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M20S+ V30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

View File

@@ -12,13 +12,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M21(BaseMiner):
class M21V10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M21"
self.nominal_chips = 105
self.model = "M21 V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M21V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

View File

@@ -12,19 +12,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M21S(BaseMiner):
class M21SV20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M21S"
self.model = "M21S V20"
self.nominal_chips = 66
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,10 +35,13 @@ class M21SV60(BaseMiner):
self.fan_count = 2
class M21SV20(BaseMiner):
class M21SV70(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M21S V20"
self.nominal_chips = 66
self.model = "M21S V70"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M21SV70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

View File

@@ -12,13 +12,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M21SPlus(BaseMiner):
class M21SPlusV20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M21S+"
self.nominal_chips = 105
self.model = "M21S+ V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M21S+ V20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

View File

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

View File

@@ -12,10 +12,10 @@
# 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
from .M20 import M20V10
from .M20S import M20SV10, M20SV20, M20SV30
from .M20S_Plus import M20SPlusV30
from .M21 import M21V10
from .M21S import M21SV20, M21SV60, M21SV70
from .M21S_Plus import M21SPlusV20
from .M29 import M29V10

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.
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M30V10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30 V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30V20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30 V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30V20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

View File

@@ -12,19 +12,60 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M30S(BaseMiner):
class M30SV10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S"
self.nominal_chips = 148
self.model = "M30S V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SV50(BaseMiner):
class M30SV20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SV30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S V30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SV40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S V40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SV50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -33,16 +74,52 @@ class M30SV50(BaseMiner):
self.fan_count = 2
class M30SVG20(BaseMiner):
class M30SV60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VG20"
self.nominal_chips = 70
self.model = "M30S V60"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVE20(BaseMiner):
class M30SV70(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S V70"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SV80(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S V80"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SV80, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
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 M30SVE20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -51,10 +128,223 @@ class M30SVE20(BaseMiner):
self.fan_count = 2
class M30SVE10(BaseMiner):
class M30SVE30(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.model = "M30S VE30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVE40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VE40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVE50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VE50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVE60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VE60"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVE70(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VE70"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVE70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVF10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VF10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVF10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVF20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VF20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVF20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVF30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VF30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVF30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.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
class M30SVG20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VG20"
self.nominal_chips = 70
self.fan_count = 2
class M30SVG30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VG30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVG30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVG40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VG40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVG40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVH10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VH10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVH20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VH20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVH30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VH30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVH40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VH40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVH50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VH50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVH60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VH60"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVH60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SVI20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VI20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30SVI20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

View File

@@ -12,28 +12,144 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M30SPlus(BaseMiner):
class M30SPlusV10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+"
self.nominal_chips = 156
self.model = "M30S+ V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVG60(BaseMiner):
class M30SPlusV20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VG60"
self.nominal_chips = 86
self.model = "M30S+ V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVE40(BaseMiner):
class M30SPlusV30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ V30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusV40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ V40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusV50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ V50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusV60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ V60"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusV70(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ V70"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusV80(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ V80"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V80, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusV90(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ V90"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V90, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusV100(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ V100"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V100, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVE30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VE30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVE40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
@@ -42,10 +158,214 @@ class M30SPlusVE40(BaseMiner):
self.fan_count = 2
class M30SPlusVF20(BaseMiner):
class M30SPlusVE50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VE50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVE60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VE60"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVE70(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VE70"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVE80(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VE80"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE80, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVE90(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VE90"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE90, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVE100(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VE100"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE100, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
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 M30SPlusVF30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VF30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VF30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M36SPlusVG30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M36S+ VG30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M36SPlusVG30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVG30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VG30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VG30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
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 M30SPlusVG50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VG50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VG50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVG60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VG60"
self.nominal_chips = 86
self.fan_count = 2
class M30SPlusVH10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VH10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVH20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VH20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.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 M30SPlusVH40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VH40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusVH50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VH50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.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,31 +12,249 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M30SPlusPlus(BaseMiner):
class M30SPlusPlusV10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++"
self.model = "M30S++ V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusV20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ V20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVE30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VE30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VE30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVE40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VE40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VE40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVE50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VE50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VE50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVF40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VF40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VF40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVG30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VG30"
self.nominal_chips = 111
self.fan_count = 2
class M30SPlusPlusVG30(BaseMiner):
class M30SPlusPlusVG40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ V30"
self.nominal_chips = 111
self.fan_count = 2
class M30SPlusPlusVG40(BaseMiner):
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 M30SPlusPlusVG50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VG50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VG50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVH10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVH20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVH30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVH40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVH50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.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
class M30SPlusPlusVH70(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH70"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVH80(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH80"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH80, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVH90(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH90"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH90, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVH100(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH100"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH100, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVJ20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VJ20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VJ20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M30SPlusPlusVJ30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VJ30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VJ30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

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.
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M31V10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31 V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31V20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31 V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31V20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

View File

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

View File

@@ -12,13 +12,138 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M31S(BaseMiner):
class M31SV10(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.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 M31SV30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31SV30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SV40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31SV40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SV50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31SV50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
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
class M31SV80(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V80"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31SV80, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SV90(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V90"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31SV90, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SVE10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S VE10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31SVE10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SVE20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S VE20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31SVE20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SVE30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S VE30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M31SVE30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2

View File

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

View File

@@ -12,22 +12,243 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M31SPlus(BaseMiner):
class M31SPlusV10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+"
self.nominal_chips = 78
self.model = "M31S+ V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SPlusVE20(BaseMiner):
class M31SPlusV20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.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 M31SPlusV50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ V50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.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
class M31SPlusV100(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V100"
self.nominal_chips = 111
self.fan_count = 2
class M31SPlusVE10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VE10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
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 M31SPlusVE30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VE30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SPlusVE40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VE40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SPlusVE50(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VE50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SPlusVE60(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VE60"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SPlusVE80(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VE80"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VE80, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SPlusVF20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VF20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VF20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SPlusVF30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VF30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VF30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SPlusVG20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VG20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VG20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M31SPlusVG30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VG30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VG30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.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

View File

@@ -12,19 +12,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M32(BaseMiner):
class M32V10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M32"
self.nominal_chips = 74
self.model = "M32 V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M32V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2
class M32V20(BaseMiner):
class M32V20(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 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,53 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M33V10(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M33 V10"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M33V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 0
class M33V20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M33 V20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M33V20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 0
class M33V30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M33 V30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M33V30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 0

View File

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

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.
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M33SPlusVH20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M33S+ VH20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 0
class M33SPlusVH30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M33S+ VH30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S+ VH30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 0

View File

@@ -0,0 +1,53 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import warnings
from pyasic.miners._types.makes import WhatsMiner
class M33SPlusPlusVH20(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M33S++ VH20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 0
class M33SPlusPlusVH30(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M33S++ VH30"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 0
class M33SPlusPlusVG40(WhatsMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M33S++ VG40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VG40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 0

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