Compare commits

..

1 Commits

Author SHA1 Message Date
UpstreamData
7a1936d095 improve how the fault light looks on whatsminers 2022-08-11 11:59:19 -06:00
164 changed files with 1698 additions and 5282 deletions

View File

@@ -1,17 +0,0 @@
[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,4 +6,3 @@ pyvenv.cfg
.env/ .env/
bin/ bin/
lib/ lib/
.idea/

View File

@@ -3,18 +3,13 @@ repos:
rev: v4.3.0 rev: v4.3.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml - id: check-yaml
- id: check-added-large-files - id: check-added-large-files
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.6.0 rev: 22.6.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: isort (python)
- repo: local - repo: local
hooks: hooks:
- id: unittest - id: unittest

View File

@@ -7,40 +7,57 @@
[![Read the Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/) [![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) [![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) [![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
## Documentation and Supported Miners ## Documentation
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/). Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/)
Supported miners are listed in the docs, [here](https://pyasic.readthedocs.io/en/latest/miners/supported_types/). ## Usage
## Installation ### Standard Usage
You can install pyasic directly from pip with the command `pip install pyasic`. 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)
## Developer Setup ### Developers
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. 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.
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> <br>
This repo uses poetry for dependencies, which can be installed by following the guide on their website [here](https://python-poetry.org/docs/#installation). To write your own custom programs with this repo, you have many options.
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. 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.
Finally, initialize pre-commit hooks with `poetry run pre-commit install`. There are 2 main ways to get a miner and it's functions via scanning or via the MinerFactory.
### Documentation Testing
Testing the documentation can be done by running `poetry run mkdocs serve`, whcih will serve the documentation locally on port 8000.
## Interfacing with miners programmatically
There are 2 main ways to get a miner (and the functions attached to it), via scanning or via the `MinerFactory()`.
#### Scanning for miners #### Scanning for miners
```python ```python
import asyncio import asyncio
import sys
from pyasic.network import MinerNetwork 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 # define asynchronous function to scan for miners
async def scan_and_get_data(): async def scan_and_get_data():
@@ -67,19 +84,26 @@ if __name__ == "__main__":
asyncio.run(scan_and_get_data()) asyncio.run(scan_and_get_data())
``` ```
</br>
#### Getting a miner if you know the IP #### Getting a miner if you know the IP
```python ```python
import asyncio import asyncio
import sys
from pyasic import get_miner from pyasic.miners.miner_factory import MinerFactory
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# define asynchronous function to get miner and data # define asynchronous function to get miner and data
async def get_miner_data(miner_ip: str): async def get_miner_data(miner_ip: str):
# Use MinerFactory to get miner # Use MinerFactory to get miner
# MinerFactory is a singleton, so we can just get the instance in place # MinerFactory is a singleton, so we can just get the instance in place
miner = await get_miner(miner_ip) miner = await MinerFactory().get_miner(miner_ip)
# Get data from the miner # Get data from the miner
data = await miner.get_data() data = await miner.get_data()
@@ -96,13 +120,19 @@ If needed, this library exposes a wrapper for the miner API that can be used for
#### List available API commands #### List available API commands
```python ```python
import asyncio import asyncio
import sys
from pyasic import get_miner from pyasic.miners.miner_factory import MinerFactory
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
async def get_api_commands(miner_ip: str): async def get_api_commands(miner_ip: str):
# Get the miner # Get the miner
miner = await get_miner(miner_ip) miner = await MinerFactory().get_miner(miner_ip)
# List all available commands # List all available commands
print(miner.api.get_commands()) print(miner.api.get_commands())
@@ -118,13 +148,19 @@ The miner API commands will raise an `APIError` if they fail with a bad status c
```python ```python
import asyncio import asyncio
import sys
from pyasic import get_miner from pyasic.miners.miner_factory import MinerFactory
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
async def get_api_commands(miner_ip: str): async def get_api_commands(miner_ip: str):
# Get the miner # Get the miner
miner = await get_miner(miner_ip) miner = await MinerFactory().get_miner(miner_ip)
# Run the devdetails command # Run the devdetails command
# This is equivalent to await miner.api.send_command("devdetails") # This is equivalent to await miner.api.send_command("devdetails")

View File

@@ -23,11 +23,3 @@
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
<br>
## Innosilicon Error Codes
::: pyasic.data.error_codes.InnosiliconError
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -101,174 +101,3 @@ async def gather_miner_data(): # define async scan function to allow awaiting
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(gather_miner_data()) 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,61 +57,3 @@
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 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,14 +43,6 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## S19 XP
::: pyasic.miners.antminer.bmminer.X19.S19_XP.BMMinerS19XP
handler: python
options:
show_root_heading: false
heading_level: 4
## T19 ## T19
::: pyasic.miners.antminer.bmminer.X19.T19.BMMinerT19 ::: pyasic.miners.antminer.bmminer.X19.T19.BMMinerT19
@@ -58,45 +50,3 @@
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 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,16 +1,6 @@
# pyasic # pyasic
## X9 Models ## X9 Models
## X9 (BOS)
::: pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9
handler: python
options:
show_root_heading: false
heading_level: 4
## S9 ## S9
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9 ::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9

View File

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

View File

@@ -89,7 +89,7 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## M30S++VG40 ## M30S+VG40
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40 ::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
handler: python handler: python
@@ -97,25 +97,9 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## M30S++VH60
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVH60
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S ## M31S
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S
handler: python
options:
show_root_heading: false
heading_level: 4
## M31SV70
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S ::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S
handler: python handler: python
options: options:
@@ -138,62 +122,6 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## M31S+V30
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV30
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+V40
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV40
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+V60
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV60
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+V80
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV80
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+V90
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV90
handler: python
options:
show_root_heading: false
heading_level: 4
## M32
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32
handler: python
options:
show_root_heading: false
heading_level: 4
## M32V20
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20
handler: python
options:
show_root_heading: false
heading_level: 4
## M32S ## M32S
::: pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S ::: pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S
@@ -201,19 +129,3 @@
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## M34S+
::: pyasic.miners.whatsminer.btminer.M3X.M34S_Plus.BTMinerM34SPlus
handler: python
options:
show_root_heading: false
heading_level: 4
## M34S+VE10
::: pyasic.miners.whatsminer.btminer.M3X.M34S_Plus.BTMinerM34SPlusVE10
handler: python
options:
show_root_heading: false
heading_level: 4

View File

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

View File

@@ -11,6 +11,7 @@ nav:
- BTMiner: "miners/backends/btminer.md" - BTMiner: "miners/backends/btminer.md"
- CGMiner: "miners/backends/cgminer.md" - CGMiner: "miners/backends/cgminer.md"
- Hiveon: "miners/backends/hiveon.md" - Hiveon: "miners/backends/hiveon.md"
- Classes: - Classes:
- Antminer X9: "miners/antminer/X9.md" - Antminer X9: "miners/antminer/X9.md"
- Antminer X17: "miners/antminer/X17.md" - Antminer X17: "miners/antminer/X17.md"
@@ -21,14 +22,14 @@ nav:
- Avalon 10X: "miners/avalonminer/A10X.md" - Avalon 10X: "miners/avalonminer/A10X.md"
- Whatsminer M2X: "miners/whatsminer/M2X.md" - Whatsminer M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md" - Whatsminer M3X: "miners/whatsminer/M3X.md"
- Whatsminer M5X: "miners/whatsminer/M5X.md"
- Innosilicon T3X: "miners/innosilicon/T3X.md"
- Network: - Network:
- Miner Network: "network/miner_network.md" - Miner Network: "network/miner_network.md"
- Miner Network Range: "network/miner_network_range.md" - Miner Network Range: "network/miner_network_range.md"
- Dataclasses: - Data:
- Miner Data: "data/miner_data.md" - Miner Data: "data/miner_data.md"
- Error Codes: "data/error_codes.md" - Error Codes: "data/error_codes.md"
- Config:
- Miner Config: "config/miner_config.md" - Miner Config: "config/miner_config.md"
- Advanced: - Advanced:
- Miner APIs: - Miner APIs:

653
poetry.lock generated
View File

@@ -11,13 +11,13 @@ idna = ">=2.8"
sniffio = ">=1.1" sniffio = ">=1.1"
[package.extras] [package.extras]
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
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)"] test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
trio = ["trio (>=0.16)"] trio = ["trio (>=0.16)"]
[[package]] [[package]]
name = "asyncssh" name = "asyncssh"
version = "2.12.0" version = "2.11.0"
description = "AsyncSSH: Asynchronous SSHv2 client and server library" description = "AsyncSSH: Asynchronous SSHv2 client and server library"
category = "main" category = "main"
optional = false optional = false
@@ -38,7 +38,7 @@ pywin32 = ["pywin32 (>=227)"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2022.9.24" version = "2022.6.15"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
category = "main" category = "main"
optional = false optional = false
@@ -55,36 +55,9 @@ python-versions = "*"
[package.dependencies] [package.dependencies]
pycparser = "*" 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]] [[package]]
name = "cryptography" name = "cryptography"
version = "38.0.1" version = "37.0.4"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main" category = "main"
optional = false optional = false
@@ -95,56 +68,11 @@ cffi = ">=1.12"
[package.extras] [package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 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)"] ssh = ["bcrypt (>=3.1.5)"]
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
[[package]]
name = "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]] [[package]]
name = "h11" name = "h11"
@@ -187,181 +115,19 @@ rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
sniffio = "*" sniffio = "*"
[package.extras] [package.extras]
brotli = ["brotli", "brotlicffi"] brotli = ["brotlicffi", "brotli"]
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"]
http2 = ["h2 (>=3,<5)"] http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"] 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]] [[package]]
name = "idna" name = "idna"
version = "3.4" version = "3.3"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.5" 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]] [[package]]
name = "passlib" name = "passlib"
version = "1.7.4" version = "1.7.4"
@@ -373,37 +139,9 @@ python-versions = "*"
[package.extras] [package.extras]
argon2 = ["argon2-cffi (>=18.2.0)"] argon2 = ["argon2-cffi (>=18.2.0)"]
bcrypt = ["bcrypt (>=3.1.0)"] bcrypt = ["bcrypt (>=3.1.0)"]
build_docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"]
totp = ["cryptography"] 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]] [[package]]
name = "pyaml" name = "pyaml"
version = "21.10.1" version = "21.10.1"
@@ -424,57 +162,13 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "pymdown-extensions" name = "pyyaml"
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" version = "6.0"
description = "YAML parser and emitter for Python" description = "YAML parser and emitter for Python"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" 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]] [[package]]
name = "rfc3986" name = "rfc3986"
version = "1.5.0" version = "1.5.0"
@@ -489,34 +183,13 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
[package.extras] [package.extras]
idna2008 = ["idna"] 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]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.0" version = "1.2.0"
description = "Sniff out which async library your code is running under" description = "Sniff out which async library your code is running under"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.5"
[[package]] [[package]]
name = "toml" name = "toml"
@@ -534,50 +207,10 @@ category = "main"
optional = false optional = false
python-versions = ">=3.7" 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] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "98c3026a6f27c29c0357bbfa07166d6d8b604a869f3a802adc3bb3610f86964c" content-hash = "8d93eafd928d7fed4b0a00d13e46982c2d4310c37acb2faec7e7a477b3f35e9c"
[metadata.files] [metadata.files]
anyio = [ anyio = [
@@ -585,12 +218,12 @@ anyio = [
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
] ]
asyncssh = [ asyncssh = [
{file = "asyncssh-2.12.0-py3-none-any.whl", hash = "sha256:6841c4242c606fd51188c974ec2f4887efeec67ecdfa5b84140711dacd985ab3"}, {file = "asyncssh-2.11.0-py3-none-any.whl", hash = "sha256:7302348cbd54c58d3259da17f13e77912de1b005e366b15c8b183d948c8a91a8"},
{file = "asyncssh-2.12.0.tar.gz", hash = "sha256:274101322c4b941823aeed8e1ab6e7be5191686c6db2d2bd35afeba30505e780"}, {file = "asyncssh-2.11.0.tar.gz", hash = "sha256:59c36ce77ba9dda8dd57ad875776e7105ddb1fa851bc039bb3aeadeac4f67b56"},
] ]
certifi = [ certifi = [
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
] ]
cffi = [ cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
@@ -658,61 +291,29 @@ cffi = [
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, {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 = [ cryptography = [
{file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"},
{file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"},
{file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"},
{file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"},
{file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"},
{file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"},
{file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"},
{file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"},
{file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"},
{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 = [ h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
@@ -726,108 +327,14 @@ httpx = [
{file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"},
{file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, {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 = [ idna = [
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
]
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 = [ passlib = [
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, {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 = [ pyaml = [
{file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"}, {file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"},
{file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"}, {file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"},
@@ -836,19 +343,7 @@ pycparser = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
] ]
pymdown-extensions = [ pyyaml = [
{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_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-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
@@ -856,13 +351,6 @@ 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-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-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {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-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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
@@ -890,25 +378,13 @@ PyYAML = [
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, {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 = [ rfc3986 = [
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, {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 = [ sniffio = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
] ]
toml = [ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
@@ -918,38 +394,3 @@ typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, {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,14 +13,39 @@
# limitations under the License. # limitations under the License.
import asyncio import asyncio
import ipaddress
import json import json
import logging import ipaddress
import re
import warnings import warnings
import logging
from typing import Union from typing import Union
from pyasic.errors import APIError, APIWarning
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."
class BaseMinerAPI: class BaseMinerAPI:
@@ -35,81 +60,6 @@ class BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated") raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls) return object.__new__(cls)
def __repr__(self):
return f"{self.__class__.__name__}: {str(self.ip)}"
async def send_command(
self,
command: Union[str, bytes],
parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
allow_warning: bool = True,
) -> dict:
"""Send an API command to the miner and return the result.
Parameters:
command: The command to sent to the miner.
parameters: Any additional parameters to be sent with the command.
ignore_errors: Whether to raise APIError when the command returns an error.
allow_warning: Whether to warn if the command fails.
Returns:
The return data from the API command parsed from JSON into a dict.
"""
logging.debug(f"{self} - (Send Privileged Command) - {command} " + f'with args {parameters}' if parameters else '')
# create the command
cmd = {"command": command}
if parameters:
cmd["parameter"] = parameters
# send the command
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
data = self._load_api_data(data)
# check for if the user wants to allow errors to return
if not ignore_errors:
# validate the command succeeded
validation = self._validate_command_output(data)
if not validation[0]:
if allow_warning:
logging.warning(
f"{self.ip}: API Command Error: {command}: {validation[1]}"
)
raise APIError(validation[1])
logging.debug(f"{self} - (Send Command) - Received data.")
return data
# Privileged command handler, only used by whatsminers, defined here for consistency.
async def send_privileged_command(self, *args, **kwargs) -> dict:
return await self.send_command(*args, **kwargs)
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner.
Parameters:
*commands: The commands to send as a multicommand to the miner.
allow_warning: A boolean to supress APIWarnings.
"""
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# standard format doesn't work for X19
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
return {}
logging.debug(f"{self} - (Multicommand) - Received data")
return data
@property
def commands(self) -> list:
return self.get_commands()
def get_commands(self) -> list: def get_commands(self) -> list:
"""Get a list of command accessible to a specific type of API on the miner. """Get a list of command accessible to a specific type of API on the miner.
@@ -122,8 +72,8 @@ class BaseMinerAPI:
# each function in self # each function in self
dir(self) dir(self)
if callable(getattr(self, func)) and if callable(getattr(self, func)) and
# no __ or _ methods # no __ methods
not func.startswith("__") and not func.startswith("_") and not func.startswith("__") and
# remove all functions that are in this base class # remove all functions that are in this base class
func func
not in [ not in [
@@ -147,22 +97,81 @@ If you are sure you want to use this command please use API.send_command("{comma
) )
return return_commands return return_commands
async def _send_bytes(self, data: bytes) -> bytes: 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.
"""
try: try:
# get reader and writer streams # get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), self.port) reader, writer = await asyncio.open_connection(str(self.ip), self.port)
# handle OSError 121 # handle OSError 121
except OSError as e: except OSError as e:
if getattr(e, "winerror") == "121": if e.winerror == "121":
logging.warning(f"{self} - ([Hidden] Send Bytes) - Semaphore timeout expired.") logging.warning("Semaphore Timeout has Expired.")
return b"{}" return {}
# create the command
cmd = {"command": command}
if parameters:
cmd["parameter"] = parameters
# send the command # send the command
writer.write(data) writer.write(json.dumps(cmd).encode("utf-8"))
await writer.drain() await writer.drain()
# instantiate data # instantiate data
ret_data = b"" data = b""
# loop to receive all the data # loop to receive all the data
try: try:
@@ -170,15 +179,26 @@ If you are sure you want to use this command please use API.send_command("{comma
d = await reader.read(4096) d = await reader.read(4096)
if not d: if not d:
break break
ret_data += d data += d
except Exception as e: except Exception as e:
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}") logging.warning(f"{self.ip}: API Command Error: - {e}")
data = self._load_api_data(data)
# close the connection # close the connection
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
return ret_data # check for if the user wants to allow errors to return
if not ignore_errors:
# validate the command succeeded
validation = self._validate_command_output(data)
if not validation[0]:
if not x19_command:
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
raise APIError(validation[1])
return data
@staticmethod @staticmethod
def _validate_command_output(data: dict) -> tuple: def _validate_command_output(data: dict) -> tuple:
@@ -209,39 +229,32 @@ If you are sure you want to use this command please use API.send_command("{comma
@staticmethod @staticmethod
def _load_api_data(data: bytes) -> dict: def _load_api_data(data: bytes) -> dict:
# some json from the API returns with a null byte (\x00) on the end str_data = None
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:]}"
# 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: 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) parsed_data = json.loads(str_data)
# handle bad json
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
raise APIError(f"Decode Error {e}: {str_data}") raise APIError(f"Decode Error {e}: {str_data}")
return parsed_data return parsed_data

View File

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

View File

@@ -166,7 +166,7 @@ class BOSMinerAPI(BaseMinerAPI):
return await self.send_command("estats") return await self.send_command("estats")
async def check(self, command: str) -> dict: async def check(self, command: str) -> dict:
"""Check if the command `command` exists in BOSMiner. """Check if the command command exists in BOSMiner.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>

View File

@@ -12,27 +12,28 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import base64 import asyncio
import binascii
import hashlib
import json
import logging
import re import re
import json
import hashlib
import binascii
import base64
import logging
from typing import Union from typing import Union
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from passlib.handlers.md5_crypt import md5_crypt from passlib.handlers.md5_crypt import md5_crypt
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from pyasic.API import BaseMinerAPI from pyasic.API import BaseMinerAPI, APIError
from pyasic.errors import APIError
from pyasic.settings import PyasicSettings from pyasic.settings import PyasicSettings
### IMPORTANT ### ### IMPORTANT ###
# you need to change the password of the miners using the Whatsminer # 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 # 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 # 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 # or the privileged API will not work using admin as the password. If
# you change the password, you can pass that to this class as pwd, # you change the password, you can pass that to the this class as pwd,
# or add it as the Whatsminer_pwd in the settings.toml file. # or add it as the Whatsminer_pwd in the settings.toml file.
@@ -81,7 +82,7 @@ def _add_to_16(string: str) -> bytes:
def parse_btminer_priviledge_data(token_data: dict, data: dict): def parse_btminer_priviledge_data(token_data: dict, data: dict):
"""Parses data returned from the BTMiner privileged API. """Parses data returned from the BTMiner privileged API.
Parses data from the BTMiner privileged API using the token Parses data from the BTMiner privileged API using the the token
from the API in an AES format. from the API in an AES format.
Parameters: Parameters:
@@ -185,26 +186,58 @@ class BTMinerAPI(BaseMinerAPI):
self.pwd = pwd self.pwd = pwd
self.current_token = None self.current_token = None
async def send_privileged_command( async def send_command(
self, command: Union[str, bytes], ignore_errors: bool = False, **kwargs self,
command: Union[str, bytes],
parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
**kwargs,
) -> dict: ) -> dict:
logging.debug(f"{self} - (Send Privileged Command) - {command} " + f'with args {kwargs}' if len(kwargs) > 0 else '') # check if command is a string
command = {"cmd": command} # if its bytes its encoded and needs to be sent raw
for kwarg in kwargs: if isinstance(command, str):
if kwargs[kwarg]: # if it is a string, put it into the standard command format
command[kwarg] = kwargs[kwarg] command = json.dumps({"command": command}).encode("utf-8")
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
data = await self._send_bytes(enc_command)
data = self._load_api_data(data)
try: try:
data = parse_btminer_priviledge_data(self.current_token, data) # 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: except Exception as e:
logging.info(f"{str(self.ip)}: {e}") logging.info(f"{str(self.ip)}: {e}")
data = self._load_api_data(data)
# close the connection
writer.close()
await writer.wait_closed()
# check if the returned data is encoded
if "enc" in data.keys():
# try to parse the encoded data
try:
data = parse_btminer_priviledge_data(self.current_token, data)
except Exception as e:
logging.info(f"{str(self.ip)}: {e}")
if not ignore_errors: if not ignore_errors:
# if it fails to validate, it is likely an error # if it fails to validate, it is likely an error
validation = self._validate_command_output(data) validation = self._validate_command_output(data)
@@ -223,7 +256,6 @@ class BTMinerAPI(BaseMinerAPI):
An encoded token and md5 password, which are used for the privileged API. An encoded token and md5 password, which are used for the privileged API.
</details> </details>
""" """
logging.debug(f"{self} - (Get Token) - Getting token")
# get the token # get the token
data = await self.send_command("get_token") data = await self.send_command("get_token")
@@ -246,7 +278,6 @@ class BTMinerAPI(BaseMinerAPI):
"host_sign": host_sign, "host_sign": host_sign,
"host_passwd_md5": host_passwd_md5, "host_passwd_md5": host_passwd_md5,
} }
logging.debug(f"{self} - (Get Token) - Gathered token data: {self.current_token}")
return self.current_token return self.current_token
#### PRIVILEGED COMMANDS #### #### PRIVILEGED COMMANDS ####
@@ -288,18 +319,46 @@ class BTMinerAPI(BaseMinerAPI):
A dict from the API to confirm the pools were updated. A dict from the API to confirm the pools were updated.
</details> </details>
""" """
return await self.send_privileged_command( # get the token and password from the miner
"update_pools", token_data = await self.get_token()
pool1=pool_1,
worker1=worker_1, # parse pool data
passwd1=passwd_1, if not pool_1:
pool2=pool_2, raise APIError("No pools set.")
worker2=worker_2, elif pool_2 and pool_3:
passwd2=passwd_2, command = {
pool3=pool_3, "cmd": "update_pools",
worker3=worker_3, "pool1": pool_1,
passwd3=passwd_3, "worker1": worker_1,
) "passwd1": passwd_1,
"pool2": pool_2,
"worker2": worker_2,
"passwd2": passwd_2,
"pool3": pool_3,
"worker3": worker_3,
"passwd3": passwd_3,
}
elif pool_2:
command = {
"cmd": "update_pools",
"pool1": pool_1,
"worker1": worker_1,
"passwd1": passwd_1,
"pool2": pool_2,
"worker2": worker_2,
"passwd2": passwd_2,
}
else:
command = {
"cmd": "update_pools",
"pool1": pool_1,
"worker1": worker_1,
"passwd1": passwd_1,
}
# encode the command with the token data
enc_command = create_privileged_cmd(token_data, command)
# send the command
return await self.send_command(enc_command)
async def restart(self) -> dict: async def restart(self) -> dict:
"""Restart BTMiner using the API. """Restart BTMiner using the API.
@@ -313,7 +372,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the restart. A reply informing of the restart.
</details> </details>
""" """
return await self.send_privileged_command("restart_btminer") command = {"cmd": "restart_btminer"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def power_off(self, respbefore: bool = True) -> dict: async def power_off(self, respbefore: bool = True) -> dict:
"""Power off the miner using the API. """Power off the miner using the API.
@@ -330,8 +392,12 @@ class BTMinerAPI(BaseMinerAPI):
</details> </details>
""" """
if respbefore: if respbefore:
return await self.send_privileged_command("power_off", respbefore="true") command = {"cmd": "power_off", "respbefore": "true"}
return await self.send_privileged_command("power_off", respbefore="false") else:
command = {"cmd": "power_off", "respbefore": "false"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def power_on(self) -> dict: async def power_on(self) -> dict:
"""Power on the miner using the API. """Power on the miner using the API.
@@ -346,7 +412,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of powering on. A reply informing of the status of powering on.
</details> </details>
""" """
return await self.send_privileged_command("power_on") command = {"cmd": "power_on"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def reset_led(self) -> dict: async def reset_led(self) -> dict:
"""Reset the LED on the miner using the API. """Reset the LED on the miner using the API.
@@ -361,7 +430,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of resetting the LED. A reply informing of the status of resetting the LED.
</details> </details>
""" """
return await self.set_led(auto=True) command = {"cmd": "set_led", "param": "auto"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def set_led( async def set_led(
self, self,
@@ -389,11 +461,19 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting the LED. A reply informing of the status of setting the LED.
</details> </details>
""" """
if auto: if not auto:
return await self.send_privileged_command("set_led", param=auto) command = {
return await self.send_privileged_command( "cmd": "set_led",
"set_led", color=color, period=period, duration=duration, start=start "color": color,
) "period": period,
"duration": duration,
"start": start,
}
else:
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, ignore_errors=True)
async def set_low_power(self) -> dict: async def set_low_power(self) -> dict:
"""Set low power mode on the miner using the API. """Set low power mode on the miner using the API.
@@ -408,7 +488,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting low power mode. A reply informing of the status of setting low power mode.
</details> </details>
""" """
return await self.send_privileged_command("set_low_power") command = {"cmd": "set_low_power"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def update_firmware(self): # noqa - static async def update_firmware(self): # noqa - static
"""Not implemented.""" """Not implemented."""
@@ -426,7 +509,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of the reboot. A reply informing of the status of the reboot.
</details> </details>
""" """
return await self.send_privileged_command("reboot") command = {"cmd": "reboot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def factory_reset(self) -> dict: async def factory_reset(self) -> dict:
"""Reset the miner to factory defaults. """Reset the miner to factory defaults.
@@ -438,7 +524,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of the reset. A reply informing of the status of the reset.
</details> </details>
""" """
return await self.send_privileged_command("factory_reset") command = {"cmd": "factory_reset"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def update_pwd(self, old_pwd: str, new_pwd: str) -> dict: async def update_pwd(self, old_pwd: str, new_pwd: str) -> dict:
"""Update the admin user's password. """Update the admin user's password.
@@ -465,10 +554,11 @@ class BTMinerAPI(BaseMinerAPI):
f"New password too long, the max length is 8. " f"New password too long, the max length is 8. "
f"Password size: {len(new_pwd.encode('utf-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)
try: try:
data = await self.send_privileged_command( data = await self.send_command(enc_command)
"update_pwd", old=old_pwd, new=new_pwd
)
except APIError as e: except APIError as e:
raise e raise e
self.pwd = new_pwd self.pwd = new_pwd
@@ -497,9 +587,10 @@ class BTMinerAPI(BaseMinerAPI):
f"range. Please set a % between -10 and " f"range. Please set a % between -10 and "
f"100" f"100"
) )
return await self.send_privileged_command( command = {"cmd": "set_target_freq", "percent": str(percent)}
"set_target_freq", percent=str(percent) token_data = await self.get_token()
) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def enable_fast_boot(self) -> dict: async def enable_fast_boot(self) -> dict:
"""Turn on fast boot. """Turn on fast boot.
@@ -515,7 +606,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of enabling fast boot. A reply informing of the status of enabling fast boot.
</details> </details>
""" """
return await self.send_privileged_command("enable_btminer_fast_boot") command = {"cmd": "enable_btminer_fast_boot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def disable_fast_boot(self) -> dict: async def disable_fast_boot(self) -> dict:
"""Turn off fast boot. """Turn off fast boot.
@@ -531,7 +625,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of disabling fast boot. A reply informing of the status of disabling fast boot.
</details> </details>
""" """
return await self.send_privileged_command("disable_btminer_fast_boot") command = {"cmd": "disable_btminer_fast_boot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def enable_web_pools(self) -> dict: async def enable_web_pools(self) -> dict:
"""Turn on web pool updates. """Turn on web pool updates.
@@ -547,7 +644,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of enabling web pools. A reply informing of the status of enabling web pools.
</details> </details>
""" """
return await self.send_privileged_command("enable_web_pools") command = {"cmd": "enable_web_pools"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def disable_web_pools(self) -> dict: async def disable_web_pools(self) -> dict:
"""Turn off web pool updates. """Turn off web pool updates.
@@ -563,7 +663,10 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of disabling web pools. A reply informing of the status of disabling web pools.
</details> </details>
""" """
return await self.send_privileged_command("disable_web_pools") command = {"cmd": "disable_web_pools"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def set_hostname(self, hostname: str) -> dict: async def set_hostname(self, hostname: str) -> dict:
"""Set the hostname of the miner. """Set the hostname of the miner.
@@ -581,10 +684,13 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting the hostname. A reply informing of the status of setting the hostname.
</details> </details>
""" """
return await self.send_privileged_command("set_hostname", hostname=hostname) command = {"cmd": "set_hostname", "hostname": hostname}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def set_power_pct(self, percent: int) -> dict: async def set_power_pct(self, percent: int) -> dict:
"""Set the power percentage of the miner based on current power. Used for temporary adjustment. """Set the power percentage of the miner.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>
@@ -606,7 +712,10 @@ class BTMinerAPI(BaseMinerAPI):
f"range. Please set a % between 0 and " f"range. Please set a % between 0 and "
f"100" f"100"
) )
return await self.send_privileged_command("set_power_pct", percent=str(percent)) command = {"cmd": "set_power_pct", "percent": str(percent)}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def pre_power_on(self, complete: bool, msg: str) -> dict: async def pre_power_on(self, complete: bool, msg: str) -> dict:
"""Configure or check status of pre power on. """Configure or check status of pre power on.
@@ -637,125 +746,13 @@ class BTMinerAPI(BaseMinerAPI):
'"adjust continue"]' '"adjust continue"]'
) )
if complete: if complete:
return await self.send_privileged_command( complete = "true"
"pre_power_on", complete="true", msg=msg else:
) complete = "false"
return await self.send_privileged_command( command = {"cmd": "pre_power_on", "complete": complete, "msg": msg}
"pre_power_on", complete="false", msg=msg token_data = await self.get_token()
) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
### ADDED IN V2.0.5 Whatsminer API ###
async def set_temp_offset(self, temp_offset: int):
"""Set the offset of miner hash board target temperature.
<details>
<summary>Expand</summary>
Set the offset of miner hash board target temperature, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
temp_offset: Target temperature offset.
Returns:
A reply informing of the status of setting temp offset.
</details>
"""
if not -30 < temp_offset < 0:
raise APIError(
f"Temp offset is outside of the allowed "
f"range. Please set a number between -30 and "
f"0."
)
return await self.send_privileged_command("set_temp_offset", temp_offset=temp_offset)
async def adjust_power_limit(self, power_limit: int):
"""Set the upper limit of the miner's power. Cannot be higher than the ordinary power of the machine.
<details>
<summary>Expand</summary>
Set the upper limit of the miner's power, only works after
changing the password of the miner using the Whatsminer tool.
The miner will reboot after this is set.
Parameters:
power_limit: New power limit.
Returns:
A reply informing of the status of setting power limit.
</details>
"""
return await self.send_privileged_command("adjust_power_limit", power_limit=power_limit)
async def adjust_upfreq_speed(self, upfreq_speed: int):
"""Set the upfreq speed, 0 is the normal speed, 9 is the fastest speed.
<details>
<summary>Expand</summary>
Set the upfreq speed, 0 is the normal speed, 9 is the fastest speed, only works after
changing the password of the miner using the Whatsminer tool.
The faster the speed, the greater the final hash rate and power deviation, and the stability
may be impacted. Fast boot mode cannot be used at the same time.
Parameters:
upfreq_speed: New upfreq speed.
Returns:
A reply informing of the status of setting upfreq speed.
</details>
"""
if not 0 < upfreq_speed < 9:
raise APIError(
f"Upfreq speed is outside of the allowed "
f"range. Please set a number between 0 (Normal) and "
f"9 (Fastest)."
)
return await self.send_privileged_command("adjust_upfreq_speed", upfreq_speed=upfreq_speed)
async def set_poweroff_cool(self, poweroff_cool: bool):
"""Set whether to cool the machine when mining is stopped.
<details>
<summary>Expand</summary>
Set whether to cool the machine when mining is stopped, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
poweroff_cool: Whether to cool the miner during power off mode.
Returns:
A reply informing of the status of setting power off cooling mode.
</details>
"""
return await self.send_privileged_command("set_poweroff_cool", poweroff_cool=int(poweroff_cool))
async def set_fan_zero_speed(self, fan_zero_speed: bool):
"""Sets whether the fan speed supports the lowest 0 speed.
<details>
<summary>Expand</summary>
Sets whether the fan speed supports the lowest 0 speed, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
fan_zero_speed: Whether the fan is allowed to support 0 speed.
Returns:
A reply informing of the status of setting fan minimum speed.
</details>
"""
return await self.send_privileged_command("set_fan_zero_speed", fan_zero_speed=int(fan_zero_speed))
#### END privileged COMMANDS #### #### END privileged COMMANDS ####
@@ -881,18 +878,4 @@ class BTMinerAPI(BaseMinerAPI):
General miner info. General miner info.
</details> </details>
""" """
return await self.send_command("get_miner_info", allow_warning=False) return await self.send_command("get_miner_info")
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,9 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging from pyasic.API import BaseMinerAPI
from pyasic.API import APIError, BaseMinerAPI
class CGMinerAPI(BaseMinerAPI): class CGMinerAPI(BaseMinerAPI):
@@ -38,35 +36,6 @@ class CGMinerAPI(BaseMinerAPI):
def __init__(self, ip: str, port: int = 4028): def __init__(self, ip: str, port: int = 4028):
super().__init__(ip, port) super().__init__(ip, port)
async def multicommand(
self, *commands: str, allow_warning: bool = True
) -> dict:
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesn't work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
data = await self._x19_multicommand(*command.split("+"))
return data
async def _x19_multicommand(self, *commands):
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError as e:
raise APIError(e)
except Exception as e:
logging.warning(f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}")
return data
async def version(self) -> dict: async def version(self) -> dict:
"""Get miner version info. """Get miner version info.
<details> <details>

View File

@@ -18,7 +18,7 @@ from pyasic.API import BaseMinerAPI
class UnknownAPI(BaseMinerAPI): class UnknownAPI(BaseMinerAPI):
"""An abstraction of an API for a miner which is unknown. """An abstraction of an API for a miner which is unknown.
This class is designed to try to be an intersection of as many miner APIs This class is designed to try to be a intersection of as many miner APIs
and API commands as possible (API ⋂ API), to ensure that it can be used and API commands as possible (API ⋂ API), to ensure that it can be used
with as many APIs as possible. with as many APIs as possible.
""" """

View File

@@ -11,45 +11,3 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # 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,16 +12,15 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json from dataclasses import dataclass, asdict
import logging from typing import Literal, List
import random import random
import string import string
import time
from dataclasses import asdict, dataclass, fields
from typing import Dict, List, Literal
import toml import toml
import yaml import yaml
import json
import time
@dataclass @dataclass
@@ -38,10 +37,6 @@ class _Pool:
username: str = "" username: str = ""
password: str = "" password: str = ""
@classmethod
def fields(cls):
return fields(cls)
def from_dict(self, data: dict): def from_dict(self, data: dict):
"""Convert raw pool data as a dict to usable data and save it to this class. """Convert raw pool data as a dict to usable data and save it to this class.
@@ -83,23 +78,6 @@ class _Pool:
pool = {"url": self.url, "user": username, "pass": self.password} pool = {"url": self.url, "user": username, "pass": self.password}
return pool 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: def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a string usable by an Avalonminer device. """Convert the data in this class to a string usable by an Avalonminer device.
@@ -141,10 +119,6 @@ class _PoolGroup:
group_name: str = None group_name: str = None
pools: List[_Pool] = None pools: List[_Pool] = None
@classmethod
def fields(cls):
return fields(cls)
def __post_init__(self): def __post_init__(self):
if not self.group_name: if not self.group_name:
self.group_name = "".join( self.group_name = "".join(
@@ -180,21 +154,8 @@ class _PoolGroup:
pools.append(pool.as_x19(user_suffix=user_suffix)) pools.append(pool.as_x19(user_suffix=user_suffix))
return pools 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]: def as_wm(self, user_suffix: str = None) -> List[dict]:
"""Convert the data in this class to a list usable by a Whatsminer device. """Convert the data in this class to a list usable by an Whatsminer device.
Parameters: Parameters:
user_suffix: The suffix to append to username. user_suffix: The suffix to append to username.
@@ -256,7 +217,7 @@ class MinerConfig:
temp_mode: Literal["auto", "manual", "disabled"] = "auto" temp_mode: Literal["auto", "manual", "disabled"] = "auto"
temp_target: float = 70.0 temp_target: float = 70.0
temp_hot: float = 80.0 temp_hot: float = 80.0
temp_dangerous: float = 100.0 temp_dangerous: float = 10.0
minimum_fans: int = None minimum_fans: int = None
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
@@ -272,13 +233,8 @@ class MinerConfig:
dps_shutdown_enabled: bool = None dps_shutdown_enabled: bool = None
dps_shutdown_duration: float = None dps_shutdown_duration: float = None
@classmethod
def fields(cls):
return fields(cls)
def as_dict(self) -> dict: def as_dict(self) -> dict:
"""Convert the data in this class to a dict.""" """Convert the data in this class to a dict."""
logging.debug(f"MinerConfig - (To Dict) - Dumping Dict config")
data_dict = asdict(self) data_dict = asdict(self)
for key in asdict(self).keys(): for key in asdict(self).keys():
if data_dict[key] is None: if data_dict[key] is None:
@@ -287,12 +243,10 @@ class MinerConfig:
def as_toml(self) -> str: def as_toml(self) -> str:
"""Convert the data in this class to toml.""" """Convert the data in this class to toml."""
logging.debug(f"MinerConfig - (To TOML) - Dumping TOML config")
return toml.dumps(self.as_dict()) return toml.dumps(self.as_dict())
def as_yaml(self) -> str: def as_yaml(self) -> str:
"""Convert the data in this class to yaml.""" """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) return yaml.dump(self.as_dict(), sort_keys=False)
def from_raw(self, data: dict): def from_raw(self, data: dict):
@@ -302,7 +256,6 @@ class MinerConfig:
Parameters: Parameters:
data: The raw config data to convert. data: The raw config data to convert.
""" """
logging.debug(f"MinerConfig - (From Raw) - Loading raw config")
pool_groups = [] pool_groups = []
for key in data.keys(): for key in data.keys():
if key == "pools": if key == "pools":
@@ -316,10 +269,6 @@ class MinerConfig:
self.temp_mode = "manual" self.temp_mode = "manual"
if data.get("bitmain-fan-pwm"): if data.get("bitmain-fan-pwm"):
self.fan_speed = int(data["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": elif key == "fan_control":
for _key in data[key].keys(): for _key in data[key].keys():
if _key == "min_fans": if _key == "min_fans":
@@ -365,12 +314,6 @@ class MinerConfig:
return self return self
def from_api(self, pools: list): 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 = [] _pools = []
for pool in pools: for pool in pools:
url = pool.get("URL") url = pool.get("URL")
@@ -385,7 +328,6 @@ class MinerConfig:
Parameters: Parameters:
data: The dict config data to convert. data: The dict config data to convert.
""" """
logging.debug(f"MinerConfig - (From Dict) - Loading Dict config")
pool_groups = [] pool_groups = []
for group in data["pool_groups"]: for group in data["pool_groups"]:
pool_groups.append(_PoolGroup().from_dict(group)) pool_groups.append(_PoolGroup().from_dict(group))
@@ -401,7 +343,6 @@ class MinerConfig:
Parameters: Parameters:
data: The toml config data to convert. data: The toml config data to convert.
""" """
logging.debug(f"MinerConfig - (From TOML) - Loading TOML config")
return self.from_dict(toml.loads(data)) return self.from_dict(toml.loads(data))
def from_yaml(self, data: str): def from_yaml(self, data: str):
@@ -410,26 +351,15 @@ class MinerConfig:
Parameters: Parameters:
data: The yaml config data to convert. 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)) return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
def as_wm(self, user_suffix: str = None) -> Dict[str, int]: def as_wm(self, user_suffix: str = None) -> List[dict]:
"""Convert the data in this class to a config usable by a Whatsminer device. """Convert the data in this class to a config usable by an Whatsminer device.
Parameters: Parameters:
user_suffix: The suffix to append to username. user_suffix: The suffix to append to username.
""" """
logging.debug(f"MinerConfig - (As Whatsminer) - Generating Whatsminer config") return self.pool_groups[0].as_x19(user_suffix=user_suffix)
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: def as_x19(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an X19 device. """Convert the data in this class to a config usable by an X19 device.
@@ -437,15 +367,11 @@ class MinerConfig:
Parameters: Parameters:
user_suffix: The suffix to append to username. user_suffix: The suffix to append to username.
""" """
logging.debug(f"MinerConfig - (As X19) - Generating X19 config")
cfg = { cfg = {
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix), "pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
"bitmain-fan-ctrl": False, "bitmain-fan-ctrl": False,
"bitmain-fan-pwn": 100, "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": if not self.temp_mode == "auto":
cfg["bitmain-fan-ctrl"] = True cfg["bitmain-fan-ctrl"] = True
@@ -461,7 +387,6 @@ class MinerConfig:
Parameters: Parameters:
user_suffix: The suffix to append to username. 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) cfg = self.pool_groups[0].as_avalon(user_suffix=user_suffix)
return cfg return cfg
@@ -472,7 +397,6 @@ class MinerConfig:
model: The model of the miner to be used in the format portion of the config. model: The model of the miner to be used in the format portion of the config.
user_suffix: The suffix to append to username. user_suffix: The suffix to append to username.
""" """
logging.debug(f"MinerConfig - (As BOS) - Generating BOSMiner config")
cfg = { cfg = {
"format": { "format": {
"version": "1.2+", "version": "1.2+",

View File

@@ -12,26 +12,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import copy from typing import Union, List
import json from dataclasses import dataclass, field, asdict
import logging from datetime import datetime
import time
from dataclasses import asdict, dataclass, field, fields
from datetime import datetime, timezone
from typing import List, Union
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error from .error_codes import X19Error, WhatsminerError, BraiinsOSError
@dataclass
class HashBoard:
slot: int = 0
hashrate: float = 0.0
temp: int = -1
chip_temp: int = -1
chips: int = 0
expected_chips: int = 0
missing: bool = True
@dataclass @dataclass
@@ -61,14 +46,13 @@ class MinerData:
fan_2: The speed of the second fan as an int. fan_2: The speed of the second fan as an int.
fan_3: The speed of the third fan as an int. fan_3: The speed of the third fan as an int.
fan_4: The speed of the fourth 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. 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. 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. 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. 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. 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. percent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
nominal: Whether the number of chips in the miner is nominal. Calculated automatically. nominal: The nominal amount of chips in the miner. Calculated automatically.
pool_split: The pool split as a str. pool_split: The pool split as a str.
pool_1_url: The first pool url on the miner as a str. pool_1_url: The first pool url on the miner as a str.
pool_1_user: The first pool user on the miner as a str. pool_1_user: The first pool user on the miner as a str.
@@ -76,7 +60,6 @@ class MinerData:
pool_2_user: The second pool user on the miner as a str. pool_2_user: The second pool user on the miner as a str.
errors: A list of errors on the miner. errors: A list of errors on the miner.
fault_light: Whether or not the fault light is on as a boolean. 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 ip: str
@@ -85,29 +68,26 @@ class MinerData:
model: str = "Unknown" model: str = "Unknown"
hostname: str = "Unknown" hostname: str = "Unknown"
hashrate: float = 0 hashrate: float = 0
hashboards: List[HashBoard] = field(default_factory=list) left_board_hashrate: float = 0
ideal_hashboards: int = 1 center_board_hashrate: float = 0
left_board_hashrate: float = field(init=False) right_board_hashrate: float = 0
center_board_hashrate: float = field(init=False)
right_board_hashrate: float = field(init=False)
temperature_avg: int = field(init=False) temperature_avg: int = field(init=False)
env_temp: float = -1.0 env_temp: float = 0
left_board_temp: int = field(init=False) left_board_temp: int = 0
left_board_chip_temp: int = field(init=False) left_board_chip_temp: int = 0
center_board_temp: int = field(init=False) center_board_temp: int = 0
center_board_chip_temp: int = field(init=False) center_board_chip_temp: int = 0
right_board_temp: int = field(init=False) right_board_temp: int = 0
right_board_chip_temp: int = field(init=False) right_board_chip_temp: int = 0
wattage: int = -1 wattage: int = 0
wattage_limit: int = -1 wattage_limit: int = 0
fan_1: int = -1 fan_1: int = -1
fan_2: int = -1 fan_2: int = -1
fan_3: int = -1 fan_3: int = -1
fan_4: int = -1 fan_4: int = -1
fan_psu: int = -1 left_chips: int = 0
left_chips: int = field(init=False) center_chips: int = 0
center_chips: int = field(init=False) right_chips: int = 0
right_chips: int = field(init=False)
total_chips: int = field(init=False) total_chips: int = field(init=False)
ideal_chips: int = 1 ideal_chips: int = 1
percent_ideal: float = field(init=False) percent_ideal: float = field(init=False)
@@ -117,213 +97,22 @@ class MinerData:
pool_1_user: str = "Unknown" pool_1_user: str = "Unknown"
pool_2_url: str = "" pool_2_url: str = ""
pool_2_user: str = "" pool_2_user: str = ""
errors: List[ errors: List[Union[WhatsminerError, BraiinsOSError, X19Error]] = field(
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError] default_factory=list
] = field(default_factory=list) )
fault_light: Union[bool, None] = None 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): def __post_init__(self):
self.datetime = datetime.now(timezone.utc).astimezone() self.datetime = datetime.now()
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 @property
def total_chips(self): # noqa - Skip PyCharm inspection def total_chips(self): # noqa - Skip PyCharm inspection
return sum([hb.chips for hb in self.hashboards]) return self.right_chips + self.center_chips + self.left_chips
@total_chips.setter @total_chips.setter
def total_chips(self, val): def total_chips(self, val):
pass 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 @property
def nominal(self): # noqa - Skip PyCharm inspection def nominal(self): # noqa - Skip PyCharm inspection
return self.ideal_chips == self.total_chips return self.ideal_chips == self.total_chips
@@ -344,9 +133,13 @@ class MinerData:
def temperature_avg(self): # noqa - Skip PyCharm inspection def temperature_avg(self): # noqa - Skip PyCharm inspection
total_temp = 0 total_temp = 0
temp_count = 0 temp_count = 0
for hb in self.hashboards: for temp in [
if hb.temp and not hb.temp == -1: self.left_board_chip_temp,
total_temp += hb.temp self.center_board_chip_temp,
self.right_board_chip_temp,
]:
if temp and not temp == 0:
total_temp += temp
temp_count += 1 temp_count += 1
if not temp_count > 0: if not temp_count > 0:
return 0 return 0
@@ -356,92 +149,5 @@ class MinerData:
def temperature_avg(self, val): def temperature_avg(self, val):
pass pass
@property def asdict(self):
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) 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from dataclasses import asdict, dataclass, fields from dataclasses import dataclass, asdict
@dataclass @dataclass
@@ -21,15 +21,9 @@ class X19Error:
Attributes: Attributes:
error_message: The error message as a string. 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_message: str
error_code: int = 0
@classmethod
def fields(cls):
return fields(cls)
def asdict(self): def asdict(self):
return asdict(self) return asdict(self)

View File

@@ -12,13 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import TypeVar
from .bos import BraiinsOSError
from .innosilicon import InnosiliconError
from .whatsminer import WhatsminerError from .whatsminer import WhatsminerError
from .bos import BraiinsOSError
from .X19 import X19Error 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from dataclasses import asdict, dataclass, fields from dataclasses import dataclass, asdict
@dataclass @dataclass
@@ -21,16 +21,9 @@ class BraiinsOSError:
Attributes: Attributes:
error_message: The error message as a string. 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_message: str
error_code: int = 0
@classmethod
def fields(cls):
return fields(cls)
def asdict(self): def asdict(self):
return asdict(self) return asdict(self)

View File

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

View File

@@ -1,41 +0,0 @@
# 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,7 +13,6 @@
# limitations under the License. # limitations under the License.
import logging import logging
from pyasic.settings import PyasicSettings from pyasic.settings import PyasicSettings

View File

@@ -12,13 +12,126 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import asyncssh
import logging
import ipaddress import ipaddress
from typing import Union from abc import ABC, abstractmethod
from pyasic.miners.base import AnyMiner, BaseMiner from pyasic.data import MinerData
from pyasic.miners.miner_factory import MinerFactory from pyasic.config import MinerConfig
# abstracted version of get miner that is easier to access class BaseMiner(ABC):
async def get_miner(ip: Union[ipaddress.ip_address, str]) -> AnyMiner: def __init__(self, *args) -> None:
return await MinerFactory().get_miner(ip) 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))

View File

@@ -14,13 +14,14 @@
import ipaddress import ipaddress
import logging import logging
from typing import List, Union from typing import Union
from pyasic.API.bmminer import BMMinerAPI from pyasic.API.bmminer import BMMinerAPI
from pyasic.config import MinerConfig from pyasic.miners import BaseMiner
from pyasic.data import HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData from pyasic.data import MinerData
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings from pyasic.settings import PyasicSettings
@@ -153,9 +154,6 @@ class BMMiner(BaseMiner):
return True return True
return False return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def check_light(self) -> bool: async def check_light(self) -> bool:
if not self.light: if not self.light:
self.light = False self.light = False
@@ -167,7 +165,7 @@ class BMMiner(BaseMiner):
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
return False return False
async def get_errors(self) -> List[MinerErrorData]: async def get_errors(self) -> list:
return [] return []
async def get_mac(self) -> str: async def get_mac(self) -> str:
@@ -176,23 +174,13 @@ class BMMiner(BaseMiner):
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
return False return False
async def stop_mining(self) -> bool: async def get_data(self) -> MinerData:
return False
async def resume_mining(self) -> bool:
return False
async def get_data(self, allow_warning: bool = False) -> MinerData:
"""Get data from the miner. """Get data from the miner.
Returns: Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data. A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
""" """
data = MinerData( data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
ip=str(self.ip),
ideal_chips=self.nominal_chips * self.ideal_hashboards,
ideal_hashboards=self.ideal_hashboards,
)
board_offset = -1 board_offset = -1
fan_offset = -1 fan_offset = -1
@@ -220,7 +208,7 @@ class BMMiner(BaseMiner):
miner_data = None miner_data = None
for i in range(PyasicSettings().miner_get_data_retries): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"summary", "pools", "stats", allow_warning=allow_warning "summary", "pools", "stats", ignore_x19_error=True
) )
if miner_data: if miner_data:
break break
@@ -243,7 +231,7 @@ class BMMiner(BaseMiner):
if stats: if stats:
boards = stats.get("STATS") boards = stats.get("STATS")
if boards: if boards:
if len(boards) > 1: if len(boards) > 0:
for board_num in range(1, 16, 5): for board_num in range(1, 16, 5):
for _b_num in range(5): for _b_num in range(5):
b = boards[1].get(f"chain_acn{board_num + _b_num}") b = boards[1].get(f"chain_acn{board_num + _b_num}")
@@ -253,38 +241,19 @@ class BMMiner(BaseMiner):
if board_offset == -1: if board_offset == -1:
board_offset = 1 board_offset = 1
env_temp_list = [] data.left_chips = boards[1].get(f"chain_acn{board_offset}")
for i in range(board_offset, board_offset + self.ideal_hashboards): data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
hashboard = HashBoard( data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
slot=i - board_offset, expected_chips=self.nominal_chips
)
chip_temp = boards[1].get(f"temp{i}") data.left_board_hashrate = round(
if chip_temp: float(boards[1].get(f"chain_rate{board_offset}")) / 1000, 2
hashboard.chip_temp = round(chip_temp) )
data.center_board_hashrate = round(
temp = boards[1].get(f"temp2_{i}") float(boards[1].get(f"chain_rate{board_offset+1}")) / 1000, 2
if temp: )
hashboard.temp = round(temp) data.right_board_hashrate = round(
float(boards[1].get(f"chain_rate{board_offset+2}")) / 1000, 2
hashrate = boards[1].get(f"chain_rate{i}") )
if hashrate:
hashboard.hashrate = round(float(hashrate) / 1000, 2)
chips = boards[1].get(f"chain_acn{i}")
if chips:
hashboard.chips = chips
hashboard.missing = False
if (not chips) or (not chips > 0):
hashboard.missing = True
data.hashboards.append(hashboard)
if f"temp_pcb{i}" in boards[1].keys():
env_temp = boards[1][f"temp_pcb{i}"].split("-")[0]
if not env_temp == 0:
env_temp_list.append(int(env_temp))
if not env_temp_list == []:
data.env_temp = round(sum(env_temp_list) / len(env_temp_list))
if stats: if stats:
temp = stats.get("STATS") temp = stats.get("STATS")
@@ -302,6 +271,20 @@ class BMMiner(BaseMiner):
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}") data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
) )
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
env_temp_list = []
for item in range(3):
board_temp = temp[1].get(f"temp{item + board_offset}")
chip_temp = temp[1].get(f"temp2_{item + board_offset}")
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
setattr(data, f"{board_map[item]}_temp", board_temp)
if f"temp_pcb{item}" in temp[1].keys():
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
if not env_temp == 0:
env_temp_list.append(int(env_temp))
if not env_temp_list == []:
data.env_temp = sum(env_temp_list) / len(env_temp_list)
if pools: if pools:
pool_1 = None pool_1 = None
pool_2 = None pool_2 = None
@@ -349,7 +332,3 @@ class BMMiner(BaseMiner):
data.pool_split = str(quota) data.pool_split = str(quota)
return data return data
async def set_power_limit(self, wattage: int) -> bool:
return False

View File

@@ -13,19 +13,22 @@
# limitations under the License. # limitations under the License.
import ipaddress import ipaddress
import json
import logging import logging
from typing import List, Union import json
from typing import Union
import httpx
import toml import toml
from pyasic.miners import BaseMiner
from pyasic.API.bosminer import BOSMinerAPI from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API import APIError
from pyasic.data.error_codes import BraiinsOSError
from pyasic.data import MinerData
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import HashBoard, MinerData
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings from pyasic.settings import PyasicSettings
@@ -66,27 +69,6 @@ class BOSMiner(BaseMiner):
# return the result, either command output or None # return the result, either command output or None
return str(result) return str(result)
async def send_graphql_query(self, query) -> Union[dict, None]:
url = f"http://{self.ip}/graphql"
try:
async with httpx.AsyncClient() as client:
_auth = await client.post(
url,
json={
"query": 'mutation{auth{login(username:"'
+ self.uname
+ '", password:"'
+ self.pwd
+ '"){__typename}}}'
},
)
d = await client.post(url, json={"query": query})
if d.status_code == 200:
return d.json()
except (httpx.ReadError, httpx.ReadTimeout):
return None
return None
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
"""Sends command to turn on fault light on the miner.""" """Sends command to turn on fault light on the miner."""
logging.debug(f"{self}: Sending fault_light on command.") logging.debug(f"{self}: Sending fault_light on command.")
@@ -121,20 +103,6 @@ class BOSMiner(BaseMiner):
return True return True
return False return False
async def stop_mining(self) -> bool:
data = await self.api.pause()
if data.get("PAUSE"):
if data["PAUSE"][0]:
return True
return False
async def resume_mining(self) -> bool:
data = await self.api.resume()
if data.get("RESUME"):
if data["RESUME"][0]:
return True
return False
async def reboot(self) -> bool: async def reboot(self) -> bool:
"""Reboots power to the physical miner.""" """Reboots power to the physical miner."""
logging.debug(f"{self}: Sending reboot command.") logging.debug(f"{self}: Sending reboot command.")
@@ -170,11 +138,6 @@ class BOSMiner(BaseMiner):
""" """
if self.hostname: if self.hostname:
return self.hostname return self.hostname
# get hostname through GraphQL
if data := await self.send_graphql_query("{bos {hostname}}"):
self.hostname = data["data"]["bos"]["hostname"]
return self.hostname
try: try:
async with (await self._get_ssh_connection()) as conn: async with (await self._get_ssh_connection()) as conn:
if conn is not None: if conn is not None:
@@ -238,15 +201,9 @@ class BOSMiner(BaseMiner):
if self.version: if self.version:
logging.debug(f"Found version for {self.ip}: {self.version}") logging.debug(f"Found version for {self.ip}: {self.version}")
return self.version return self.version
version_data = None
# try to get data from graphql
data = await self.send_graphql_query("{bos{info{version{full}}}}")
if data:
version_data = data["bos"]["info"]["version"]["full"]
if not version_data: # get output of bos version file
# try version data file version_data = await self.send_ssh_command("cat /etc/bos_version")
version_data = await self.send_ssh_command("cat /etc/bos_version")
# if we get the version data, parse it # if we get the version data, parse it
if version_data: if version_data:
@@ -258,12 +215,22 @@ class BOSMiner(BaseMiner):
logging.warning(f"Failed to get model for miner: {self}") logging.warning(f"Failed to get model for miner: {self}")
return None return None
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: async def send_config(self, yaml_config, ip_user: bool = False) -> None:
"""Configures miner with yaml config.""" """Configures miner with yaml config."""
logging.debug(f"{self}: Sending config.") logging.debug(f"{self}: Sending config.")
toml_conf = config.as_bos( if ip_user:
model=self.model.replace(" (BOS)", ""), user_suffix=user_suffix suffix = str(self.ip).split(".")[-1]
) toml_conf = (
MinerConfig()
.from_yaml(yaml_config)
.as_bos(model=self.model.replace(" (BOS)", ""), user_suffix=suffix)
)
else:
toml_conf = (
MinerConfig()
.from_yaml(yaml_config)
.as_bos(model=self.model.replace(" (BOS)", ""))
)
async with (await self._get_ssh_connection()) as conn: async with (await self._get_ssh_connection()) as conn:
await conn.run("/etc/init.d/bosminer stop") await conn.run("/etc/init.d/bosminer stop")
logging.debug(f"{self}: Opening SFTP connection.") logging.debug(f"{self}: Opening SFTP connection.")
@@ -275,26 +242,11 @@ class BOSMiner(BaseMiner):
await conn.run("/etc/init.d/bosminer start") await conn.run("/etc/init.d/bosminer start")
async def check_light(self) -> bool: async def check_light(self) -> bool:
if self.light: if not self.light:
return self.light self.light = False
# get light through GraphQL
if data := await self.send_graphql_query("{bos {faultLight}}"):
try:
self.light = data["data"]["bos"]["faultLight"]
return self.light
except (TypeError, KeyError, ValueError, IndexError):
pass
# get light via ssh if that fails (10x slower)
data = (
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
).strip()
self.light = False
if data == "50":
self.light = True
return self.light return self.light
async def get_errors(self) -> List[MinerErrorData]: async def get_errors(self) -> list:
tunerstatus = None tunerstatus = None
errors = [] errors = []
@@ -304,10 +256,7 @@ class BOSMiner(BaseMiner):
logging.warning(e) logging.warning(e)
if tunerstatus: if tunerstatus:
try: tuner = tunerstatus[0].get("TUNERSTATUS")
tuner = tunerstatus[0].get("TUNERSTATUS")
except KeyError:
tuner = tunerstatus.get("TUNERSTATUS")
if tuner: if tuner:
if len(tuner) > 0: if len(tuner) > 0:
chain_status = tuner[0].get("TunerChainStatus") chain_status = tuner[0].get("TunerChainStatus")
@@ -327,34 +276,24 @@ class BOSMiner(BaseMiner):
if board["Status"] not in [ if board["Status"] not in [
"Stable", "Stable",
"Testing performance profile", "Testing performance profile",
"Tuning individual chips"
]: ]:
_error = board["Status"].split(" {")[0] _error = board["Status"]
_error = _error[0].lower() + _error[1:] _error = _error[0].lower() + _error[1:]
errors.append( errors.append(
BraiinsOSError(f"{board_map[_id]} {_error}") BraiinsOSError(f"{board_map[_id]} {_error}")
) )
return errors return errors
async def get_data(self, allow_warning: bool = True) -> MinerData: async def get_data(self) -> MinerData:
"""Get data from the miner. """Get data from the miner.
Returns: Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data. A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
""" """
d = await self._graphql_get_data() data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
if d:
return d
data = MinerData( board_offset = -1
ip=str(self.ip), fan_offset = -1
ideal_chips=self.nominal_chips * self.ideal_hashboards,
ideal_hashboards=self.ideal_hashboards,
hashboards=[
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
],
)
model = await self.get_model() model = await self.get_model()
hostname = await self.get_hostname() hostname = await self.get_hostname()
@@ -382,7 +321,6 @@ class BOSMiner(BaseMiner):
"devdetails", "devdetails",
"fans", "fans",
"devs", "devs",
allow_warning=allow_warning,
) )
except APIError as e: except APIError as e:
if str(e.message) == "Not ready": if str(e.message) == "Not ready":
@@ -413,13 +351,14 @@ class BOSMiner(BaseMiner):
temp = temps[0].get("TEMPS") temp = temps[0].get("TEMPS")
if temp: if temp:
if len(temp) > 0: if len(temp) > 0:
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
offset = 6 if temp[0]["ID"] in [6, 7, 8] else temp[0]["ID"] offset = 6 if temp[0]["ID"] in [6, 7, 8] else temp[0]["ID"]
for board in temp: for board in temp:
_id = board["ID"] - offset _id = board["ID"] - offset
chip_temp = round(board["Chip"]) chip_temp = round(board["Chip"])
board_temp = round(board["Board"]) board_temp = round(board["Board"])
data.hashboards[_id].chip_temp = chip_temp setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
data.hashboards[_id].temp = board_temp setattr(data, f"{board_map[_id]}_temp", board_temp)
if fans: if fans:
fan_data = fans[0].get("FANS") fan_data = fans[0].get("FANS")
@@ -481,7 +420,7 @@ class BOSMiner(BaseMiner):
wattage_limit = tuner[0].get("PowerLimit") wattage_limit = tuner[0].get("PowerLimit")
if wattage_limit: if wattage_limit:
data.wattage_limit = wattage_limit data.wattage_limit = wattage_limit
if wattage is not None: if wattage:
data.wattage = wattage data.wattage = wattage
chain_status = tuner[0].get("TunerChainStatus") chain_status = tuner[0].get("TunerChainStatus")
@@ -502,7 +441,7 @@ class BOSMiner(BaseMiner):
"Stable", "Stable",
"Testing performance profile", "Testing performance profile",
]: ]:
_error = board["Status"].split(" {")[0] _error = board["Status"]
_error = _error[0].lower() + _error[1:] _error = _error[0].lower() + _error[1:]
data.errors.append( data.errors.append(
BraiinsOSError(f"{board_map[_id]} {_error}") BraiinsOSError(f"{board_map[_id]} {_error}")
@@ -512,173 +451,29 @@ class BOSMiner(BaseMiner):
boards = devdetails[0].get("DEVDETAILS") boards = devdetails[0].get("DEVDETAILS")
if boards: if boards:
if len(boards) > 0: if len(boards) > 0:
board_map = {0: "left_chips", 1: "center_chips", 2: "right_chips"}
offset = 6 if boards[0]["ID"] in [6, 7, 8] else boards[0]["ID"] offset = 6 if boards[0]["ID"] in [6, 7, 8] else boards[0]["ID"]
for board in boards: for board in boards:
_id = board["ID"] - offset _id = board["ID"] - offset
chips = board["Chips"] chips = board["Chips"]
data.hashboards[_id].chips = chips setattr(data, board_map[_id], chips)
if chips > 0:
data.hashboards[_id].missing = False
else:
data.hashboards[_id].missing = True
if devs: if devs:
boards = devs[0].get("DEVS") boards = devs[0].get("DEVS")
if boards: if boards:
if len(boards) > 0: if len(boards) > 0:
board_map = {
0: "left_board_hashrate",
1: "center_board_hashrate",
2: "right_board_hashrate",
}
offset = 6 if boards[0]["ID"] in [6, 7, 8] else boards[0]["ID"] offset = 6 if boards[0]["ID"] in [6, 7, 8] else boards[0]["ID"]
for board in boards: for board in boards:
_id = board["ID"] - offset _id = board["ID"] - offset
hashrate = round(board["MHS 1m"] / 1000000, 2) hashrate = round(board["MHS 1m"] / 1000000, 2)
data.hashboards[_id].hashrate = hashrate setattr(data, board_map[_id], hashrate)
return data
async def _graphql_get_data(self) -> Union[MinerData, None]:
data = MinerData(
ip=str(self.ip),
ideal_chips=self.nominal_chips * self.ideal_hashboards,
ideal_hashboards=self.ideal_hashboards,
hashboards=[
HashBoard(slot=i, expected_chips=self.nominal_chips, missing=True)
for i in range(self.ideal_hashboards)
],
)
query = "{bos {hostname}, bosminer{config{... on BosminerConfig{groups{pools{url, user}, strategy{... on QuotaStrategy {quota}}}}}, info{fans{name, rpm}, workSolver{realHashrate{mhs1M}, temperatures{degreesC}, power{limitW, approxConsumptionW}, childSolvers{name, realHashrate{mhs1M}, hwDetails{chips}, tuner{statusMessages}, temperatures{degreesC}}}}}}"
query_data = await self.send_graphql_query(query)
if not query_data:
return None
query_data = query_data["data"]
if not query_data:
return None
data.mac = await self.get_mac()
data.model = await self.get_model()
if query_data.get("bos"):
if query_data["bos"].get("hostname"):
data.hostname = query_data["bos"]["hostname"]
try:
if query_data["bosminer"]["info"]["workSolver"]["realHashrate"].get("mhs1M"):
data.hashrate = round(
query_data["bosminer"]["info"]["workSolver"]["realHashrate"]["mhs1M"]
/ 1000000,
2,
)
except (TypeError, KeyError, ValueError, IndexError):
pass
boards = None
if query_data.get("bosminer"):
if query_data["bosminer"].get("info"):
if query_data["bosminer"]["info"].get("workSolver"):
boards = query_data["bosminer"]["info"]["workSolver"].get("childSolvers")
if boards:
offset = 6 if int(boards[0]["name"]) in [6, 7, 8] else int(boards[0]["name"])
for hb in boards:
_id = int(hb["name"]) - offset
board = data.hashboards[_id]
board.hashrate = round(hb["realHashrate"]["mhs1M"] / 1000000, 2)
temps = hb["temperatures"]
try:
if len(temps) > 0:
board.temp = round(hb["temperatures"][0]["degreesC"])
if len(temps) > 1:
board.chip_temp = round(hb["temperatures"][1]["degreesC"])
except (TypeError, KeyError, ValueError, IndexError):
pass
details = hb.get("hwDetails")
if details:
if chips := details["chips"]:
board.chips = chips
board.missing = False
tuner = hb.get("tuner")
if tuner:
if msg := tuner.get("statusMessages"):
if len(msg) > 0:
if hb["tuner"]["statusMessages"][0] not in [
"Stable",
"Testing performance profile",
"Tuning individual chips"
]:
data.errors.append(
BraiinsOSError(f"Slot {_id} {hb['tuner']['statusMessages'][0]}")
)
try:
data.wattage = query_data["bosminer"]["info"]["workSolver"]["power"]["approxConsumptionW"]
except (TypeError, KeyError, ValueError, IndexError):
data.wattage = 0
try:
data.wattage_limit = query_data["bosminer"]["info"]["workSolver"]["power"]["limitW"]
except (TypeError, KeyError, ValueError, IndexError):
pass
for n in range(self.fan_count):
try:
setattr(data, f"fan_{n + 1}", query_data["bosminer"]["info"]["fans"][n]["rpm"])
except (TypeError, KeyError, ValueError, IndexError):
pass
groups = None
if query_data.get("bosminer"):
if query_data["bosminer"].get("config"):
groups = query_data["bosminer"]["config"].get("groups")
if groups:
if len(groups) == 1:
try:
data.pool_1_user = groups[0]["pools"][0]["user"]
except (TypeError, KeyError, ValueError, IndexError):
pass
try:
data.pool_1_url = groups[0]["pools"][0]["url"]
except (TypeError, KeyError, ValueError, IndexError):
pass
try:
data.pool_2_user = groups[0]["pools"][1]["user"]
except (TypeError, KeyError, ValueError, IndexError):
pass
try:
data.pool_2_url = groups[0]["pools"][1]["url"]
except (TypeError, KeyError, ValueError, IndexError):
pass
data.quota = 0
else:
try:
data.pool_1_user = groups[0]["pools"][0]["user"]
except (TypeError, KeyError, ValueError, IndexError):
pass
try:
data.pool_1_url = groups[0]["pools"][0]["url"]
except (TypeError, KeyError, ValueError, IndexError):
pass
try:
data.pool_2_user = groups[1]["pools"][0]["user"]
except (TypeError, KeyError, ValueError, IndexError):
pass
try:
data.pool_2_url = groups[1]["pools"][0]["url"]
except (TypeError, KeyError, ValueError, IndexError):
pass
if groups[0]["strategy"].get("quota"):
data.quota = str(groups[0]["strategy"]["quota"]) + "/" + str(groups[1]["strategy"]["quota"])
data.fault_light = await self.check_light()
return data return data
async def get_mac(self): async def get_mac(self):
result = await self.send_ssh_command("cat /sys/class/net/eth0/address") result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
return result.upper().strip() return result.upper().strip()
async def set_power_limit(self, wattage: int) -> bool:
try:
cfg = await self.get_config()
cfg.autotuning_wattage = wattage
await self.send_config(cfg)
except Exception as e:
logging.warning(f"{self} set_power_limit: {e}")
return False
else:
return True

View File

@@ -12,15 +12,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import ipaddress
import logging import logging
from typing import List, Union
import ipaddress
from typing import Union
from pyasic.API.bosminer import BOSMinerAPI from pyasic.API.bosminer import BOSMinerAPI
from pyasic.config import MinerConfig from pyasic.miners import BaseMiner
from pyasic.data import MinerData
from pyasic.data.error_codes import MinerErrorData
from pyasic.miners.base import BaseMiner
class BOSMinerOld(BaseMiner): class BOSMinerOld(BaseMiner):
@@ -77,7 +75,7 @@ class BOSMinerOld(BaseMiner):
async def get_config(self) -> None: async def get_config(self) -> None:
return None return None
async def get_errors(self) -> List[MinerErrorData]: async def get_errors(self) -> list:
return [] return []
async def get_hostname(self) -> str: async def get_hostname(self) -> str:
@@ -94,18 +92,3 @@ class BOSMinerOld(BaseMiner):
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
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:
return None
async def get_data(self, **kwargs) -> MinerData:
return MinerData(ip=str(self.ip))
async def set_power_limit(self, wattage: int) -> bool:
return False

View File

@@ -14,14 +14,17 @@
import ipaddress import ipaddress
import logging import logging
from typing import List, Union from typing import Union
from pyasic.API.btminer import BTMinerAPI 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.config import MinerConfig
from pyasic.data import HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData, WhatsminerError
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings from pyasic.settings import PyasicSettings
@@ -106,6 +109,7 @@ class BTMiner(BaseMiner):
if "Code" in data.keys(): if "Code" in data.keys():
if data["Code"] == 131: if data["Code"] == 131:
return True return True
print(data)
return False return False
async def check_light(self) -> bool: async def check_light(self) -> bool:
@@ -152,89 +156,33 @@ class BTMiner(BaseMiner):
return True return True
return False return False
async def get_errors(self) -> List[MinerErrorData]: async def get_errors(self) -> list:
data = [] return []
try:
err_data = await self.api.get_error_code()
if err_data:
if err_data.get("Msg"):
if err_data["Msg"].get("error_code"):
for err in err_data["Msg"]["error_code"]:
if isinstance(err, dict):
for code in err:
data.append(WhatsminerError(error_code=int(code)))
else:
data.append(WhatsminerError(error_code=int(err)))
except APIError:
summary_data = await self.api.summary()
if summary_data.get("SUMMARY"):
summary_data = summary_data["SUMMARY"]
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}"):
if not summary_data[0][f"Error Code {i}"] == "":
data.append(
WhatsminerError(
error_code=summary_data[0][f"Error Code {i}"]
)
)
return data
async def reboot(self) -> bool: async def reboot(self) -> bool:
data = await self.api.reboot()
if data.get("Msg"):
if data["Msg"] == "API command OK":
return True
return False return False
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
data = await self.api.restart()
if data.get("Msg"):
if data["Msg"] == "API command OK":
return True
return False return False
async def stop_mining(self) -> bool: async def send_config(self, yaml_config, ip_user: bool = False):
try: if ip_user:
data = await self.api.power_off(respbefore=True) suffix = str(self.ip).split(".")[-1]
except APIError: conf = MinerConfig().from_yaml(yaml_config).as_wm(user_suffix=suffix)
return False else:
if data.get("Msg"): conf = MinerConfig().from_yaml(yaml_config).as_wm()
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"]
await self.api.update_pools( await self.api.update_pools(
pools_conf[0]["url"], conf[0]["url"],
pools_conf[0]["user"], conf[0]["user"],
pools_conf[0]["pass"], conf[0]["pass"],
pools_conf[1]["url"], conf[1]["url"],
pools_conf[1]["user"], conf[1]["user"],
pools_conf[1]["pass"], conf[1]["pass"],
pools_conf[2]["url"], conf[2]["url"],
pools_conf[2]["user"], conf[2]["user"],
pools_conf[2]["pass"], conf[2]["pass"],
) )
try:
await self.api.adjust_power_limit(conf["wattage"])
except APIError:
# cannot set wattage
pass
async def get_config(self) -> MinerConfig: async def get_config(self) -> MinerConfig:
pools = None pools = None
@@ -250,17 +198,13 @@ class BTMiner(BaseMiner):
cfg = cfg.from_api(pools["POOLS"]) cfg = cfg.from_api(pools["POOLS"])
return cfg return cfg
async def get_data(self, allow_warning: bool = True) -> MinerData: async def get_data(self) -> MinerData:
"""Get data from the miner. """Get data from the miner.
Returns: Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data. A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
""" """
data = MinerData( data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
ip=str(self.ip),
ideal_chips=self.nominal_chips * self.ideal_hashboards,
ideal_hashboards=self.ideal_hashboards,
)
mac = None mac = None
@@ -289,25 +233,18 @@ class BTMiner(BaseMiner):
miner_data = None miner_data = None
for i in range(PyasicSettings().miner_get_data_retries): for i in range(PyasicSettings().miner_get_data_retries):
try: try:
miner_data = await self.api.multicommand("summary", "devs", "pools", allow_warning=allow_warning) miner_data = await self.api.multicommand("summary", "devs", "pools")
if miner_data: if miner_data:
break break
except APIError: except APIError:
pass pass
if not miner_data: if not miner_data:
return data return data
summary = miner_data.get("summary")[0] summary = miner_data.get("summary")[0]
devs = miner_data.get("devs")[0] devs = miner_data.get("devs")[0]
pools = miner_data.get("pools")[0] pools = miner_data.get("pools")[0]
try:
psu_data = await self.api.get_psu()
except APIError:
psu_data = None
try:
err_data = await self.api.get_error_code()
except APIError:
err_data = None
if summary: if summary:
summary_data = summary.get("SUMMARY") summary_data = summary.get("SUMMARY")
@@ -323,9 +260,6 @@ class BTMiner(BaseMiner):
if summary_data[0].get("Power Limit"): if summary_data[0].get("Power Limit"):
wattage_limit = summary_data[0]["Power Limit"] wattage_limit = summary_data[0]["Power Limit"]
if summary_data[0].get("Power Fanspeed"):
data.fan_psu = summary_data[0]["Power Fanspeed"]
data.fan_1 = summary_data[0]["Fan Speed In"] data.fan_1 = summary_data[0]["Fan Speed In"]
data.fan_2 = summary_data[0]["Fan Speed Out"] data.fan_2 = summary_data[0]["Fan Speed Out"]
@@ -345,47 +279,39 @@ class BTMiner(BaseMiner):
if summary_data[0].get("Error Code Count"): if summary_data[0].get("Error Code Count"):
for i in range(summary_data[0]["Error Code Count"]): for i in range(summary_data[0]["Error Code Count"]):
if summary_data[0].get(f"Error Code {i}"): if summary_data[0].get(f"Error Code {i}"):
if not summary_data[0][f"Error Code {i}"] == "":
data.errors.append(
WhatsminerError(
error_code=summary_data[0][
f"Error Code {i}"
]
)
)
if psu_data:
psu = psu_data.get("Msg")
if psu:
if psu.get("fan_speed"):
data.fan_psu = psu["fan_speed"]
if err_data:
if err_data.get("Msg"):
if err_data["Msg"].get("error_code"):
for err in err_data["Msg"]["error_code"]:
if isinstance(err, dict):
for code in err:
data.errors.append( data.errors.append(
WhatsminerError(error_code=int(code)) WhatsminerError(
error_code=summary_data[0][f"Error Code {i}"]
)
) )
else:
data.errors.append(WhatsminerError(error_code=int(err)))
if devs: if devs:
dev_data = devs.get("DEVS") temp_data = devs.get("DEVS")
if dev_data: if temp_data:
for board in dev_data: board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
temp_board = HashBoard( for board in temp_data:
slot=board["ASC"], _id = board["ASC"]
chip_temp=round(board["Chip Temp Avg"]), chip_temp = round(board["Chip Temp Avg"])
temp=round(board["Temperature"]), board_temp = round(board["Temperature"])
hashrate=round(board["MHS 1m"] / 1000000, 2), hashrate = round(board["MHS 1m"] / 1000000, 2)
chips=board["Effective Chips"], setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
missing=False if board["Effective Chips"] > 0 else True, setattr(data, f"{board_map[_id]}_temp", board_temp)
expected_chips=self.nominal_chips, setattr(data, f"{board_map[_id]}_hashrate", hashrate)
)
data.hashboards.append(temp_board) 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: if pools:
pool_1 = None pool_1 = None
@@ -444,13 +370,3 @@ class BTMiner(BaseMiner):
data.mac = mac data.mac = mac
return data return data
async def set_power_limit(self, wattage: int) -> bool:
try:
await self.api.adjust_power_limit(wattage)
except Exception as e:
logging.warning(f"{self} set_power_limit: {e}")
return False
else:
return True

View File

@@ -14,14 +14,15 @@
import ipaddress import ipaddress
import logging import logging
from typing import List, Union from typing import Union
from pyasic.API.cgminer import CGMinerAPI from pyasic.API.cgminer import CGMinerAPI
from pyasic.config import MinerConfig from pyasic.miners import BaseMiner
from pyasic.data import HashBoard, MinerData from pyasic.API import APIError
from pyasic.data.error_codes import MinerErrorData
from pyasic.errors import APIError from pyasic.data import MinerData
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings from pyasic.settings import PyasicSettings
@@ -116,7 +117,8 @@ class CGMiner(BaseMiner):
return True return True
return False return False
async def resume_mining(self) -> bool: async def start_cgminer(self) -> None:
"""Start cgminer hashing process."""
commands = [ commands = [
"mkdir -p /etc/tmp/", "mkdir -p /etc/tmp/",
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root', 'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
@@ -125,9 +127,9 @@ class CGMiner(BaseMiner):
] ]
commands = ";".join(commands) commands = ";".join(commands)
await self.send_ssh_command(commands) await self.send_ssh_command(commands)
return True
async def stop_mining(self) -> bool: async def stop_cgminer(self) -> None:
"""Restart cgminer hashing process."""
commands = [ commands = [
"mkdir -p /etc/tmp/", "mkdir -p /etc/tmp/",
'echo "" > /etc/tmp/root', 'echo "" > /etc/tmp/root',
@@ -136,7 +138,6 @@ class CGMiner(BaseMiner):
] ]
commands = ";".join(commands) commands = ";".join(commands)
await self.send_ssh_command(commands) await self.send_ssh_command(commands)
return True
async def get_config(self) -> str: async def get_config(self) -> str:
"""Gets the config for the miner and sets it as `self.config`. """Gets the config for the miner and sets it as `self.config`.
@@ -161,26 +162,19 @@ class CGMiner(BaseMiner):
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
return False return False
async def get_errors(self) -> List[MinerErrorData]: async def get_errors(self) -> list:
return [] return []
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def get_mac(self) -> str: async def get_mac(self) -> str:
return "00:00:00:00:00:00" return "00:00:00:00:00:00"
async def get_data(self, allow_warning: bool = False) -> MinerData: async def get_data(self) -> MinerData:
"""Get data from the miner. """Get data from the miner.
Returns: Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data. A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
""" """
data = MinerData( data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
ip=str(self.ip),
ideal_chips=self.nominal_chips * self.ideal_hashboards,
ideal_hashboards=self.ideal_hashboards,
)
board_offset = -1 board_offset = -1
fan_offset = -1 fan_offset = -1
@@ -203,7 +197,7 @@ class CGMiner(BaseMiner):
miner_data = None miner_data = None
for i in range(PyasicSettings().miner_get_data_retries): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"summary", "pools", "stats", allow_warning=allow_warning "summary", "pools", "stats", ignore_x19_error=True
) )
if miner_data: if miner_data:
break break
@@ -236,38 +230,19 @@ class CGMiner(BaseMiner):
if board_offset == -1: if board_offset == -1:
board_offset = 1 board_offset = 1
env_temp_list = [] data.left_chips = boards[1].get(f"chain_acn{board_offset}")
for i in range(board_offset, board_offset + self.ideal_hashboards): data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
hashboard = HashBoard( data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
slot=i - board_offset, expected_chips=self.nominal_chips
)
chip_temp = boards[1].get(f"temp{i}") data.left_board_hashrate = round(
if chip_temp: float(boards[1].get(f"chain_rate{board_offset}")) / 1000, 2
hashboard.chip_temp = round(chip_temp) )
data.center_board_hashrate = round(
temp = boards[1].get(f"temp2_{i}") float(boards[1].get(f"chain_rate{board_offset+1}")) / 1000, 2
if temp: )
hashboard.temp = round(temp) data.right_board_hashrate = round(
float(boards[1].get(f"chain_rate{board_offset+2}")) / 1000, 2
hashrate = boards[1].get(f"chain_rate{i}") )
if hashrate:
hashboard.hashrate = round(float(hashrate) / 1000, 2)
chips = boards[1].get(f"chain_acn{i}")
if chips:
hashboard.chips = chips
hashboard.missing = False
if (not chips) or (not chips > 0):
hashboard.missing = True
data.hashboards.append(hashboard)
if f"temp_pcb{i}" in boards[1].keys():
env_temp = boards[1][f"temp_pcb{i}"].split("-")[0]
if not env_temp == 0:
env_temp_list.append(int(env_temp))
if not env_temp_list == []:
data.env_temp = round(sum(env_temp_list) / len(env_temp_list))
if stats: if stats:
temp = stats.get("STATS") temp = stats.get("STATS")
@@ -285,6 +260,19 @@ class CGMiner(BaseMiner):
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}") data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
) )
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
env_temp_list = []
for item in range(3):
board_temp = temp[1].get(f"temp{item + board_offset}")
chip_temp = temp[1].get(f"temp2_{item + board_offset}")
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
setattr(data, f"{board_map[item]}_temp", board_temp)
if f"temp_pcb{item}" in temp[1].keys():
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
if not env_temp == 0:
env_temp_list.append(int(env_temp))
data.env_temp = sum(env_temp_list) / len(env_temp_list)
if pools: if pools:
pool_1 = None pool_1 = None
pool_2 = None pool_2 = None
@@ -335,6 +323,3 @@ class CGMiner(BaseMiner):
data.pool_split = str(quota) data.pool_split = str(quota)
return data return data
async def set_power_limit(self, wattage: int) -> bool:
return False

View File

@@ -12,9 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import ipaddress
from pyasic.miners._backends import BMMiner from pyasic.miners._backends import BMMiner
import ipaddress
class Hiveon(BMMiner): class Hiveon(BMMiner):
@@ -61,6 +60,3 @@ class Hiveon(BMMiner):
bad_boards[board] = [] bad_boards[board] = []
bad_boards[board].append(chain) bad_boards[board].append(chain)
return bad_boards return bad_boards
async def set_power_limit(self, wattage: int) -> bool:
return False

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
# 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 S19XP(BaseMiner): # 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners.base import BaseMiner from pyasic.miners import BaseMiner
class S19a(BaseMiner): # noqa - ignore ABC method implementation class S19a(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip

View File

@@ -1,24 +0,0 @@
# 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 S19aPro(BaseMiner): # 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners.base import BaseMiner from pyasic.miners import BaseMiner
class S19j(BaseMiner): # noqa - ignore ABC method implementation class S19j(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip

View File

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

View File

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

View File

@@ -14,9 +14,10 @@
from .S19 import S19 from .S19 import S19
from .S19_Pro import S19Pro 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 import S19j
from .S19j_Pro import S19jPro from .S19j_Pro import S19jPro
from .S19a import S19a
from .T19 import T19 from .T19 import T19

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
# 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 InnosiliconT3HPlus(BaseMiner): # 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

@@ -1,15 +0,0 @@
# 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,7 @@
from .M20 import M20, M20V10 from .M20 import M20, M20V10
from .M20S import M20S, M20SV10, M20SV20 from .M20S import M20S, M20SV10, M20SV20
from .M20S_Plus import M20SPlus from .M20S_Plus import M20SPlus
from .M21 import M21 from .M21 import M21
from .M21S import M21S, M21SV20, M21SV60 from .M21S import M21S, M21SV20, M21SV60
from .M21S_Plus import M21SPlus from .M21S_Plus import M21SPlus

View File

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

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners.base import BaseMiner from pyasic.miners import BaseMiner
class M30SPlus(BaseMiner): # noqa - ignore ABC method implementation class M30SPlus(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip
@@ -24,7 +24,7 @@ class M30SPlus(BaseMiner): # noqa - ignore ABC method implementation
self.fan_count = 2 self.fan_count = 2
class M30SPlusVG60(BaseMiner): # noqa - ignore ABC method implementation class M30SPlusVG60(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip
@@ -33,7 +33,7 @@ class M30SPlusVG60(BaseMiner): # noqa - ignore ABC method implementation
self.fan_count = 2 self.fan_count = 2
class M30SPlusVE40(BaseMiner): # noqa - ignore ABC method implementation class M30SPlusVE40(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip
@@ -42,7 +42,7 @@ class M30SPlusVE40(BaseMiner): # noqa - ignore ABC method implementation
self.fan_count = 2 self.fan_count = 2
class M30SPlusVF20(BaseMiner): # noqa - ignore ABC method implementation class M30SPlusVF20(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip

View File

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

View File

@@ -12,22 +12,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners.base import BaseMiner from pyasic.miners import BaseMiner
class M31S(BaseMiner): # noqa - ignore ABC method implementation class M31S(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip
self.model = "M31S" self.model = "M31S"
self.nominal_chips = 111 # TODO: Add chip count for this miner (per board) - self.nominal_chips
self.fan_count = 2
class M31SV70(BaseMiner): # 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 self.fan_count = 2

View File

@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners.base import BaseMiner from pyasic.miners import BaseMiner
class M31SPlus(BaseMiner): # noqa - ignore ABC method implementation class M31SPlus(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip
@@ -24,55 +24,10 @@ class M31SPlus(BaseMiner): # noqa - ignore ABC method implementation
self.fan_count = 2 self.fan_count = 2
class M31SPlusVE20(BaseMiner): # noqa - ignore ABC method implementation class M31SPlusVE20(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip
self.model = "M31S+ VE20" self.model = "M31S+ VE20"
self.nominal_chips = 78 self.nominal_chips = 78
self.fan_count = 2 self.fan_count = 2
class M31SPlusV30(BaseMiner): # 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(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V40"
self.nominal_chips = 123
self.fan_count = 2
class M31SPlusV60(BaseMiner): # 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(BaseMiner): # 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(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ V90"
self.nominal_chips = 117
self.fan_count = 2

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +0,0 @@
# 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 M50(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M50"
self.nominal_chips = 105
self.fan_count = 2
class M50VH50(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M50 VH50"
self.nominal_chips = 105
self.fan_count = 2

View File

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

View File

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

View File

@@ -12,9 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners._types import S17 # noqa - Ignore access to _module
from .X17 import BMMinerX17 from .X17 import BMMinerX17
from pyasic.miners._types import S17 # noqa - Ignore access to _module
class BMMinerS17(BMMinerX17, S17): class BMMinerS17(BMMinerX17, S17):

View File

@@ -12,9 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners._types import S17Plus # noqa - Ignore access to _module
from .X17 import BMMinerX17 from .X17 import BMMinerX17
from pyasic.miners._types import S17Plus # noqa - Ignore access to _module
class BMMinerS17Plus(BMMinerX17, S17Plus): class BMMinerS17Plus(BMMinerX17, S17Plus):

View File

@@ -12,9 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners._types import S17Pro # noqa - Ignore access to _module
from .X17 import BMMinerX17 from .X17 import BMMinerX17
from pyasic.miners._types import S17Pro # noqa - Ignore access to _module
class BMMinerS17Pro(BMMinerX17, S17Pro): class BMMinerS17Pro(BMMinerX17, S17Pro):

View File

@@ -12,9 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners._types import S17e # noqa - Ignore access to _module
from .X17 import BMMinerX17 from .X17 import BMMinerX17
from pyasic.miners._types import S17e # noqa - Ignore access to _module
class BMMinerS17e(BMMinerX17, S17e): class BMMinerS17e(BMMinerX17, S17e):

View File

@@ -12,9 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners._types import T17 # noqa - Ignore access to _module
from .X17 import BMMinerX17 from .X17 import BMMinerX17
from pyasic.miners._types import T17 # noqa - Ignore access to _module
class BMMinerT17(BMMinerX17, T17): class BMMinerT17(BMMinerX17, T17):

View File

@@ -12,9 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners._types import T17Plus # noqa - Ignore access to _module
from .X17 import BMMinerX17 from .X17 import BMMinerX17
from pyasic.miners._types import T17Plus # noqa - Ignore access to _module
class BMMinerT17Plus(BMMinerX17, T17Plus): class BMMinerT17Plus(BMMinerX17, T17Plus):

View File

@@ -12,9 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners._types import T17e # noqa - Ignore access to _module
from .X17 import BMMinerX17 from .X17 import BMMinerX17
from pyasic.miners._types import T17e # noqa - Ignore access to _module
class BMMinerT17e(BMMinerX17, T17e): class BMMinerT17e(BMMinerX17, T17e):

View File

@@ -12,12 +12,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import Union from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
from pyasic.settings import PyasicSettings
import httpx import httpx
from typing import Union
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
from pyasic.settings import PyasicSettings
class BMMinerX17(BMMiner): class BMMinerX17(BMMiner):

View File

@@ -16,6 +16,7 @@ from .S17 import BMMinerS17
from .S17_Plus import BMMinerS17Plus from .S17_Plus import BMMinerS17Plus
from .S17_Pro import BMMinerS17Pro from .S17_Pro import BMMinerS17Pro
from .S17e import BMMinerS17e from .S17e import BMMinerS17e
from .T17 import BMMinerT17 from .T17 import BMMinerT17
from .T17_Plus import BMMinerT17Plus from .T17_Plus import BMMinerT17Plus
from .T17e import BMMinerT17e from .T17e import BMMinerT17e

View File

@@ -12,9 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners._types import S19 # noqa - Ignore access to _module
from .X19 import BMMinerX19 from .X19 import BMMinerX19
from pyasic.miners._types import S19 # noqa - Ignore access to _module
class BMMinerS19(BMMinerX19, S19): class BMMinerS19(BMMinerX19, S19):

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