Compare commits

..

13 Commits

Author SHA1 Message Date
UpstreamData
efd7c9bf87 version: bump version number. 2023-06-12 10:15:19 -06:00
UpstreamData
708d48dcc7 bug: fix caching, and update references to MinerFactory() to miner_factory 2023-06-12 10:14:20 -06:00
UpstreamData
44d6e4cdd4 feature: add caching to miner factory. 2023-06-12 09:44:47 -06:00
UpstreamData
b823e13694 refactor: refactor the format of miner types. 2023-06-09 11:42:25 -06:00
UpstreamData
442b21810d bug: fix not instantiating some web sessions properly. 2023-06-08 16:23:00 -06:00
UpstreamData
a2553aab9d bug: fix some naming issues, and add timeout to getting miner model. 2023-06-08 16:14:11 -06:00
UpstreamData
7cef0b3d3c feature: refactor get_miner to allow models to be selected as strings then selected in the top level get_miner function. 2023-06-08 09:33:55 -06:00
UpstreamData
6875ef54bd feature: add Avalonminer support in update miner factory, and add support for A1166 and A1246. 2023-06-07 15:53:07 -06:00
UpstreamData
22f2e2edcd bug: remove some redundant .upper() calls. 2023-06-07 13:46:18 -06:00
UpstreamData
3b1ad46a1a bug: remove some redundant .upper() calls. 2023-06-07 13:45:32 -06:00
UpstreamData
0b9df0f7c4 feature: refactor miner class list to be much more readable. 2023-06-07 13:44:50 -06:00
UpstreamData
c56686a18d feature: refactor to aiohttp and fix a lot of bugs with factory. Still needs support for some miners. 2023-06-07 11:37:52 -06:00
UpstreamData
d6a42238d0 feature: Start refactor to new style of get_miner. Needs testing and stability fixes. 2023-06-05 09:03:49 -06:00
737 changed files with 14345 additions and 46719 deletions

View File

@@ -13,10 +13,10 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v2
- name: Publish GH release
uses: softprops/action-gh-release@v2.1.0
uses: softprops/action-gh-release@v0.1.14
- name: Build using poetry and publish to PyPi
uses: JRubics/poetry-publish@v2.0
uses: JRubics/poetry-publish@v1.11
with:
pypi_token: ${{ secrets.PYPI_API_KEY }}

1
.gitignore vendored
View File

@@ -7,4 +7,3 @@ pyvenv.cfg
bin/
lib/
.idea/
.vs/

View File

@@ -1,33 +1,16 @@
ci:
skip:
- poetry-lock
- unittest
- generate-docs
repos:
- repo: https://github.com/python-poetry/poetry
rev: 2.1.2
hooks:
- id: poetry-check
- id: poetry-lock
- id: poetry-install
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: check-yaml
name: check-yaml for mkdocs.yml
files: ^mkdocs\.yml$
args: [--unsafe]
- id: check-yaml
name: check-yaml for other YAML files
exclude: ^mkdocs\.yml$
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 25.1.0
rev: 22.6.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 6.0.1
rev: 5.10.1
hooks:
- id: isort
name: isort (python)
@@ -38,12 +21,7 @@ repos:
name: unittest
entry: python -m unittest discover
language: system
types: [ python ]
'types': [python]
args: ["-p '*test.py'"] # Probably this option is absolutely not needed.
pass_filenames: false
- id: generate-docs
name: generate-docs
entry: python docs/generate_miners.py
language: system
types: [ python ]
pass_filenames: false
stages: [commit]

View File

@@ -1,18 +1,20 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-20.04
tools: { python: "3.11" }
jobs:
pre_create_environment:
- asdf plugin add poetry
- asdf install poetry latest
- asdf global poetry latest
- poetry config virtualenvs.create false
post_install:
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --only docs
tools:
python: "3.9"
mkdocs:
configuration: mkdocs.yml
configuration: mkdocs.yml
# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: docs/requirements.txt

325
README.md
View File

@@ -1,293 +1,142 @@
# pyasic
*A simplified and standardized interface for Bitcoin ASICs.*
*A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH.*
[![PyPI - Version](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pyasic)](https://pypi.org/project/pyasic/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![pypi](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)
[![python](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![Read the Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![GitHub](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
## Documentation and Supported Miners
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/).
[![Python - Supported Versions](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![CodeFactor - Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
[![Commit Activity - master](https://img.shields.io/github/commit-activity/y/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/commits/master/)
Supported miners are listed in the docs, [here](https://pyasic.readthedocs.io/en/latest/miners/supported_types/).
[![Code Style - Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Read The Docs - Docs](https://img.shields.io/readthedocs/pyasic)](https://docs.pyasic.org)
[![License - Apache 2.0](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
---
## Intro
Welcome to `pyasic`! `pyasic` uses an asynchronous method of communicating with ASIC miners on your network, which makes it super fast.
[Click here to view supported miner types](https://docs.pyasic.org/en/latest/miners/supported_types/)
---
## Installation
You can install pyasic directly from pip with the command `pip install pyasic`.
It is recommended to install `pyasic` in a [virtual environment](https://realpython.com/python-virtual-environments-a-primer/#what-other-popular-options-exist-aside-from-venv) to isolate it from the rest of your system. Options include:
- [pypoetry](https://python-poetry.org/): the reccommended way, since pyasic already uses it by default. Use version 2.0+
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).
```
poetry install
```
## Developer Setup
It is highly reccommended that you contribute to this project through [`pyasic-super`](https://github.com/UpstreamData/pyasic-super) using its submodules. This allows testing in conjunction with other `pyasic` related programs.
- [venv](https://docs.python.org/3/library/venv.html): included in Python standard library but has fewer features than other options
- [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv): [pyenv](https://github.com/pyenv/pyenv) plugin for managing virtualenvs
<br>
```
pyenv install <python version number>
pyenv virtualenv <python version number> <env name>
pyenv activate <env name>
```
This repo uses poetry for dependencies, which can be installed by following the guide on their website [here](https://python-poetry.org/docs/#installation).
- [conda](https://docs.conda.io/en/latest/)
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.
##### Installing `pyasic`
Finally, initialize pre-commit hooks with `poetry run pre-commit install`.
`python -m pip install pyasic` or `poetry install`
### Documentation Testing
Testing the documentation can be done by running `poetry run mkdocs serve`, whcih will serve the documentation locally on port 8000.
##### Additional Developer Setup
```
poetry install --with dev
pre-commit install
```
## Interfacing with miners programmatically
##### Building Documentation Locally
```
poetry install --with docs
python docs/generate_miners.py
poetry run mkdocs serve
```
There are 2 main ways to get a miner (and the functions attached to it), via scanning or via the `MinerFactory()`.
---
## Getting started
Getting started with `pyasic` is easy. First, find your miner (or miners) on the network by scanning for them or getting the correct class automatically for them if you know the IP.
##### Scanning for miners
To scan for miners in `pyasic`, we use the class `MinerNetwork`, which abstracts the search, communication, identification, setup, and return of a miner to 1 command.
The command `MinerNetwork.scan()` returns a list that contains any miners found.
```python
import asyncio # asyncio for handling the async part
from pyasic.network import MinerNetwork # miner network handles the scanning
async def scan_miners(): # define async scan function to allow awaiting
# create a miner network
# you can pass in any IP and it will use that in a subnet with a /24 mask (255 IPs).
network = MinerNetwork.from_subnet("192.168.1.50/24") # this uses the 192.168.1.0-255 network
# scan for miners asynchronously
# this will return the correct type of miners if they are supported with all functionality.
miners = await network.scan()
print(miners)
if __name__ == "__main__":
asyncio.run(scan_miners()) # run the scan asynchronously with asyncio.run()
```
---
##### Creating miners based on IP
If you already know the IP address of your miner or miners, you can use the `MinerFactory` to communicate and identify the miners, or an abstraction of its functionality, `get_miner()`.
The function `get_miner()` will return any miner it found at the IP address specified, or an `UnknownMiner` if it cannot identify the miner.
```python
import asyncio # asyncio for handling the async part
from pyasic import get_miner # handles miner creation
async def get_miners(): # define async scan function to allow awaiting
# get the miner with the miner factory
# the miner factory is a singleton, and will always use the same object and cache
# this means you can always call it as MinerFactory().get_miner(), or just get_miner()
miner_1 = await get_miner("192.168.1.75")
miner_2 = await get_miner("192.168.1.76")
print(miner_1, miner_2)
# can also gather these, since they are async
# gathering them will get them both at the same time
# this makes it much faster to get a lot of miners at a time
tasks = [get_miner("192.168.1.75"), get_miner("192.168.1.76")]
miners = await asyncio.gather(*tasks)
print(miners)
if __name__ == "__main__":
asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run()
```
---
## Data gathering
Once you have your miner(s) identified, you will likely want to get data from the miner(s). You can do this using a built-in function in each miner called `get_data()`.
This function will return an instance of the dataclass `MinerData` with all data it can gather from the miner.
Each piece of data in a `MinerData` instance can be referenced by getting it as an attribute, such as `MinerData().hashrate`.
##### One miner
#### Scanning for miners
```python
import asyncio
from pyasic import get_miner
async def gather_miner_data():
miner = await get_miner("192.168.1.75")
if miner is not None:
miner_data = await miner.get_data()
print(miner_data) # all data from the dataclass
print(miner_data.hashrate) # hashrate of the miner in TH/s
from pyasic.network import MinerNetwork
# define asynchronous function to scan for miners
async def scan_and_get_data():
# Define network range to be used for scanning
# This can take a list of IPs, a constructor string, or an IP and subnet mask
# The standard mask is /24 (x.x.x.0-255), and you can pass any IP address in the subnet
net = MinerNetwork("192.168.1.69", mask=24)
# Scan the network for miners
# This function returns a list of miners of the correct type as a class
miners: list = await net.scan_network_for_miners()
# We can now get data from any of these miners
# To do them all we have to create a list of tasks and gather them
tasks = [miner.get_data() for miner in miners]
# Gather all tasks asynchronously and run them
data = await asyncio.gather(*tasks)
# Data is now a list of MinerData, and we can reference any part of that
# Print out all data for now
for item in data:
print(item)
if __name__ == "__main__":
asyncio.run(gather_miner_data())
```
---
##### Multiple miners
You can do something similar with multiple miners, with only needing to make a small change to get all the data at once.
```python
import asyncio # asyncio for handling the async part
from pyasic.network import MinerNetwork # miner network handles the scanning
async def gather_miner_data(): # define async scan function to allow awaiting
network = MinerNetwork.from_subnet("192.168.1.50/24")
miners = await network.scan()
# we need to asyncio.gather() all the miners get_data() functions to make them run together
all_miner_data = await asyncio.gather(*[miner.get_data() for miner in miners])
for miner_data in all_miner_data:
print(miner_data) # print out all the data one by one
if __name__ == "__main__":
asyncio.run(gather_miner_data())
asyncio.run(scan_and_get_data())
```
---
## Miner control
`pyasic` exposes a standard interface for each miner using control functions.
Every miner class in `pyasic` must implement all the control functions defined in `BaseMiner`.
These functions are
`check_light`,
`fault_light_off`,
`fault_light_on`,
`get_config`,
`get_data`,
`get_errors`,
`get_hostname`,
`get_model`,
`reboot`,
`restart_backend`,
`stop_mining`,
`resume_mining`,
`is_mining`,
`send_config`, and
`set_power_limit`.
##### Usage
#### Getting a miner if you know the IP
```python
import asyncio
from pyasic import get_miner
async def set_fault_light():
miner = await get_miner("192.168.1.20")
# define asynchronous function to get miner and data
async def get_miner_data(miner_ip: str):
# Use MinerFactory to get miner
# MinerFactory is a singleton, so we can just get the instance in place
miner = await get_miner(miner_ip)
# call control function
await miner.fault_light_on()
# Get data from the miner
data = await miner.get_data()
print(data)
if __name__ == "__main__":
asyncio.run(set_fault_light())
asyncio.run(get_miner_data("192.168.1.69"))
```
---
## Helper dataclasses
### Advanced data gathering
##### `MinerConfig` and `MinerData`
If needed, this library exposes a wrapper for the miner API that can be used for advanced data gathering.
`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()`.
You can see more information on basic usage of the APIs past this example in the docs [here](https://pyasic.readthedocs.io/en/latest/API/api/).
---
Please see the appropriate API documentation page (pyasic docs -> Advanced -> Miner APIs -> your API type) for a link to that specific miner's API documentation page and more information.
##### MinerData
`MinerData` is a return from the [`get_data()`](#get-data) function, and is used to have a consistent dataset across all returns.
You can call `MinerData.as_dict()` to get the dataclass as a dictionary, and there are many other helper functions contained in the class to convert to different data formats.
`MinerData` instances can also be added to each other to combine their data and can be divided by a number to divide all their data, allowing you to get average data from many miners by doing -
```python
from pyasic import MinerData
# examples of miner data
d1 = MinerData("192.168.1.1")
d2 = MinerData("192.168.1.2")
list_of_miner_data = [d1, d2]
average_data = sum(list_of_miner_data, start=MinerData("0.0.0.0"))/len(list_of_miner_data)
```
---
##### MinerConfig
`MinerConfig` is `pyasic`'s way to represent a configuration file from a miner.
It is designed to unionize the configuration of all supported miner types, and is the return from [`get_config()`](#get-config).
Each miner has a unique way to convert the `MinerConfig` to their specific type, there are helper functions in the class.
In most cases these helper functions should not be used, as [`send_config()`](#send-config) takes a [`MinerConfig` and will do the conversion to the right type for you.
You can use the `MinerConfig` as follows:
#### List available API commands
```python
import asyncio
from pyasic import get_miner
async def set_fault_light():
miner = await get_miner("192.168.1.20")
async def get_api_commands(miner_ip: str):
# Get the miner
miner = await get_miner(miner_ip)
# get config
cfg = await miner.get_config()
# List all available commands
# Can also be called explicitly with the function miner.api.get_commands()
print(miner.api.commands)
# send config
await miner.send_config(cfg)
if __name__ == "__main__":
asyncio.run(set_fault_light())
asyncio.run(get_api_commands("192.168.1.69"))
```
---
## Settings
#### Use miner API commands to gather data
`pyasic` has settings designed to make using large groups of miners easier. You can set the default password for all types of miners using the `pyasic.settings` module, used as follows:
The miner API commands will raise an `APIError` if they fail with a bad status code, to bypass this you must send them manually by using `miner.api.send_command(command, ignore_errors=True)`
```python
from pyasic import settings
import asyncio
settings.update("default_antminer_web_password", "my_pwd")
```
from pyasic import get_miner
##### Default values:
```
"network_ping_retries": 1,
"network_ping_timeout": 3,
"network_scan_semaphore": None,
"factory_get_retries": 1,
"factory_get_timeout": 3,
"get_data_retries": 1,
"api_function_timeout": 5,
"antminer_mining_mode_as_str": False,
"default_whatsminer_rpc_password": "admin",
"default_innosilicon_web_password": "admin",
"default_antminer_web_password": "root",
"default_bosminer_web_password": "root",
"default_vnish_web_password": "admin",
"default_goldshell_web_password": "123456789",
"default_auradine_web_password": "admin",
"default_epic_web_password": "letmein",
"default_hive_web_password": "admin",
"default_antminer_ssh_password": "miner",
"default_bosminer_ssh_password": "root",
# ADVANCED
# Only use this if you know what you are doing
"socket_linger_time": 1000,
async def get_api_commands(miner_ip: str):
# Get the miner
miner = await get_miner(miner_ip)
# Run the devdetails command
# This is equivalent to await miner.api.send_command("devdetails")
devdetails: dict = await miner.api.devdetails()
print(devdetails)
if __name__ == "__main__":
asyncio.run(get_api_commands("192.168.1.69"))
```

26
docs/API/api.md Normal file
View File

@@ -0,0 +1,26 @@
# pyasic
## Miner APIs
Each miner has a unique API that is used to communicate with it.
Each of these API types has commands that differ between them, and some commands have data that others do not.
Each miner that is a subclass of [`BaseMiner`][pyasic.miners.BaseMiner] should have an API linked to it as `Miner.api`.
All API implementations inherit from [`BaseMinerAPI`][pyasic.API.BaseMinerAPI], which implements the basic communications protocols.
[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] cannot be instantiated directly, it will raise a `TypeError`.
Use these instead -
#### [BFGMiner API][pyasic.API.bfgminer.BFGMinerAPI]
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]
#### [BOSMiner API][pyasic.API.bosminer.BOSMinerAPI]
#### [BTMiner API][pyasic.API.btminer.BTMinerAPI]
#### [CGMiner API][pyasic.API.cgminer.CGMinerAPI]
#### [Unknown API][pyasic.API.unknown.UnknownAPI]
<br>
## BaseMinerAPI
::: pyasic.API.BaseMinerAPI
handler: python
options:
heading_level: 4

View File

@@ -1,6 +1,6 @@
# pyasic
## BMMinerRPCAPI
::: pyasic.rpc.bmminer.BMMinerRPCAPI
## BFGMinerAPI
::: pyasic.API.bfgminer.BFGMinerAPI
handler: python
options:
show_root_heading: false

View File

@@ -1,6 +1,6 @@
# pyasic
## ePICWebAPI
::: pyasic.web.epic.ePICWebAPI
## BMMinerAPI
::: pyasic.API.bmminer.BMMinerAPI
handler: python
options:
show_root_heading: false

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

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

View File

@@ -1,6 +1,6 @@
# pyasic
## VNishWebAPI
::: pyasic.web.vnish.VNishWebAPI
## BTMinerAPI
::: pyasic.API.btminer.BTMinerAPI
handler: python
options:
show_root_heading: false

View File

@@ -1,6 +1,6 @@
# pyasic
## MaraWebAPI
::: pyasic.web.marathon.MaraWebAPI
## CGMinerAPI
::: pyasic.API.cgminer.CGMinerAPI
handler: python
options:
show_root_heading: false

View File

@@ -1,6 +1,6 @@
# pyasic
## UnknownRPCAPI
::: pyasic.rpc.unknown.UnknownRPCAPI
## UnknownAPI
::: pyasic.API.unknown.UnknownAPI
handler: python
options:
show_root_heading: false

View File

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

View File

@@ -1,6 +1,6 @@
# pyasic
## Miner Data
::: pyasic.data.MinerData
handler: python
options:
@@ -13,10 +13,3 @@
options:
show_root_heading: false
heading_level: 4
## Fan Data
::: pyasic.data.Fan
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,204 +0,0 @@
import asyncio
import importlib
import os
import warnings
from pathlib import Path
from pyasic.miners.factory import MINER_CLASSES, MinerTypes
warnings.filterwarnings("ignore")
def path(cls):
module = importlib.import_module(cls.__module__)
return module.__name__ + "." + cls.__name__
def make(cls):
p = path(cls)
return p.split(".")[2]
def model_type(cls):
p = path(cls)
return p.split(".")[4]
def backend_str(backend: MinerTypes) -> str:
match backend:
case MinerTypes.ANTMINER:
return "Stock Firmware Antminers"
case MinerTypes.AURADINE:
return "Stock Firmware Auradine Miners"
case MinerTypes.AVALONMINER:
return "Stock Firmware Avalonminers"
case MinerTypes.VNISH:
return "Vnish Firmware Miners"
case MinerTypes.EPIC:
return "ePIC Firmware Miners"
case MinerTypes.BRAIINS_OS:
return "BOS+ Firmware Miners"
case MinerTypes.HIVEON:
return "HiveOS Firmware Miners"
case MinerTypes.INNOSILICON:
return "Stock Firmware Innosilicons"
case MinerTypes.WHATSMINER:
return "Stock Firmware Whatsminers"
case MinerTypes.GOLDSHELL:
return "Stock Firmware Goldshells"
case MinerTypes.LUX_OS:
return "LuxOS Firmware Miners"
case MinerTypes.MARATHON:
return "Mara Firmware Miners"
case MinerTypes.BITAXE:
return "Stock Firmware BitAxe Miners"
case MinerTypes.LUCKYMINER:
return "Stock Firmware Lucky Miners"
case MinerTypes.ICERIVER:
return "Stock Firmware IceRiver Miners"
case MinerTypes.HAMMER:
return "Stock Firmware Hammer Miners"
case MinerTypes.VOLCMINER:
return "Stock Firmware Volcminers"
case MinerTypes.ELPHAPEX:
return "Stock Firmware Elphapex Miners"
case MinerTypes.MSKMINER:
return "MSKMiner Firmware Miners"
raise TypeError("Unknown miner backend, cannot generate docs")
def create_url_str(mtype: str):
return (
mtype.lower()
.replace(" ", "-")
.replace("(", "")
.replace(")", "")
.replace("+", "_1")
)
HEADER_FORMAT = "# pyasic\n## {} Models\n\n"
MINER_HEADER_FORMAT = "## {}\n"
DATA_FORMAT = """
- [{}] Shutdowns
- [{}] Power Modes
- [{}] Setpoints
- [{}] Presets
::: {}
handler: python
options:
show_root_heading: false
heading_level: 0
"""
SUPPORTED_TYPES_HEADER = """# pyasic
## Supported Miners
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
Keep in mind that some functionality is only supported for specific miners or firmwares, please check the page for your miner to make sure the functionality you need is supported.
##### pyasic currently supports the following miners and subtypes:
<style>
details {
margin:0px;
padding-top:0px;
padding-bottom:0px;
}
</style>
"""
BACKEND_TYPE_HEADER = """
<details>
<summary>{}:</summary>
<ul>"""
MINER_TYPE_HEADER = """
<details>
<summary>{} Series:</summary>
<ul>"""
MINER_DETAILS = """
<li><a href="../{}/{}#{}">{}</a></li>"""
MINER_TYPE_CLOSER = """
</ul>
</details>"""
BACKEND_TYPE_CLOSER = """
</ul>
</details>"""
m_data = {}
done = []
for m in MINER_CLASSES:
for t in sorted(MINER_CLASSES[m], key=lambda x: x or ""):
if t is not None and MINER_CLASSES[m][t] not in done:
miner = MINER_CLASSES[m][t]
if make(miner) not in m_data:
m_data[make(miner)] = {}
if model_type(miner) not in m_data[make(miner)]:
m_data[make(miner)][model_type(miner)] = []
m_data[make(miner)][model_type(miner)].append(miner)
done.append(miner)
def create_directory_structure(directory, data):
if not os.path.exists(directory):
os.makedirs(directory)
for key, value in data.items():
subdirectory = os.path.join(directory, key)
if isinstance(value, dict):
create_directory_structure(subdirectory, value)
elif isinstance(value, list):
file_path = os.path.join(subdirectory + ".md")
with open(file_path, "w") as file:
file.write(HEADER_FORMAT.format(key))
for item in value:
obj = item("1.1.1.1")
header = obj.model
file.write(MINER_HEADER_FORMAT.format(header))
file.write(
DATA_FORMAT.format(
"x" if obj.supports_shutdown else " ",
"x" if obj.supports_power_modes else " ",
"x" if obj.supports_autotuning else " ",
"x" if obj.supports_presets else " ",
path(item),
)
)
def create_supported_types(directory):
with open(os.path.join(directory, "supported_types.md"), "w") as file:
file.write(SUPPORTED_TYPES_HEADER)
for mback in MINER_CLASSES:
backend_types = {}
file.write(BACKEND_TYPE_HEADER.format(backend_str(mback)))
for mtype in MINER_CLASSES[mback]:
if mtype is None:
continue
m = MINER_CLASSES[mback][mtype]
if model_type(m) not in backend_types:
backend_types[model_type(m)] = []
backend_types[model_type(m)].append(m)
for mtype in backend_types:
file.write(MINER_TYPE_HEADER.format(mtype))
for minstance in backend_types[mtype]:
model = minstance("1.1.1.1").model
file.write(
MINER_DETAILS.format(
make(minstance), mtype, create_url_str(model), model
)
)
file.write(MINER_TYPE_CLOSER)
file.write(BACKEND_TYPE_CLOSER)
if __name__ == "__main__":
root_directory = Path(__file__).parent.joinpath("miners")
create_directory_structure(root_directory, m_data)
create_supported_types(root_directory)

View File

@@ -1,212 +1,259 @@
# pyasic
*A simplified and standardized interface for Bitcoin ASICs.*
*A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH.*
[![PyPI - Version](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pyasic)](https://pypi.org/project/pyasic/)
[![Python - Supported Versions](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![CodeFactor - Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
[![Commit Activity - master](https://img.shields.io/github/commit-activity/y/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/commits/master/)
[![Code Style - Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Read The Docs - Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![License - Apache 2.0](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![pypi](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)
[![python](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![Read the Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![GitHub](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
## Intro
---
Welcome to `pyasic`! `pyasic` uses an asynchronous method of communicating with ASIC miners on your network, which makes it super fast.
Welcome to pyasic! Pyasic uses an asynchronous method of communicating with asic miners on your network, which makes it super fast.
[Click here to view supported miner types](miners/supported_types.md)
[Supported Miner Types](miners/supported_types.md)
## Installation
---
It is recommended to install `pyasic` in a [virtual environment](https://realpython.com/python-virtual-environments-a-primer/#what-other-popular-options-exist-aside-from-venv) to isolate it from the rest of your system.
`pyasic` can be installed directly from pip, either with `pip install pyasic`, or a different command if using a tool like `pypoetry`.
Getting started with pyasic is easy. First, find your miner (or miners) on the network by scanning for them or getting the correct class automatically for them if you know the IP.
## Getting started
---
Getting started with `pyasic` is easy. First, find your miner (or miners) on the network by scanning for them or getting the correct class automatically for them if you know the IP.
<br>
### Scanning for miners
To scan for miners in `pyasic`, we use the class [`MinerNetwork`][pyasic.network.MinerNetwork], which abstracts the search, communication, identification, setup, and return of a miner to 1 command.
The command [`MinerNetwork.scan()`][pyasic.network.MinerNetwork.scan] returns a list that contains any miners found.
```python3
import asyncio# (1)!
from pyasic.network import MinerNetwork# (2)!
## Scanning for miners
To scan for miners in pyasic, we use the class [`MinerNetwork`][pyasic.network.MinerNetwork], which abstracts the search, communication, identification, setup, and return of a miner to 1 command.
The command [`MinerNetwork().scan_network_for_miners()`][pyasic.network.MinerNetwork.scan_network_for_miners] returns a list that contains any miners found.
```python
import asyncio # asyncio for handling the async part
from pyasic.network import MinerNetwork # miner network handles the scanning
async def scan_miners():# (3)!
network = MinerNetwork.from_subnet("192.168.1.50/24")# (4)!
async def scan_miners(): # define async scan function to allow awaiting
# create a miner network
# you can pass in any IP and it will use that in a subnet with a /24 mask (255 IPs).
network = MinerNetwork("192.168.1.50") # this uses the 192.168.1.0-255 network
miners = await network.scan()# (5)!
# scan for miners asynchronously
# this will return the correct type of miners if they are supported with all functionality.
miners = await network.scan_network_for_miners()
print(miners)
if __name__ == "__main__":
asyncio.run(scan_miners())# (6)!
asyncio.run(scan_miners()) # run the scan asynchronously with asyncio.run()
```
1. `asyncio` for handling the async part.
2. `MinerNetwork` handles the scanning.
3. Define an async function to allow awaiting.
4. Create a miner network.
You can pass in any IP and it will use that in a subnet with a /24 mask (255 IPs).
This uses the 192.168.1.0-255 network.
5. Scan for miners asynchronously.
This will return the correct type of miners (if they are supported) with all functionality.
6. Run the scan asynchronously with asyncio.run().
<br>
---
### Creating miners based on IP
If you already know the IP address of your miner or miners, you can use the [`MinerFactory`][pyasic.miners.factory.MinerFactory] to communicate and identify the miners, or an abstraction of its functionality, [`get_miner()`][pyasic.miners.get_miner].
The function [`get_miner()`][pyasic.miners.get_miner] will return any miner it found at the IP address specified, or an `UnknownMiner` if it cannot identify the miner.
## Creating miners based on IP
If you already know the IP address of your miner or miners, you can use the [`MinerFactory`][pyasic.miners.miner_factory.MinerFactory] to communicate and identify the miners, or an abstraction of its functionality, [`get_miner()`][pyasic.miners.miner_factory.MinerFactory.get_miner].
The function [`get_miner()`][pyasic.miners.miner_factory.MinerFactory.get_miner] will return any miner it found at the IP address specified, or an `UnknownMiner` if it cannot identify the miner.
```python
import asyncio# (1)!
from pyasic import get_miner# (2)!
import asyncio # asyncio for handling the async part
from pyasic import get_miner # handles miner creation
async def get_miners():# (3)!
miner_1 = await get_miner("192.168.1.75")# (4)!
async def get_miners(): # define async scan function to allow awaiting
# get the miner with the miner factory
# the miner factory is a singleton, and will always use the same object and cache
# this means you can always call it as MinerFactory().get_miner(), or just get_miner()
miner_1 = await get_miner("192.168.1.75")
miner_2 = await get_miner("192.168.1.76")
print(miner_1, miner_2)
# can also gather these, since they are async
tasks = [get_miner("192.168.1.75"), get_miner("192.168.1.76")]
miners = await asyncio.gather(*tasks)# (5)!
miners = await asyncio.gather(*tasks)
print(miners)
if __name__ == "__main__":
asyncio.run(get_miners())# (6)!
asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run()
```
1. `asyncio` for handling the async part.
2. `get_miner` handles the miner type selection.
3. Define an async function to allow awaiting.
4. Get the miner.
5. Can also gather these, since they are async.
Gathering them will get them both at the same time.
This makes it much faster to get a lot of miners at a time.
6. Get the miners asynchronously with asyncio.run().
<br>
## Data gathering
---
Once you have your miner(s) identified, you will likely want to get data from the miner(s). You can do this using a built-in function in each miner called `get_data()`.
## Getting data from miners
Once you have your miner(s) identified, you will likely want to get data from the miner(s). You can do this using a built in function in each miner called `get_data()`.
This function will return an instance of the dataclass [`MinerData`][pyasic.data.MinerData] with all data it can gather from the miner.
Each piece of data in a [`MinerData`][pyasic.data.MinerData] instance can be referenced by getting it as an attribute, such as [`MinerData().hashrate`][pyasic.data.MinerData].
### One miner
```python
import asyncio# (1)!
from pyasic import get_miner# (2)!
import asyncio
from pyasic.miners.miner_factory import MinerFactory
async def gather_miner_data():# (3)!
miner = await get_miner("192.168.1.75")# (4)!
if miner is not None:# (5)!
miner_data = await miner.get_data()# (6)!
print(miner_data)# (7)!
print(miner_data.hashrate) # hashrate of the miner in TH/s
async def gather_miner_data():
miner = await MinerFactory().get_miner("192.168.1.75")
miner_data = await miner.get_data()
print(miner_data) # all data from the dataclass
print(miner_data.hashrate) # hashrate of the miner in TH/s
if __name__ == "__main__":
asyncio.run(gather_miner_data())# (9)!
asyncio.run(gather_miner_data())
```
1. `asyncio` for handling the async part.
2. `get_miner` handles the miner type selection.
3. Define an async function to allow awaiting.
4. Get the miner.
5. Make sure the miner exists.
If this result is `None`, the miner may be offline.
6. Get data from the miner.
7. All the data from the dataclass.
8. Hashrate of the miner, with unit information.
9. Get the miner data asynchronously with asyncio.run().
### Multiple miners
You can do something similar with multiple miners, with only needing to make a small change to get all the data at once.
```python
import asyncio# (1)!
from pyasic.network import MinerNetwork# (2)!
import asyncio # asyncio for handling the async part
from pyasic.network import MinerNetwork # miner network handles the scanning
async def gather_miner_data():# (3)!
network = MinerNetwork.from_subnet("192.168.1.50/24")# (4)!
miners = await network.scan()# (5)!
async def gather_miner_data(): # define async scan function to allow awaiting
network = MinerNetwork("192.168.1.50")
miners = await network.scan_network_for_miners()
# we need to asyncio.gather() all the miners get_data() functions to make them run together
all_miner_data = await asyncio.gather(*[miner.get_data() for miner in miners])
for miner_data in all_miner_data:
print(miner_data)# (7)!
print(miner_data) # print out all the data one by one
if __name__ == "__main__":
asyncio.run(gather_miner_data())# (8)!
asyncio.run(gather_miner_data())
```
1. `asyncio` for handling the async part.
2. `MinerNetwork` handles the scanning.
3. Define an async function to allow awaiting.
4. Create a miner network.
5. Scan for miners asynchronously.
6. Use `asyncio.gather()` with all the miners' `get_data()` functions to make them run together.
7. Print out the data one at a time.
8. Get the miner data asynchronously with asyncio.run().
<br>
## Miner control
---
`pyasic` exposes a standard interface for each miner using control functions.
Every miner class in `pyasic` must implement all the following control functions.
## Controlling miners via pyasic
Every miner class in pyasic must implement all the control functions defined in [`BaseMiner`][pyasic.miners.BaseMiner].
[`check_light`][pyasic.miners.base.MinerProtocol.check_light],
[`fault_light_off`][pyasic.miners.base.MinerProtocol.fault_light_off],
[`fault_light_on`][pyasic.miners.base.MinerProtocol.fault_light_on],
[`get_config`][pyasic.miners.base.MinerProtocol.get_config],
[`get_data`][pyasic.miners.base.MinerProtocol.get_data],
[`get_errors`][pyasic.miners.base.MinerProtocol.get_errors],
[`get_hostname`][pyasic.miners.base.MinerProtocol.get_hostname],
[`get_model`][pyasic.miners.base.MinerProtocol.get_model],
[`reboot`][pyasic.miners.base.MinerProtocol.reboot],
[`restart_backend`][pyasic.miners.base.MinerProtocol.restart_backend],
[`stop_mining`][pyasic.miners.base.MinerProtocol.stop_mining],
[`resume_mining`][pyasic.miners.base.MinerProtocol.resume_mining],
[`is_mining`][pyasic.miners.base.MinerProtocol.is_mining],
[`send_config`][pyasic.miners.base.MinerProtocol.send_config], and
[`set_power_limit`][pyasic.miners.base.MinerProtocol.set_power_limit].
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).
### Usage
```python
import asyncio# (1)!
from pyasic import get_miner# (2)!
<br>
### Check Light
::: pyasic.miners.BaseMiner.check_light
handler: python
options:
heading_level: 4
async def set_fault_light():# (3)!
miner = await get_miner("192.168.1.20")# (4)!
<br>
await miner.fault_light_on()# (5)!
### Fault Light Off
::: pyasic.miners.BaseMiner.fault_light_off
handler: python
options:
heading_level: 4
if __name__ == "__main__":
asyncio.run(set_fault_light())# (6)!
```
<br>
1. `asyncio` for handling the async part.
2. `get_miner` handles the miner type selection.
3. Define an async function to allow awaiting.
4. Get the miner.
5. Call the miner control function.
6. Call the control function asynchronously with asyncio.run().
### Fault Light On
::: pyasic.miners.BaseMiner.fault_light_on
handler: python
options:
heading_level: 4
<br>
## Helper dataclasses
---
### Get Config
::: pyasic.miners.BaseMiner.get_config
handler: python
options:
heading_level: 4
### [`MinerConfig`][pyasic.config.MinerConfig] and [`MinerData`][pyasic.data.MinerData]
<br>
`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()`.
### 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()`][pyasic.miners.base.MinerProtocol.get_data] function, and is used to have a consistent dataset across all returns.
[`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.as_dict()`][pyasic.data.MinerData.as_dict] to get the dataclass as a dictionary, and there are many other helper functions contained in the class to convert to different data formats.
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
@@ -221,69 +268,13 @@ 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 designed to unionize the configuration of all supported miner types, and is the return from [`get_config()`][pyasic.miners.base.MinerProtocol.get_config].
[`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()`][pyasic.miners.base.MinerProtocol.send_config] takes a [`MinerConfig`][pyasic.config.MinerConfig] and will do the conversion to the right type for you.
You can use the [`MinerConfig`][pyasic.config.MinerConfig] as follows:
```python
import asyncio
from pyasic import get_miner
async def set_fault_light():
miner = await get_miner("192.168.1.20")
# get config
cfg = await miner.get_config()
# send config
await miner.send_config(cfg)
if __name__ == "__main__":
asyncio.run(set_fault_light())
```
## Settings
---
`pyasic` has settings designed to make using large groups of miners easier. You can set the default password for all types of miners using the `pyasic.settings` module, used as follows:
```python
from pyasic import settings
settings.update("default_antminer_web_password", "my_pwd")
```
### Default values:
```
"network_ping_retries": 1,
"network_ping_timeout": 3,
"network_scan_semaphore": None,
"factory_get_retries": 1,
"factory_get_timeout": 3,
"get_data_retries": 1,
"api_function_timeout": 5,
"antminer_mining_mode_as_str": False,
"default_whatsminer_rpc_password": "admin",
"default_innosilicon_web_password": "admin",
"default_antminer_web_password": "root",
"default_bosminer_web_password": "root",
"default_vnish_web_password": "admin",
"default_goldshell_web_password": "123456789",
"default_auradine_web_password": "admin",
"default_epic_web_password": "letmein",
"default_hive_web_password": "admin",
"default_antminer_ssh_password": "miner",
"default_bosminer_ssh_password": "root",
# ADVANCED
# Only use this if you know what you are doing
"socket_linger_time": 1000,
```
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

@@ -1,29 +1,11 @@
# pyasic
## X15 Models
## Z15 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
## Z15
::: pyasic.miners.antminer.cgminer.X15.Z15.CGMinerZ15
::: pyasic.miners.zec.antminer.cgminer.X15.Z15.CGMinerZ15
handler: python
options:
show_root_heading: false
heading_level: 0
## Z15 Pro (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X15.Z15.BMMinerZ15Pro
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,211 +1,117 @@
# pyasic
## X17 Models
## S17 (Stock)
## S17
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17
::: pyasic.miners.btc.antminer.bmminer.X17.S17.BMMinerS17
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## S17 Pro (Stock)
## S17+
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17Pro
::: pyasic.miners.btc.antminer.bmminer.X17.S17_Plus.BMMinerS17Plus
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## S17+ (Stock)
## S17 Pro
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17Plus
::: pyasic.miners.btc.antminer.bmminer.X17.S17_Pro.BMMinerS17Pro
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## S17e (Stock)
## S17e
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17e
::: pyasic.miners.btc.antminer.bmminer.X17.S17e.BMMinerS17e
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## T17 (Stock)
## T17
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17
::: pyasic.miners.btc.antminer.bmminer.X17.T17.BMMinerT17
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## T17+ (Stock)
## T17+
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17Plus
::: pyasic.miners.btc.antminer.bmminer.X17.T17_Plus.BMMinerT17Plus
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## T17e (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
## T17e
::: pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17e
::: pyasic.miners.btc.antminer.bmminer.X17.T17e.BMMinerT17e
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## S17 (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
## S17 (BOS)
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17
::: pyasic.miners.btc.antminer.bosminer.X17.S17.BOSMinerS17
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## S17 Pro (BOS+)
## S17+ (BOS)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17Pro
::: pyasic.miners.btc.antminer.bosminer.X17.S17_Plus.BOSMinerS17Plus
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## S17+ (BOS+)
## S17 Pro (BOS)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17Plus
::: pyasic.miners.btc.antminer.bosminer.X17.S17_Pro.BOSMinerS17Pro
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## S17e (BOS+)
## S17e (BOS)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17e
::: pyasic.miners.btc.antminer.bosminer.X17.S17e.BOSMinerS17e
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## T17 (BOS+)
## T17 (BOS)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17
::: pyasic.miners.btc.antminer.bosminer.X17.T17.BOSMinerT17
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## T17+ (BOS+)
## T17+ (BOS)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17Plus
::: pyasic.miners.btc.antminer.bosminer.X17.T17_Plus.BOSMinerT17Plus
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## T17e (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
## T17e (BOS)
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17e
::: pyasic.miners.btc.antminer.bosminer.X17.T17e.BOSMinerT17e
handler: python
options:
show_root_heading: false
heading_level: 0
## S17 Pro (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X17.S17.VNishS17Pro
handler: python
options:
show_root_heading: false
heading_level: 0
## S17+ (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X17.S17.VNishS17Plus
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

File diff suppressed because it is too large Load Diff

View File

@@ -1,328 +0,0 @@
# pyasic
## X21 Models
## S21 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X21.S21.BMMinerS21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 Hydro (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X21.S21.BMMinerS21Hydro
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 Pro (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X21.S21.BMMinerS21Pro
handler: python
options:
show_root_heading: false
heading_level: 0
## S21+ (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X21.S21.BMMinerS21Plus
handler: python
options:
show_root_heading: false
heading_level: 0
## S21+ Hydro (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X21.S21.BMMinerS21PlusHydro
handler: python
options:
show_root_heading: false
heading_level: 0
## T21 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X21.T21.BMMinerT21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X21.S21.BOSMinerS21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 Hydro (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X21.S21.BOSMinerS21Hydro
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 Pro (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X21.S21.BOSMinerS21Pro
handler: python
options:
show_root_heading: false
heading_level: 0
## S21+ (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X21.S21.BOSMinerS21Plus
handler: python
options:
show_root_heading: false
heading_level: 0
## S21+ Hydro (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X21.S21.BOSMinerS21PlusHydro
handler: python
options:
show_root_heading: false
heading_level: 0
## T21 (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X21.T21.BOSMinerT21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 Hydro (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21Hydro
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 Pro (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21Pro
handler: python
options:
show_root_heading: false
heading_level: 0
## S21+ (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21Plus
handler: python
options:
show_root_heading: false
heading_level: 0
## S21+ Hydro (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21PlusHydro
handler: python
options:
show_root_heading: false
heading_level: 0
## T21 (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.T21.VNishT21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 (ePIC)
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.epic.X21.S21.ePICS21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 Pro (ePIC)
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.epic.X21.S21.ePICS21Pro
handler: python
options:
show_root_heading: false
heading_level: 0
## T21 (ePIC)
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.epic.X21.T21.ePICT21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 (LuxOS)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.luxos.X21.S21.LUXMinerS21
handler: python
options:
show_root_heading: false
heading_level: 0
## T21 (LuxOS)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.luxos.X21.T21.LUXMinerT21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 (MaraFW)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.marathon.X21.S21.MaraS21
handler: python
options:
show_root_heading: false
heading_level: 0
## T21 (MaraFW)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.marathon.X21.T21.MaraT21
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,81 +1,11 @@
# pyasic
## X3 Models
## D3 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
## HS3
::: pyasic.miners.antminer.cgminer.X3.D3.CGMinerD3
::: pyasic.miners.hns.antminer.cgminer.X3.HS3.CGMinerHS3
handler: python
options:
show_root_heading: false
heading_level: 0
## HS3 (Stock)
- [ ] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X3.HS3.BMMinerHS3
handler: python
options:
show_root_heading: false
heading_level: 0
## KA3 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X3.KA3.BMMinerKA3
handler: python
options:
show_root_heading: false
heading_level: 0
## KS3 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X3.KS3.BMMinerKS3
handler: python
options:
show_root_heading: false
heading_level: 0
## L3+ (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X3.L3.BMMinerL3Plus
handler: python
options:
show_root_heading: false
heading_level: 0
## L3+ (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X3.L3.VNishL3Plus
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,42 +1,11 @@
# pyasic
## X5 Models
## DR5 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
## DR5
::: pyasic.miners.antminer.cgminer.X5.DR5.CGMinerDR5
::: pyasic.miners.dcr.antminer.cgminer.X5.DR5.CGMinerDR5
handler: python
options:
show_root_heading: false
heading_level: 0
## KS5 (Stock)
- [ ] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X5.KS5.BMMinerKS5
handler: python
options:
show_root_heading: false
heading_level: 0
## KS5 (Stock)
- [ ] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X5.KS5.BMMinerKS5Pro
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,55 +1,11 @@
# pyasic
## X7 Models
## D7 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
## L7
::: pyasic.miners.antminer.bmminer.X7.D7.BMMinerD7
::: pyasic.miners.ltc.antminer.bmminer.X7.L7.BMMinerL7
handler: python
options:
show_root_heading: false
heading_level: 0
## K7 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X7.K7.BMMinerK7
handler: python
options:
show_root_heading: false
heading_level: 0
## L7 (Stock)
- [ ] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X7.L7.BMMinerL7
handler: python
options:
show_root_heading: false
heading_level: 0
## L7 (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X7.L7.VNishL7
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,146 +1,44 @@
# pyasic
## X9 Models
## D9 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
## X9 (BOS)
::: pyasic.miners.antminer.bmminer.X9.D9.BMMinerD9
::: pyasic.miners.btc.antminer.bosminer.X9.S9.BOSMinerS9
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## E9Pro (Stock)
- [ ] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
## S9
::: pyasic.miners.antminer.bmminer.X9.E9.BMMinerE9Pro
::: pyasic.miners.btc.antminer.bmminer.X9.S9.BMMinerS9
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## L9 (Stock)
## S9i
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X9.L9.BMMinerL9
::: pyasic.miners.btc.antminer.bmminer.X9.S9i.BMMinerS9i
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## S9 (Stock)
## T9
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9
::: pyasic.miners.btc.antminer.bmminer.X9.T9.BMMinerT9
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## S9i (Stock)
## E9 Pro
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9i
::: pyasic.miners.etc.antminer.cgminer.X9.E9_Pro.CGMinerE9Pro
handler: python
options:
show_root_heading: false
heading_level: 0
## S9j (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9j
handler: python
options:
show_root_heading: false
heading_level: 0
## T9 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X9.T9.BMMinerT9
handler: python
options:
show_root_heading: false
heading_level: 0
## S9 (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9
handler: python
options:
show_root_heading: false
heading_level: 0
## L9 (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X9.L9.VNishL9
handler: python
options:
show_root_heading: false
heading_level: 0
## T9 (Hive)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.hiveon.X9.T9.HiveonT9
handler: python
options:
show_root_heading: false
heading_level: 0
## S9 (LuxOS)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.luxos.X9.S9.LUXMinerS9
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,42 +0,0 @@
# pyasic
## AD Models
## AT1500 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.auradine.flux.AD.AT1.AuradineFluxAT1500
handler: python
options:
show_root_heading: false
heading_level: 0
## AT2860 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.auradine.flux.AD.AT2.AuradineFluxAT2860
handler: python
options:
show_root_heading: false
heading_level: 0
## AT2880 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.auradine.flux.AD.AT2.AuradineFluxAT2880
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,29 +0,0 @@
# pyasic
## AI Models
## AI2500 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.auradine.flux.AI.AI2.AuradineFluxAI2500
handler: python
options:
show_root_heading: false
heading_level: 0
## AI3680 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.auradine.flux.AI.AI3.AuradineFluxAI3680
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,29 +0,0 @@
# pyasic
## AT Models
## AD2500 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.auradine.flux.AT.AD2.AuradineFluxAD2500
handler: python
options:
show_root_heading: false
heading_level: 0
## AD3500 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.auradine.flux.AT.AD3.AuradineFluxAD3500
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,42 +1,26 @@
# pyasic
## A10X Models
## Avalon 1026 (Stock)
## A1026
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026
::: pyasic.miners.btc.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## Avalon 1047 (Stock)
## A1047
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047
::: pyasic.miners.btc.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## Avalon 1066 (Stock)
## A1066
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066
::: pyasic.miners.btc.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,29 +0,0 @@
# pyasic
## A11X Models
## Avalon 1126 Pro (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A11X.A1126.CGMinerAvalon1126Pro
handler: python
options:
show_root_heading: false
heading_level: 0
## Avalon 1166 Pro (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A11X.A1166.CGMinerAvalon1166Pro
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,16 +0,0 @@
# pyasic
## A12X Models
## Avalon 1246 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A12X.A1246.CGMinerAvalon1246
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,16 +0,0 @@
# pyasic
## A15X Models
## Avalon 1566 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A15X.A1566.CGMinerAvalon1566
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,42 +1,26 @@
# pyasic
## A7X Models
## Avalon 721 (Stock)
## A721
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A7X.A721.CGMinerAvalon721
::: pyasic.miners.btc.avalonminer.cgminer.A7X.A721.CGMinerAvalon721
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## Avalon 741 (Stock)
## A741
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A7X.A741.CGMinerAvalon741
::: pyasic.miners.btc.avalonminer.cgminer.A7X.A741.CGMinerAvalon741
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## Avalon 761 (Stock)
## A761
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A7X.A761.CGMinerAvalon761
::: pyasic.miners.btc.avalonminer.cgminer.A7X.A761.CGMinerAvalon761
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,42 +1,26 @@
# pyasic
## A8X Models
## Avalon 821 (Stock)
## A821
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A8X.A821.CGMinerAvalon821
::: pyasic.miners.btc.avalonminer.cgminer.A8X.A821.CGMinerAvalon821
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## Avalon 841 (Stock)
## A841
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A8X.A841.CGMinerAvalon841
::: pyasic.miners.btc.avalonminer.cgminer.A8X.A841.CGMinerAvalon841
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4
## Avalon 851 (Stock)
## A851
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A8X.A851.CGMinerAvalon851
::: pyasic.miners.btc.avalonminer.cgminer.A8X.A851.CGMinerAvalon851
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,16 +1,10 @@
# pyasic
## A9X Models
## Avalon 921 (Stock)
## A921
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.A9X.A921.CGMinerAvalon921
::: pyasic.miners.btc.avalonminer.cgminer.A9X.A921.CGMinerAvalon921
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,29 +0,0 @@
# pyasic
## nano Models
## Avalon Nano 3s (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.nano.nano3.CGMinerAvalonNano3s
handler: python
options:
show_root_heading: false
heading_level: 0
## Avalon Nano 3 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.nano.nano3.CGMinerAvalonNano3
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,15 +1,7 @@
# pyasic
## BOSMiner Backend
::: pyasic.miners.backends.braiins_os.BOSMiner
handler: python
options:
show_root_heading: false
heading_level: 4
## BOSer Backend
::: pyasic.miners.backends.braiins_os.BOSer
::: pyasic.miners.backends.bosminer.BOSMiner
handler: python
options:
show_root_heading: false

View File

@@ -1,14 +1,7 @@
# pyasic
## Modern Hiveon Backend
## Hiveon Backend
::: pyasic.miners.backends.hiveon.HiveonModern
handler: python
options:
show_root_heading: false
heading_level: 4
## Old Hiveon Backend
::: pyasic.miners.backends.hiveon.HiveonOld
::: pyasic.miners.backends.hiveon.Hiveon
handler: python
options:
show_root_heading: false

View File

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

View File

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

View File

@@ -1,55 +0,0 @@
# pyasic
## BM Models
## Ultra (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.bitaxe.espminer.BM.BM1366.BitAxeUltra
handler: python
options:
show_root_heading: false
heading_level: 0
## Supra (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.bitaxe.espminer.BM.BM1368.BitAxeSupra
handler: python
options:
show_root_heading: false
heading_level: 0
## Gamma (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.bitaxe.espminer.BM.BM1370.BitAxeGamma
handler: python
options:
show_root_heading: false
heading_level: 0
## Max (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.bitaxe.espminer.BM.BM1397.BitAxeMax
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,42 +0,0 @@
# pyasic
## blockminer Models
## BlockMiner 520i (ePIC)
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.blockminer.epic.blockminer.blockminer.ePICBlockMiner520i
handler: python
options:
show_root_heading: false
heading_level: 0
## BlockMiner 720i (ePIC)
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.blockminer.epic.blockminer.blockminer.ePICBlockMiner720i
handler: python
options:
show_root_heading: false
heading_level: 0
## BlockMiner eLITE 1.0 (ePIC)
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.blockminer.epic.blockminer.blockminer.ePICBlockMinerELITE1
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,29 +0,0 @@
# pyasic
## BMM Models
## BMM100 (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.braiins.braiins.BMM.BMM.BraiinsBMM100
handler: python
options:
show_root_heading: false
heading_level: 0
## BMM101 (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.braiins.braiins.BMM.BMM.BraiinsBMM101
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,42 +0,0 @@
# pyasic
## DGX Models
## DG1 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.elphapex.daoge.DGX.DG1.ElphapexDG1
handler: python
options:
show_root_heading: false
heading_level: 0
## DG1+ (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.elphapex.daoge.DGX.DG1.ElphapexDG1Plus
handler: python
options:
show_root_heading: false
heading_level: 0
## DG1Home (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.elphapex.daoge.DGX.DG1.ElphapexDG1Home
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,10 +0,0 @@
## Control functionality
All control functionality is outlined by the [`MinerProtocol`][pyasic.miners.base.MinerProtocol] class.
## Miner Protocol
::: pyasic.miners.base.MinerProtocol
handler: python
options:
heading_level: 4

View File

@@ -1,16 +0,0 @@
# pyasic
## Byte Models
## Byte (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.Byte.Byte.GoldshellByte
handler: python
options:
show_root_heading: false
heading_level: 0

View File

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

View File

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

View File

@@ -1,13 +1,18 @@
# pyasic
## AntminerModernWebAPI
::: pyasic.web.antminer.AntminerModernWebAPI
## KDX Models
## KD5
::: pyasic.miners.kda.goldshell.bfgminer.KDX.KD5.BFGMinerKD5
handler: python
options:
show_root_heading: false
heading_level: 4
## AntminerOldWebAPI
::: pyasic.web.antminer.AntminerOldWebAPI
## KD Max
::: pyasic.miners.kda.goldshell.bfgminer.KDX.KDMax.BFGMinerKDMax
handler: python
options:
show_root_heading: false

View File

@@ -1,42 +0,0 @@
# pyasic
## X5 Models
## CK5 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.X5.CK5.GoldshellCK5
handler: python
options:
show_root_heading: false
heading_level: 0
## HS5 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.X5.HS5.GoldshellHS5
handler: python
options:
show_root_heading: false
heading_level: 0
## KD5 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.X5.KD5.GoldshellKD5
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,29 +0,0 @@
# pyasic
## XBox Models
## KD Box II (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.XBox.KDBox.GoldshellKDBoxII
handler: python
options:
show_root_heading: false
heading_level: 0
## KD Box Pro (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.XBox.KDBox.GoldshellKDBoxPro
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,16 +0,0 @@
# pyasic
## XMax Models
## KD Max (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.XMax.KDMax.GoldshellKDMax
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,16 +0,0 @@
# pyasic
## DX Models
## D10 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.hammer.blackminer.DX.D10.HammerD10
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,120 +0,0 @@
# pyasic
## KSX Models
## KS0 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.KSX.KS0.IceRiverKS0
handler: python
options:
show_root_heading: false
heading_level: 0
## KS1 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.KSX.KS1.IceRiverKS1
handler: python
options:
show_root_heading: false
heading_level: 0
## KS2 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.KSX.KS2.IceRiverKS2
handler: python
options:
show_root_heading: false
heading_level: 0
## KS3 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.KSX.KS3.IceRiverKS3
handler: python
options:
show_root_heading: false
heading_level: 0
## KS3L (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.KSX.KS3.IceRiverKS3L
handler: python
options:
show_root_heading: false
heading_level: 0
## KS3M (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.KSX.KS3.IceRiverKS3M
handler: python
options:
show_root_heading: false
heading_level: 0
## KS5 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.KSX.KS5.IceRiverKS5
handler: python
options:
show_root_heading: false
heading_level: 0
## KS5L (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.KSX.KS5.IceRiverKS5L
handler: python
options:
show_root_heading: false
heading_level: 0
## KS5M (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.KSX.KS5.IceRiverKS5M
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,16 +1,10 @@
# pyasic
## A10X Models
## A10X (Stock)
## A10X
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.innosilicon.cgminer.A10X.A10X.InnosiliconA10X
::: pyasic.miners.etc.innosilicon.cgminer.A10X.A10X.CGMinerA10X
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,29 +0,0 @@
# pyasic
## A11X Models
## A11 (Stock)
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.innosilicon.cgminer.A11X.A11.InnosiliconA11
handler: python
options:
show_root_heading: false
heading_level: 0
## A11MX (Stock)
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.innosilicon.cgminer.A11X.A11M.InnosiliconA11MX
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,16 +1,10 @@
# pyasic
## T3X Models
## T3H+ (Stock)
## T3H+
- [x] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.innosilicon.cgminer.T3X.T3H.InnosiliconT3HPlus
::: pyasic.miners.btc.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus
handler: python
options:
show_root_heading: false
heading_level: 0
heading_level: 4

View File

@@ -1,29 +0,0 @@
# pyasic
## LV Models
## LV07 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.luckyminer.espminer.LV.LV07.LuckyMinerLV07
handler: python
options:
show_root_heading: false
heading_level: 0
## LV08 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.luckyminer.espminer.LV.LV08.LuckyMinerLV08
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,23 +1,7 @@
# pyasic
## Miner Factory
[`MinerFactory`][pyasic.miners.factory.MinerFactory] is the way to create miner types in `pyasic`. The most important method is [`get_miner()`][pyasic.get_miner], which is mapped to [`pyasic.get_miner()`][pyasic.get_miner], and should be used from there.
The instance used for [`pyasic.get_miner()`][pyasic.get_miner] is `pyasic.miner_factory`.
[`MinerFactory`][pyasic.miners.factory.MinerFactory] also keeps a cache, which can be cleared if needed with `pyasic.miner_factory.clear_cached_miners()`.
Finally, there is functionality to get multiple miners without using `asyncio.gather()` explicitly. Use `pyasic.miner_factory.get_multiple_miners()` with a list of IPs as strings to get a list of miner instances. You can also get multiple miners with an `AsyncGenerator` by using `pyasic.miner_factory.get_miner_generator()`.
::: pyasic.miners.factory.MinerFactory
handler: python
options:
show_root_heading: false
heading_level: 4
<br>
## Get Miner
::: pyasic.miners.get_miner
::: pyasic.miners.miner_factory.MinerFactory
handler: python
options:
show_root_heading: false
@@ -25,12 +9,12 @@ Finally, there is functionality to get multiple miners without using `asyncio.ga
<br>
## AnyMiner
::: pyasic.miners.base.AnyMiner
::: pyasic.miners.miner_factory.AnyMiner
handler: python
options:
show_root_heading: false
heading_level: 4
[`AnyMiner`][pyasic.miners.base.AnyMiner] is a placeholder type variable used for typing returns of functions.
A function returning [`AnyMiner`][pyasic.miners.base.AnyMiner] will always return a subclass of [`BaseMiner`][pyasic.miners.base.BaseMiner],
[`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] is a placeholder type variable used for typing returns of functions.
A function returning [`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] will always return a subclass of [`BaseMiner`][pyasic.miners.BaseMiner],
and is used to specify a function returning some arbitrary type of miner class instance.

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
# pyasic
## DX Models
## D1 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.volcminer.blackminer.DX.D1.VolcMinerD1
handler: python
options:
show_root_heading: false
heading_level: 0

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
# pyasic
## M7X Models
## M70 VM30 (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.whatsminer.btminer.M7X.M70.BTMinerM70VM30
handler: python
options:
show_root_heading: false
heading_level: 0

View File

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

3
docs/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
jinja2<3.1.0
mkdocs
mkdocstrings[python]

View File

@@ -1,27 +0,0 @@
# pyasic
## Miner RPC APIs
Each miner has a unique RPC API that is used to communicate with it.
Each of these API types has commands that differ between them, and some commands have data that others do not.
Each miner that is a subclass of [`BaseMiner`][pyasic.miners.base.BaseMiner] may have an API linked to it as `Miner.rpc`.
All RPC API implementations inherit from [`BaseMinerRPCAPI`][pyasic.rpc.base.BaseMinerRPCAPI], which implements the basic communications protocols.
[`BaseMinerRPCAPI`][pyasic.rpc.base.BaseMinerRPCAPI] should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
[`BaseMinerRPCAPI`][pyasic.rpc.base.BaseMinerRPCAPI] cannot be instantiated directly, it will raise a `TypeError`.
Use these instead -
#### [BFGMiner API][pyasic.rpc.bfgminer.BFGMinerRPCAPI]
#### [BMMiner API][pyasic.rpc.bmminer.BMMinerRPCAPI]
#### [BOSMiner API][pyasic.rpc.bosminer.BOSMinerRPCAPI]
#### [BTMiner API][pyasic.rpc.btminer.BTMinerRPCAPI]
#### [CGMiner API][pyasic.rpc.cgminer.CGMinerRPCAPI]
#### [LUXMiner API][pyasic.rpc.luxminer.LUXMinerRPCAPI]
#### [Unknown API][pyasic.rpc.unknown.UnknownRPCAPI]
<br>
## BaseMinerRPCAPI
::: pyasic.rpc.base.BaseMinerRPCAPI
handler: python
options:
heading_level: 4

View File

@@ -1,7 +0,0 @@
# pyasic
## BFGMinerRPCAPI
::: pyasic.rpc.bfgminer.BFGMinerRPCAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,7 +0,0 @@
# pyasic
## BOSMinerRPCAPI
::: pyasic.rpc.bosminer.BOSMinerRPCAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,7 +0,0 @@
# pyasic
## BTMinerRPCAPI
::: pyasic.rpc.btminer.BTMinerRPCAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,7 +0,0 @@
# pyasic
## CGMinerRPCAPI
::: pyasic.rpc.cgminer.CGMinerRPCAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

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

View File

@@ -1,41 +0,0 @@
# pyasic
## settings
All settings here are global settings for all of pyasic. Set these settings with `update(key, value)`.
Settings options:
- `network_ping_retries`
- `network_ping_timeout`
- `network_scan_threads`
- `factory_get_retries`
- `factory_get_timeout`
- `get_data_retries`
- `api_function_timeout`
- `antminer_mining_mode_as_str`
- `default_whatsminer_rpc_password`
- `default_innosilicon_web_password`
- `default_antminer_web_password`
- `default_bosminer_web_password`
- `default_vnish_web_password`
- `default_goldshell_web_password`
- `default_auradine_web_password`
- `default_epic_web_password`
- `default_hive_web_password`
- `default_antminer_ssh_password`
- `default_bosminer_ssh_password`
### get
::: pyasic.settings.get
handler: python
options:
show_root_heading: false
heading_level: 4
### update
::: pyasic.settings.update
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,27 +0,0 @@
# pyasic
## Miner Web APIs
Each miner has a unique Web API that is used to communicate with it.
Each of these API types has commands that differ between them, and some commands have data that others do not.
Each miner that is a subclass of [`BaseMiner`][pyasic.miners.base.BaseMiner] may have an API linked to it as `Miner.web`.
All API implementations inherit from [`BaseWebAPI`][pyasic.web.BaseWebAPI], which implements the basic communications protocols.
[`BaseWebAPI`][pyasic.web.BaseWebAPI] should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
Use these instead -
#### [AntminerModerNWebAPI][pyasic.web.antminer.AntminerModernWebAPI]
#### [AntminerOldWebAPI][pyasic.web.antminer.AntminerOldWebAPI]
#### [AuradineWebAPI][pyasic.web.auradine.AuradineWebAPI]
#### [ePICWebAPI][pyasic.web.epic.ePICWebAPI]
#### [GoldshellWebAPI][pyasic.web.goldshell.GoldshellWebAPI]
#### [InnosiliconWebAPI][pyasic.web.innosilicon.InnosiliconWebAPI]
#### [MaraWebAPI][pyasic.web.marathon.MaraWebAPI]
#### [VNishWebAPI][pyasic.web.vnish.VNishWebAPI]
<br>
## BaseWebAPI
::: pyasic.web.BaseWebAPI
handler: python
options:
heading_level: 4

View File

@@ -1,7 +0,0 @@
# pyasic
## AuradineWebAPI
::: pyasic.web.auradine.AuradineWebAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,7 +0,0 @@
# pyasic
## GoldshellWebAPI
::: pyasic.web.goldshell.GoldshellWebAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,7 +0,0 @@
# pyasic
## InnosiliconWebAPI
::: pyasic.web.innosilicon.InnosiliconWebAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,76 +1,32 @@
site_name: pyasic
repo_url: https://github.com/UpstreamData/pyasic
site_url: !ENV SITE_URL
theme:
name: material
features:
- content.code.copy
- content.code.annotate
palette:
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/weather-night
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/weather-sunny
name: Switch to auto mode
markdown_extensions:
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
nav:
- Introduction: "index.md"
- Miners:
- Supported Miners: "miners/supported_types.md"
- Standard Functionality: "miners/functions.md"
- Miner Factory: "miners/miner_factory.md"
- Network:
- Miner Network: "network/miner_network.md"
- Miner Network Range: "network/miner_network_range.md"
- Dataclasses:
- Miner Data: "data/miner_data.md"
- Error Codes: "data/error_codes.md"
- Miner Config: "config/miner_config.md"
- Advanced:
- RPC APIs:
- Intro: "rpc/api.md"
- BFGMiner: "rpc/bfgminer.md"
- BMMiner: "rpc/bmminer.md"
- BOSMiner: "rpc/bosminer.md"
- BTMiner: "rpc/btminer.md"
- CGMiner: "rpc/cgminer.md"
- LUXMiner: "rpc/luxminer.md"
- Unknown: "rpc/unknown.md"
- Web APIs:
- Intro: "web/api.md"
- Antminer: "web/antminer.md"
- Auradine: "web/auradine.md"
- ePIC: "web/epic.md"
- Goldshell: "web/goldshell.md"
- Innosilicon: "web/innosilicon.md"
- Marathon: "web/marathon.md"
- VNish: "web/vnish.md"
- Miner APIs:
- Intro: "API/api.md"
- BFGMiner: "API/bfgminer.md"
- BMMiner: "API/bmminer.md"
- BOSMiner: "API/bosminer.md"
- BTMiner: "API/btminer.md"
- CGMiner: "API/cgminer.md"
- Unknown: "API/unknown.md"
- Backends:
- BMMiner: "miners/backends/bmminer.md"
- BOSMiner: "miners/backends/bosminer.md"
- BFGMiner: "miners/backends/bfgminer.md"
- BTMiner: "miners/backends/btminer.md"
- CGMiner: "miners/backends/cgminer.md"
- LUXMiner: "miners/backends/luxminer.md"
- VNish: "miners/backends/vnish.md"
- ePIC: "miners/backends/epic.md"
- Hiveon: "miners/backends/hiveon.md"
- Classes:
- Antminer X3: "miners/antminer/X3.md"
@@ -80,38 +36,20 @@ nav:
- Antminer X15: "miners/antminer/X15.md"
- Antminer X17: "miners/antminer/X17.md"
- Antminer X19: "miners/antminer/X19.md"
- Antminer X21: "miners/antminer/X21.md"
- Braiins Mini Miners: "miners/braiins/BMM.md"
- Avalon Nano: "miners/avalonminer/nano.md"
- Avalon 7X: "miners/avalonminer/A7X.md"
- Avalon 8X: "miners/avalonminer/A8X.md"
- Avalon 9X: "miners/avalonminer/A9X.md"
- Avalon 10X: "miners/avalonminer/A10X.md"
- Avalon 11X: "miners/avalonminer/A11X.md"
- Avalon 12X: "miners/avalonminer/A12X.md"
- Whatsminer M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md"
- Whatsminer M5X: "miners/whatsminer/M5X.md"
- Whatsminer M6X: "miners/whatsminer/M6X.md"
- Whatsminer M7X: "miners/whatsminer/M7X.md"
- Innosilicon T3X: "miners/innosilicon/T3X.md"
- Innosilicon A10X: "miners/innosilicon/A10X.md"
- Innosilicon A11X: "miners/innosilicon/A11X.md"
- Goldshell Byte: "miners/goldshell/Byte.md"
- Goldshell X5: "miners/goldshell/X5.md"
- Goldshell XMax: "miners/goldshell/XMax.md"
- Goldshell XBox: "miners/goldshell/XBox.md"
- Auradine AD: "miners/auradine/AD.md"
- Auradine AI: "miners/auradine/AI.md"
- Auradine AT: "miners/auradine/AT.md"
- Blockminer: "miners/blockminer/blockminer.md"
- BitAxe BM: "miners/bitaxe/BM.md"
- Hammer DX: "miners/hammer/DX.md"
- Iceriver KSX: "miners/iceriver/KSX.md"
- Volcminer DX: "miners/volcminer/DX.md"
- Goldshell CKX: "miners/goldshell/CKX.md"
- Goldshell HSX: "miners/goldshell/HSX.md"
- Goldshell KDX: "miners/goldshell/KDX.md"
- Base Miner: "miners/base_miner.md"
- Settings:
- Settings: "settings/settings.md"
plugins:
- mkdocstrings

1717
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,26 +23,21 @@ import warnings
from typing import Union
from pyasic.errors import APIError, APIWarning
from pyasic.misc import validate_command_output
class BaseMinerRPCAPI:
def __init__(self, ip: str, port: int = 4028, api_ver: str = "0.0.0") -> None:
class BaseMinerAPI:
def __init__(self, ip: str, port: int = 4028) -> None:
# api port, should be 4028
self.port = port
# ip address of the miner
self.ip = ipaddress.ip_address(ip)
# api version if known
self.api_ver = api_ver
self.pwd = None
def __new__(cls, *args, **kwargs):
if cls is BaseMinerRPCAPI:
if cls is BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls)
def __repr__(self) -> str:
def __repr__(self):
return f"{self.__class__.__name__}: {str(self.ip)}"
async def send_command(
@@ -78,26 +73,18 @@ class BaseMinerRPCAPI:
# send the command
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
if data is None:
raise APIError("No data returned from the API.")
if data == b"Socket connect failed: Connection refused\n":
if not ignore_errors:
raise APIError(data.decode("utf-8"))
return {}
data = self._load_api_data(data)
# check for if the user wants to allow errors to return
validation = validate_command_output(data)
if not validation[0]:
if not ignore_errors:
# validate the command succeeded
raise APIError(f"{command}: {validation[1]}")
if allow_warning:
logging.warning(
f"{self.ip}: API Command Error: {command}: {validation[1]}"
)
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
@@ -114,41 +101,25 @@ class BaseMinerRPCAPI:
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"
# doesn't work for S19 which uses the backup _send_split_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
data = await self._send_split_multicommand(*commands)
data["multicommand"] = True
return data
async def _send_split_multicommand(
self, *commands, allow_warning: bool = True
) -> dict:
tasks = {}
# send all commands individually
for cmd in commands:
tasks[cmd] = asyncio.create_task(
self.send_command(cmd, allow_warning=allow_warning)
)
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
data = {}
for cmd in tasks:
while True:
# 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:
result = tasks[cmd].result()
if result is None or result == {}:
result = {}
data[cmd] = [result]
except APIError:
pass
return data
data = await self.send_command(command, allow_warning=allow_warning)
except APIError as e:
# try to identify the error
if ":" in e.message:
err_command = e.message.split(":")[0]
if err_command in commands:
commands.remove(err_command)
continue
return {command: [{}] for command in commands}
logging.debug(f"{self} - (Multicommand) - Received data")
data["multicommand"] = True
return data
@property
def commands(self) -> list:
@@ -165,7 +136,7 @@ class BaseMinerRPCAPI:
for func in
# each function in self
dir(self)
if not func in ["commands", "open_api"]
if not func == "commands"
if callable(getattr(self, func)) and
# no __ or _ methods
not func.startswith("__") and not func.startswith("_") and
@@ -173,12 +144,12 @@ class BaseMinerRPCAPI:
func
not in [
func
for func in dir(BaseMinerRPCAPI)
if callable(getattr(BaseMinerRPCAPI, func))
for func in dir(BaseMinerAPI)
if callable(getattr(BaseMinerAPI, func))
]
]
def _check_commands(self, *commands) -> list:
def _check_commands(self, *commands):
allowed_commands = self.commands
return_commands = []
@@ -193,19 +164,11 @@ If you are sure you want to use this command please use API.send_command("{comma
)
return return_commands
async def _send_bytes(
self,
data: bytes,
*,
port: int = None,
timeout: int = 100,
) -> bytes:
if port is None:
port = self.port
async def _send_bytes(self, data: bytes, timeout: int = 100) -> bytes:
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), port)
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
# handle OSError 121
except OSError as e:
if e.errno == 121:
@@ -215,18 +178,37 @@ If you are sure you want to use this command please use API.send_command("{comma
return b"{}"
# send the command
logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing")
writer.write(data)
logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
await writer.drain()
try:
data_task = asyncio.create_task(self._read_bytes(reader, timeout=timeout))
logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing")
writer.write(data)
logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
await writer.drain()
await data_task
ret_data = data_task.result()
except TimeoutError:
logging.warning(f"{self} - ([Hidden] Send Bytes) - Read timeout expired.")
ret_data = await asyncio.wait_for(reader.read(4096), timeout=timeout)
except ConnectionAbortedError:
return b"{}"
try:
# Fix for stupid whatsminer bug, reboot/restart seem to not load properly in the loop
# have to receive, save the data, check if there is more data by reading with a short timeout
# append that data if there is more, and then onto the main loop.
ret_data += await asyncio.wait_for(reader.read(1), timeout=1)
except asyncio.TimeoutError:
return ret_data
# loop to receive all the data
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
try:
while True:
try:
d = await asyncio.wait_for(reader.read(4096), timeout=timeout)
if not d:
break
ret_data += d
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except Exception as e:
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
# close the connection
logging.debug(f"{self} - ([Hidden] Send Bytes) - Closing")
@@ -235,18 +217,32 @@ If you are sure you want to use this command please use API.send_command("{comma
return ret_data
async def _read_bytes(self, reader: asyncio.StreamReader, timeout: int) -> bytes:
ret_data = b""
# loop to receive all the data
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
try:
ret_data = await asyncio.wait_for(reader.read(), timeout=timeout)
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except Exception as e:
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
return ret_data
@staticmethod
def _validate_command_output(data: dict) -> tuple:
# check if the data returned is correct or an error
# if status isn't a key, it is a multicommand
if "STATUS" not in data.keys():
for key in data.keys():
# make sure not to try to turn id into a dict
if not key == "id":
# make sure they succeeded
if "STATUS" in data[key][0].keys():
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
# this is an error
return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"]
elif "id" not in data.keys():
if data["STATUS"] not in ["S", "I"]:
return False, data["Msg"]
else:
# make sure the command succeeded
if type(data["STATUS"]) == str:
if data["STATUS"] in ["RESTART"]:
return True, None
elif data["STATUS"][0]["STATUS"] not in ("S", "I"):
# this is an error
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
return False, data["STATUS"][0]["Msg"]
return True, None
@staticmethod
def _load_api_data(data: bytes) -> dict:
@@ -268,9 +264,10 @@ If you are sure you want to use this command please use API.send_command("{comma
# fix an error with a btminer return having a missing comma. (2023-01-06 version)
str_data = str_data.replace('""temp0', '","temp0')
# fix an error with Avalonminers returning inf and nan
str_data = str_data.replace('"inf"', "0")
str_data = str_data.replace('"nan"', "0")
str_data = str_data.replace("info", "1nfo")
str_data = str_data.replace("inf", "0")
str_data = str_data.replace("1nfo", "info")
str_data = str_data.replace("nan", "0")
# fix whatever this garbage from avalonminers is `,"id":1}`
if str_data.startswith(","):
str_data = f"{{{str_data[1:]}"

674
pyasic/API/bfgminer.py Normal file
View File

@@ -0,0 +1,674 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import logging
from pyasic.API import APIError, BaseMinerAPI
class BFGMinerAPI(BaseMinerAPI):
"""An abstraction of the BFGMiner API.
Each method corresponds to an API command in BFGMiner.
[BFGMiner API documentation](https://github.com/luke-jr/bfgminer/blob/bfgminer/README.RPC)
This class abstracts use of the BFGMiner API, as well as the
methods for sending commands to it. The self.send_command()
function handles sending a command to the miner asynchronously, and
as such is the base for many of the functions in this class, which
rely on it to send the command for them.
Parameters:
ip: The IP of the miner to reference the API on.
port: The port to reference the API on. Default is 4028.
"""
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028):
super().__init__(ip, port)
self.api_ver = api_ver
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesn't work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
data = await self._x19_multicommand(*command.split("+"))
data["multicommand"] = True
return data
async def _x19_multicommand(self, *commands) -> dict:
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
)
return data
async def version(self) -> dict:
"""Get miner version info.
<details>
<summary>Expand</summary>
Returns:
Miner version information.
</details>
"""
return await self.send_command("version")
async def config(self) -> dict:
"""Get some basic configuration info.
<details>
<summary>Expand</summary>
Returns:
## Some miner configuration information:
* ASC Count <- the number of ASCs
* PGA Count <- the number of PGAs
* Pool Count <- the number of Pools
* Strategy <- the current pool strategy
* Log Interval <- the interval of logging
* Device Code <- list of compiled device drivers
* OS <- the current operating system
* Failover-Only <- failover-only setting
* Scan Time <- scan-time setting
* Queue <- queue setting
* Expiry <- expiry setting
</details>
"""
return await self.send_command("config")
async def summary(self) -> dict:
"""Get the status summary of the miner.
<details>
<summary>Expand</summary>
Returns:
The status summary of the miner.
</details>
"""
return await self.send_command("summary")
async def pools(self) -> dict:
"""Get pool information.
<details>
<summary>Expand</summary>
Returns:
Miner pool information.
</details>
"""
return await self.send_command("pools")
async def devs(self) -> dict:
"""Get data on each PGA/ASC with their details.
<details>
<summary>Expand</summary>
Returns:
Data on each PGA/ASC with their details.
</details>
"""
return await self.send_command("devs")
async def procs(self) -> dict:
"""Get data on each processor with their details.
<details>
<summary>Expand</summary>
Returns:
Data on each processor with their details.
</details>
"""
return await self.send_command("procs")
async def devscan(self, info: str = "") -> dict:
"""Get data on each processor with their details.
<details>
<summary>Expand</summary>
Parameters:
info: Info to scan for device by.
Returns:
Data on each processor with their details.
</details>
"""
return await self.send_command("devscan", parameters=info)
async def pga(self, n: int) -> dict:
"""Get data from PGA n.
<details>
<summary>Expand</summary>
Parameters:
n: The PGA number to get data from.
Returns:
Data on the PGA n.
</details>
"""
return await self.send_command("pga", parameters=n)
async def proc(self, n: int = 0) -> dict:
"""Get data processor n.
<details>
<summary>Expand</summary>
Parameters:
n: The processor to get data on.
Returns:
Data on processor n.
</details>
"""
return await self.send_command("proc", parameters=n)
async def pgacount(self) -> dict:
"""Get data fon all PGAs.
<details>
<summary>Expand</summary>
Returns:
Data on the PGAs connected.
</details>
"""
return await self.send_command("pgacount")
async def proccount(self) -> dict:
"""Get data fon all processors.
<details>
<summary>Expand</summary>
Returns:
Data on the processors connected.
</details>
"""
return await self.send_command("proccount")
async def switchpool(self, n: int) -> dict:
"""Switch pools to pool n.
<details>
<summary>Expand</summary>
Parameters:
n: The pool to switch to.
Returns:
A confirmation of switching to pool n.
</details>
"""
return await self.send_command("switchpool", parameters=n)
async def enablepool(self, n: int) -> dict:
"""Enable pool n.
<details>
<summary>Expand</summary>
Parameters:
n: The pool to enable.
Returns:
A confirmation of enabling pool n.
</details>
"""
return await self.send_command("enablepool", parameters=n)
async def addpool(self, url: str, username: str, password: str) -> dict:
"""Add a pool to the miner.
<details>
<summary>Expand</summary>
Parameters:
url: The URL of the new pool to add.
username: The users username on the new pool.
password: The worker password on the new pool.
Returns:
A confirmation of adding the pool.
</details>
"""
return await self.send_command(
"addpool", parameters=f"{url},{username},{password}"
)
async def poolpriority(self, *n: int) -> dict:
"""Set pool priority.
<details>
<summary>Expand</summary>
Parameters:
*n: Pools in order of priority.
Returns:
A confirmation of setting pool priority.
</details>
"""
pools = f"{','.join([str(item) for item in n])}"
return await self.send_command("poolpriority", parameters=pools)
async def poolquota(self, n: int, q: int) -> dict:
"""Set pool quota.
<details>
<summary>Expand</summary>
Parameters:
n: Pool number to set quota on.
q: Quota to set the pool to.
Returns:
A confirmation of setting pool quota.
</details>
"""
return await self.send_command("poolquota", parameters=f"{n},{q}")
async def disablepool(self, n: int) -> dict:
"""Disable a pool.
<details>
<summary>Expand</summary>
Parameters:
n: Pool to disable.
Returns:
A confirmation of diabling the pool.
</details>
"""
return await self.send_command("disablepool", parameters=n)
async def removepool(self, n: int) -> dict:
"""Remove a pool.
<details>
<summary>Expand</summary>
Parameters:
n: Pool to remove.
Returns:
A confirmation of removing the pool.
</details>
"""
return await self.send_command("removepool", parameters=n)
async def save(self, filename: str = None) -> dict:
"""Save the config.
<details>
<summary>Expand</summary>
Parameters:
filename: Filename to save the config as.
Returns:
A confirmation of saving the config.
</details>
"""
if filename:
return await self.send_command("save", parameters=filename)
else:
return await self.send_command("save")
async def quit(self) -> dict:
"""Quit CGMiner.
<details>
<summary>Expand</summary>
Returns:
A single "BYE" before CGMiner quits.
</details>
"""
return await self.send_command("quit")
async def notify(self) -> dict:
"""Notify the user of past errors.
<details>
<summary>Expand</summary>
Returns:
The last status and count of each devices problem(s).
</details>
"""
return await self.send_command("notify")
async def privileged(self) -> dict:
"""Check if you have privileged access.
<details>
<summary>Expand</summary>
Returns:
The STATUS section with an error if you have no privileged access, or success if you have privileged access.
</details>
"""
return await self.send_command("privileged")
async def pgaenable(self, n: int) -> dict:
"""Enable PGA n.
<details>
<summary>Expand</summary>
Parameters:
n: The PGA to enable.
Returns:
A confirmation of enabling PGA n.
</details>
"""
return await self.send_command("pgaenable", parameters=n)
async def pgadisable(self, n: int) -> dict:
"""Disable PGA n.
<details>
<summary>Expand</summary>
Parameters:
n: The PGA to disable.
Returns:
A confirmation of disabling PGA n.
</details>
"""
return await self.send_command("pgadisable", parameters=n)
async def pgarestart(self, n: int) -> dict:
"""Restart PGA n.
<details>
<summary>Expand</summary>
Parameters:
n: The PGA to restart.
Returns:
A confirmation of restarting PGA n.
</details>
"""
return await self.send_command("pgadisable", parameters=n)
async def pgaidentify(self, n: int) -> dict:
"""Identify PGA n.
<details>
<summary>Expand</summary>
Parameters:
n: The PGA to identify.
Returns:
A confirmation of identifying PGA n.
</details>
"""
return await self.send_command("pgaidentify", parameters=n)
async def procenable(self, n: int) -> dict:
"""Enable processor n.
<details>
<summary>Expand</summary>
Parameters:
n: The processor to enable.
Returns:
A confirmation of enabling processor n.
</details>
"""
return await self.send_command("procenable", parameters=n)
async def procdisable(self, n: int) -> dict:
"""Disable processor n.
<details>
<summary>Expand</summary>
Parameters:
n: The processor to disable.
Returns:
A confirmation of disabling processor n.
</details>
"""
return await self.send_command("procdisable", parameters=n)
async def procrestart(self, n: int) -> dict:
"""Restart processor n.
<details>
<summary>Expand</summary>
Parameters:
n: The processor to restart.
Returns:
A confirmation of restarting processor n.
</details>
"""
return await self.send_command("procdisable", parameters=n)
async def procidentify(self, n: int) -> dict:
"""Identify processor n.
<details>
<summary>Expand</summary>
Parameters:
n: The processor to identify.
Returns:
A confirmation of identifying processor n.
</details>
"""
return await self.send_command("procidentify", parameters=n)
async def devdetails(self) -> dict:
"""Get data on all devices with their static details.
<details>
<summary>Expand</summary>
Returns:
Data on all devices with their static details.
</details>
"""
return await self.send_command("devdetails")
async def restart(self) -> dict:
"""Restart CGMiner using the API.
<details>
<summary>Expand</summary>
Returns:
A reply informing of the restart.
</details>
"""
return await self.send_command("restart")
async def stats(self) -> dict:
"""Get stats of each device/pool with more than 1 getwork.
<details>
<summary>Expand</summary>
Returns:
Stats of each device/pool with more than 1 getwork.
</details>
"""
return await self.send_command("stats")
async def check(self, command: str) -> dict:
"""Check if the command command exists in CGMiner.
<details>
<summary>Expand</summary>
Parameters:
command: The command to check.
Returns:
## Information about a command:
* Exists (Y/N) <- the command exists in this version
* Access (Y/N) <- you have access to use the command
</details>
"""
return await self.send_command("check", parameters=command)
async def failover_only(self, failover: bool) -> dict:
"""Set failover-only.
<details>
<summary>Expand</summary>
Parameters:
failover: What to set failover-only to.
Returns:
Confirmation of setting failover-only.
</details>
"""
return await self.send_command("failover-only", parameters=failover)
async def coin(self) -> dict:
"""Get information on the current coin.
<details>
<summary>Expand</summary>
Returns:
## Information about the current coin being mined:
* Hash Method <- the hashing algorithm
* Current Block Time <- blocktime as a float, 0 means none
* Current Block Hash <- the hash of the current block, blank means none
* LP <- whether LP is in use on at least 1 pool
* Network Difficulty: the current network difficulty
</details>
"""
return await self.send_command("coin")
async def debug(self, setting: str) -> dict:
"""Set a debug setting.
<details>
<summary>Expand</summary>
Parameters:
setting: Which setting to switch to.
## Options are:
* Silent
* Quiet
* Verbose
* Debug
* RPCProto
* PerDevice
* WorkTime
* Normal
Returns:
Data on which debug setting was enabled or disabled.
</details>
"""
return await self.send_command("debug", parameters=setting)
async def setconfig(self, name: str, n: int) -> dict:
"""Set config of name to value n.
<details>
<summary>Expand</summary>
Parameters:
name: The name of the config setting to set.
## Options are:
* queue
* scantime
* expiry
n: The value to set the 'name' setting to.
Returns:
The results of setting config of name to n.
</details>
"""
return await self.send_command("setconfig", parameters=f"{name},{n}")
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
"""Set PGA option opt to val on PGA n.
<details>
<summary>Expand</summary>
Options:
```
MMQ -
opt: clock
val: 2 - 250 (multiple of 2)
XBS -
opt: clock
val: 2 - 250 (multiple of 2)
```
Parameters:
n: The PGA to set the options on.
opt: The option to set. Setting this to 'help' returns a help message.
val: The value to set the option to.
Returns:
Confirmation of setting PGA n with opt[,val].
</details>
"""
if val:
return await self.send_command("pgaset", parameters=f"{n},{opt},{val}")
else:
return await self.send_command("pgaset", parameters=f"{n},{opt}")
async def pprocset(self, n: int, opt: str, val: int = None) -> dict:
"""Set processor option opt to val on processor n.
<details>
<summary>Expand</summary>
Options:
```
MMQ -
opt: clock
val: 2 - 250 (multiple of 2)
XBS -
opt: clock
val: 2 - 250 (multiple of 2)
```
Parameters:
n: The PGA to set the options on.
opt: The option to set. Setting this to 'help' returns a help message.
val: The value to set the option to.
Returns:
Confirmation of setting PGA n with opt[,val].
</details>
"""
if val:
return await self.send_command("pgaset", parameters=f"{n},{opt},{val}")
else:
return await self.send_command("pgaset", parameters=f"{n},{opt}")
async def zero(self, which: str, summary: bool) -> dict:
"""Zero a device.
<details>
<summary>Expand</summary>
Parameters:
which: Which device to zero. Setting this to 'all' zeros all devices. Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
summary: Whether or not to show a full summary.
Returns:
the STATUS section with info on the zero and optional summary.
</details>
"""
return await self.send_command("zero", parameters=f"{which},{summary}")

732
pyasic/API/bmminer.py Normal file
View File

@@ -0,0 +1,732 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import logging
from pyasic.API import APIError, BaseMinerAPI
class BMMinerAPI(BaseMinerAPI):
"""An abstraction of the BMMiner API.
Each method corresponds to an API command in BMMiner.
[BMMiner API documentation](https://github.com/jameshilliard/bmminer/blob/master/API-README)
This class abstracts use of the BMMiner API, as well as the
methods for sending commands to it. The `self.send_command()`
function handles sending a command to the miner asynchronously, and
as such is the base for many of the functions in this class, which
rely on it to send the command for them.
Parameters:
ip: The IP of the miner to reference the API on.
port: The port to reference the API on. Default is 4028.
"""
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028) -> None:
super().__init__(ip, port=port)
self.api_ver = api_ver
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesn't work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
data = await self._x19_multicommand(
*command.split("+"), allow_warning=allow_warning
)
data["multicommand"] = True
return data
async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict:
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(
await self.send_command(cmd, allow_warning=allow_warning)
)
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
)
return data
async def version(self) -> dict:
"""Get miner version info.
<details>
<summary>Expand</summary>
Returns:
Miner version information.
</details>
"""
return await self.send_command("version")
async def config(self) -> dict:
"""Get some basic configuration info.
<details>
<summary>Expand</summary>
Returns:
## Some miner configuration information:
* ASC Count <- the number of ASCs
* PGA Count <- the number of PGAs
* Pool Count <- the number of Pools
* Strategy <- the current pool strategy
* Log Interval <- the interval of logging
* Device Code <- list of compiled device drivers
* OS <- the current operating system
* Failover-Only <- failover-only setting
* Scan Time <- scan-time setting
* Queue <- queue setting
* Expiry <- expiry setting
</details>
"""
return await self.send_command("config")
async def summary(self) -> dict:
"""Get the status summary of the miner.
<details>
<summary>Expand</summary>
Returns:
The status summary of the miner.
</details>
"""
return await self.send_command("summary")
async def pools(self) -> dict:
"""Get pool information.
<details>
<summary>Expand</summary>
Returns:
Miner pool information.
</details>
"""
return await self.send_command("pools")
async def devs(self) -> dict:
"""Get data on each PGA/ASC with their details.
<details>
<summary>Expand</summary>
Returns:
Data on each PGA/ASC with their details.
</details>
"""
return await self.send_command("devs")
async def edevs(self, old: bool = False) -> dict:
"""Get data on each PGA/ASC with their details, ignoring blacklisted and zombie devices.
<details>
<summary>Expand</summary>
Parameters:
old: Include zombie devices that became zombies less than 'old' seconds ago
Returns:
Data on each PGA/ASC with their details.
</details>
"""
if old:
return await self.send_command("edevs", parameters=old)
else:
return await self.send_command("edevs")
async def pga(self, n: int) -> dict:
"""Get data from PGA n.
<details>
<summary>Expand</summary>
Parameters:
n: The PGA number to get data from.
Returns:
Data on the PGA n.
</details>
"""
return await self.send_command("pga", parameters=n)
async def pgacount(self) -> dict:
"""Get data fon all PGAs.
<details>
<summary>Expand</summary>
Returns:
Data on the PGAs connected.
</details>
"""
return await self.send_command("pgacount")
async def switchpool(self, n: int) -> dict:
"""Switch pools to pool n.
<details>
<summary>Expand</summary>
Parameters:
n: The pool to switch to.
Returns:
A confirmation of switching to pool n.
</details>
"""
return await self.send_command("switchpool", parameters=n)
async def enablepool(self, n: int) -> dict:
"""Enable pool n.
<details>
<summary>Expand</summary>
Parameters:
n: The pool to enable.
Returns:
A confirmation of enabling pool n.
</details>
"""
return await self.send_command("enablepool", parameters=n)
async def addpool(self, url: str, username: str, password: str) -> dict:
"""Add a pool to the miner.
<details>
<summary>Expand</summary>
Parameters:
url: The URL of the new pool to add.
username: The users username on the new pool.
password: The worker password on the new pool.
Returns:
A confirmation of adding the pool.
</details>
"""
return await self.send_command(
"addpool", parameters=f"{url},{username},{password}"
)
async def poolpriority(self, *n: int) -> dict:
"""Set pool priority.
<details>
<summary>Expand</summary>
Parameters:
*n: Pools in order of priority.
Returns:
A confirmation of setting pool priority.
</details>
"""
pools = f"{','.join([str(item) for item in n])}"
return await self.send_command("poolpriority", parameters=pools)
async def poolquota(self, n: int, q: int) -> dict:
"""Set pool quota.
<details>
<summary>Expand</summary>
Parameters:
n: Pool number to set quota on.
q: Quota to set the pool to.
Returns:
A confirmation of setting pool quota.
</details>
"""
return await self.send_command("poolquota", parameters=f"{n},{q}")
async def disablepool(self, n: int) -> dict:
"""Disable a pool.
<details>
<summary>Expand</summary>
Parameters:
n: Pool to disable.
Returns:
A confirmation of diabling the pool.
</details>
"""
return await self.send_command("disablepool", parameters=n)
async def removepool(self, n: int) -> dict:
"""Remove a pool.
<details>
<summary>Expand</summary>
Parameters:
n: Pool to remove.
Returns:
A confirmation of removing the pool.
</details>
"""
return await self.send_command("removepool", parameters=n)
async def save(self, filename: str = None) -> dict:
"""Save the config.
<details>
<summary>Expand</summary>
Parameters:
filename: Filename to save the config as.
Returns:
A confirmation of saving the config.
</details>
"""
if filename:
return await self.send_command("save", parameters=filename)
else:
return await self.send_command("save")
async def quit(self) -> dict:
"""Quit BMMiner.
<details>
<summary>Expand</summary>
Returns:
A single "BYE" before BMMiner quits.
</details>
"""
return await self.send_command("quit")
async def notify(self) -> dict:
"""Notify the user of past errors.
<details>
<summary>Expand</summary>
Returns:
The last status and count of each devices problem(s).
</details>
"""
return await self.send_command("notify")
async def privileged(self) -> dict:
"""Check if you have privileged access.
<details>
<summary>Expand</summary>
Returns:
The STATUS section with an error if you have no privileged access, or success if you have privileged access.
</details>
"""
return await self.send_command("privileged")
async def pgaenable(self, n: int) -> dict:
"""Enable PGA n.
<details>
<summary>Expand</summary>
Parameters:
n: The PGA to enable.
Returns:
A confirmation of enabling PGA n.
</details>
"""
return await self.send_command("pgaenable", parameters=n)
async def pgadisable(self, n: int) -> dict:
"""Disable PGA n.
<details>
<summary>Expand</summary>
Parameters:
n: The PGA to disable.
Returns:
A confirmation of disabling PGA n.
</details>
"""
return await self.send_command("pgadisable", parameters=n)
async def pgaidentify(self, n: int) -> dict:
"""Identify PGA n.
<details>
<summary>Expand</summary>
Parameters:
n: The PGA to identify.
Returns:
A confirmation of identifying PGA n.
</details>
"""
return await self.send_command("pgaidentify", parameters=n)
async def devdetails(self) -> dict:
"""Get data on all devices with their static details.
<details>
<summary>Expand</summary>
Returns:
Data on all devices with their static details.
</details>
"""
return await self.send_command("devdetails")
async def restart(self) -> dict:
"""Restart BMMiner using the API.
<details>
<summary>Expand</summary>
Returns:
A reply informing of the restart.
</details>
"""
return await self.send_command("restart")
async def stats(self) -> dict:
"""Get stats of each device/pool with more than 1 getwork.
<details>
<summary>Expand</summary>
Returns:
Stats of each device/pool with more than 1 getwork.
</details>
"""
return await self.send_command("stats")
async def estats(self, old: bool = False) -> dict:
"""Get stats of each device/pool with more than 1 getwork, ignoring zombie devices.
<details>
<summary>Expand</summary>
Parameters:
old: Include zombie devices that became zombies less than 'old' seconds ago.
Returns:
Stats of each device/pool with more than 1 getwork, ignoring zombie devices.
</details>
"""
if old:
return await self.send_command("estats", parameters=old)
else:
return await self.send_command("estats")
async def check(self, command: str) -> dict:
"""Check if the command command exists in BMMiner.
<details>
<summary>Expand</summary>
Parameters:
command: The command to check.
Returns:
## Information about a command:
* Exists (Y/N) <- the command exists in this version
* Access (Y/N) <- you have access to use the command
</details>
"""
return await self.send_command("check", parameters=command)
async def failover_only(self, failover: bool) -> dict:
"""Set failover-only.
<details>
<summary>Expand</summary>
Parameters:
failover: What to set failover-only to.
Returns:
Confirmation of setting failover-only.
</details>
"""
return await self.send_command("failover-only", parameters=failover)
async def coin(self) -> dict:
"""Get information on the current coin.
<details>
<summary>Expand</summary>
Returns:
## Information about the current coin being mined:
* Hash Method <- the hashing algorithm
* Current Block Time <- blocktime as a float, 0 means none
* Current Block Hash <- the hash of the current block, blank means none
* LP <- whether LP is in use on at least 1 pool
* Network Difficulty: the current network difficulty
</details>
"""
return await self.send_command("coin")
async def debug(self, setting: str) -> dict:
"""Set a debug setting.
<details>
<summary>Expand</summary>
Parameters:
setting: Which setting to switch to.
## Options are:
* Silent
* Quiet
* Verbose
* Debug
* RPCProto
* PerDevice
* WorkTime
* Normal
Returns:
Data on which debug setting was enabled or disabled.
</details>
"""
return await self.send_command("debug", parameters=setting)
async def setconfig(self, name: str, n: int) -> dict:
"""Set config of name to value n.
<details>
<summary>Expand</summary>
Parameters:
name: The name of the config setting to set.
## Options are:
* queue
* scantime
* expiry
n: The value to set the 'name' setting to.
Returns:
The results of setting config of name to n.
</details>
"""
return await self.send_command("setconfig", parameters=f"{name},{n}")
async def usbstats(self) -> dict:
"""Get stats of all USB devices except ztex.
<details>
<summary>Expand</summary>
Returns:
The stats of all USB devices except ztex.
</details>
"""
return await self.send_command("usbstats")
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
"""Set PGA option opt to val on PGA n.
<details>
<summary>Expand</summary>
Options:
```
MMQ -
opt: clock
val: 160 - 230 (multiple of 2)
CMR -
opt: clock
val: 100 - 220
```
Parameters:
n: The PGA to set the options on.
opt: The option to set. Setting this to 'help' returns a help message.
val: The value to set the option to.
Returns:
Confirmation of setting PGA n with opt[,val].
</details>
"""
if val:
return await self.send_command("pgaset", parameters=f"{n},{opt},{val}")
else:
return await self.send_command("pgaset", parameters=f"{n},{opt}")
async def zero(self, which: str, summary: bool) -> dict:
"""Zero a device.
<details>
<summary>Expand</summary>
Parameters:
which: Which device to zero. Setting this to 'all' zeros all devices. Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
summary: Whether or not to show a full summary.
Returns:
the STATUS section with info on the zero and optional summary.
</details>
"""
return await self.send_command("zero", parameters=f"{which},{summary}")
async def hotplug(self, n: int) -> dict:
"""Enable hotplug.
<details>
<summary>Expand</summary>
Parameters:
n: The device number to set hotplug on.
Returns:
Information on hotplug status.
</details>
"""
return await self.send_command("hotplug", parameters=n)
async def asc(self, n: int) -> dict:
"""Get data for ASC device n.
<details>
<summary>Expand</summary>
Parameters:
n: The device to get data for.
Returns:
The data for ASC device n.
</details>
"""
return await self.send_command("asc", parameters=n)
async def ascenable(self, n: int) -> dict:
"""Enable ASC device n.
<details>
<summary>Expand</summary>
Parameters:
n: The device to enable.
Returns:
Confirmation of enabling ASC device n.
</details>
"""
return await self.send_command("ascenable", parameters=n)
async def ascdisable(self, n: int) -> dict:
"""Disable ASC device n.
<details>
<summary>Expand</summary>
Parameters:
n: The device to disable.
Returns:
Confirmation of disabling ASC device n.
</details>
"""
return await self.send_command("ascdisable", parameters=n)
async def ascidentify(self, n: int) -> dict:
"""Identify ASC device n.
<details>
<summary>Expand</summary>
Parameters:
n: The device to identify.
Returns:
Confirmation of identifying ASC device n.
</details>
"""
return await self.send_command("ascidentify", parameters=n)
async def asccount(self) -> dict:
"""Get data on the number of ASC devices and their info.
<details>
<summary>Expand</summary>
Returns:
Data on all ASC devices.
</details>
"""
return await self.send_command("asccount")
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
"""Set ASC n option opt to value val.
<details>
<summary>Expand</summary>
Sets an option on the ASC n to a value. Allowed options are:
```
AVA+BTB -
opt: freq
val: 256 - 1024 (chip frequency)
BTB -
opt: millivolts
val: 1000 - 1400 (core voltage)
MBA -
opt: reset
val: 0 - # of chips (reset a chip)
opt: freq
val: 0 - # of chips, 100 - 1400 (chip frequency)
opt: ledcount
val: 0 - 100 (chip count for LED)
opt: ledlimit
val: 0 - 200 (LED off below GH/s)
opt: spidelay
val: 0 - 9999 (SPI per I/O delay)
opt: spireset
val: i or s, 0 - 9999 (SPI regular reset)
opt: spisleep
val: 0 - 9999 (SPI reset sleep in ms)
BMA -
opt: volt
val: 0 - 9
opt: clock
val: 0 - 15
```
Parameters:
n: The ASC to set the options on.
opt: The option to set. Setting this to 'help' returns a help message.
val: The value to set the option to.
Returns:
Confirmation of setting option opt to value val.
</details>
"""
if val:
return await self.send_command("ascset", parameters=f"{n},{opt},{val}")
else:
return await self.send_command("ascset", parameters=f"{n},{opt}")
async def lcd(self) -> dict:
"""Get a general all-in-one status summary of the miner.
<details>
<summary>Expand</summary>
Returns:
An all-in-one status summary of the miner.
</details>
"""
return await self.send_command("lcd")
async def lockstats(self) -> dict:
"""Write lockstats to STDERR.
<details>
<summary>Expand</summary>
Returns:
The result of writing the lock stats to STDERR.
</details>
"""
return await self.send_command("lockstats")

View File

@@ -14,10 +14,10 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.rpc.base import BaseMinerRPCAPI
from pyasic.API import BaseMinerAPI
class BOSMinerRPCAPI(BaseMinerRPCAPI):
class BOSMinerAPI(BaseMinerAPI):
"""An abstraction of the BOSMiner API.
Each method corresponds to an API command in BOSMiner.
@@ -29,8 +29,16 @@ class BOSMinerRPCAPI(BaseMinerRPCAPI):
function handles sending a command to the miner asynchronously, and
as such is the base for many of the functions in this class, which
rely on it to send the command for them.
Parameters:
ip: The IP of the miner to reference the API on.
port: The port to reference the API on. Default is 4028.
"""
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028) -> None:
super().__init__(ip, port=port)
self.api_ver = api_ver
async def asccount(self) -> dict:
"""Get data on the number of ASC devices and their info.
<details>

View File

@@ -22,19 +22,15 @@ import hashlib
import json
import logging
import re
import struct
import warnings
from asyncio import Future, StreamReader, StreamWriter
from typing import Any, AsyncGenerator, Callable, Literal, Union
from typing import Union
import httpx
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from passlib.handlers.md5_crypt import md5_crypt
from pyasic import settings
from pyasic.errors import APIError, APIWarning
from pyasic.misc import api_min_version, validate_command_output
from pyasic.rpc.base import BaseMinerRPCAPI
from pyasic.API import BaseMinerAPI
from pyasic.errors import APIError
from pyasic.misc import api_min_version
from pyasic.settings import PyasicSettings
### IMPORTANT ###
# you need to change the password of the miners using the Whatsminer
@@ -44,18 +40,12 @@ from pyasic.rpc.base import BaseMinerRPCAPI
# you change the password, you can pass that to this class as pwd,
# or add it as the Whatsminer_pwd in the settings.toml file.
PrePowerOnMessage = Union[
Literal["wait for adjust temp"],
Literal["adjust complete"],
Literal["adjust continue"],
]
def _crypt(word: str, salt: str) -> str:
r"""Encrypts a word with a salt, using a standard salt format.
"""Encrypts a word with a salt, using a standard salt format.
Encrypts a word using a salt with the format
\s*\$(\d+)\$([\w\./]*)\$. If this format is not used, a
'\s*\$(\d+)\$([\w\./]*)\$'. If this format is not used, a
ValueError is raised.
Parameters:
@@ -66,7 +56,7 @@ def _crypt(word: str, salt: str) -> str:
An MD5 hash of the word with the salt.
"""
# compile a standard format for the salt
standard_salt = re.compile(r"\s*\$(\d+)\$([\w\./]*)\$")
standard_salt = re.compile("\s*\$(\d+)\$([\w\./]*)\$")
# check if the salt matches
match = standard_salt.match(salt)
# if the matching fails, the salt is incorrect
@@ -93,7 +83,7 @@ def _add_to_16(string: str) -> bytes:
return str.encode(string) # return bytes
def parse_btminer_priviledge_data(token_data: dict, data: dict) -> dict:
def parse_btminer_priviledge_data(token_data: dict, data: dict):
"""Parses data returned from the BTMiner privileged API.
Parses data from the BTMiner privileged API using the token
@@ -163,7 +153,7 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
return api_packet_str.encode("utf-8")
class BTMinerRPCAPI(BaseMinerRPCAPI):
class BTMinerAPI(BaseMinerAPI):
"""An abstraction of the API for MicroBT Whatsminers, BTMiner.
Each method corresponds to an API command in BMMiner.
@@ -184,12 +174,24 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
encoded using a token from the miner, all privileged commands do
this automatically for you and will decode the output to look like
a normal output from a miner API.
Parameters:
ip: The IP of the miner to reference the API on.
port: The port to reference the API on. Default is 4028.
pwd: The admin password of the miner. Default is admin.
"""
def __init__(self, ip: str, port: int = 4028, api_ver: str = "0.0.0") -> None:
super().__init__(ip, port, api_ver)
self.pwd = settings.get("default_whatsminer_rpc_password", "admin")
self.token = None
def __init__(
self,
ip: str,
api_ver: str = "0.0.0",
port: int = 4028,
pwd: str = PyasicSettings().global_whatsminer_password,
):
super().__init__(ip, port)
self.pwd = pwd
self.current_token = None
self.api_ver = api_ver
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner.
@@ -201,40 +203,27 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# commands starting with "get_" and the "status" command aren't supported, but we can fake that
split_commands = []
# commands starting with "get_" aren't supported, but we can fake that
get_commands_data = {}
for command in list(commands):
if command.startswith("get_") or command == "status":
if command.startswith("get_"):
commands.remove(command)
# send seperately and append later
split_commands.append(command)
try:
get_commands_data[command] = [
await self.send_command(command, allow_warning=allow_warning)
]
except APIError:
get_commands_data[command] = [{}]
command = "+".join(commands)
tasks = []
if len(split_commands) > 0:
tasks.append(
asyncio.create_task(
self._send_split_multicommand(
*split_commands, allow_warning=allow_warning
)
)
)
tasks.append(
asyncio.create_task(self.send_command(command, allow_warning=allow_warning))
)
try:
all_data = await asyncio.gather(*tasks)
main_data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
return {}
data = {}
for item in all_data:
data.update(item)
main_data = {command: [{}] for command in commands}
logging.debug(f"{self} - (Multicommand) - Received data")
data = dict(**main_data, **get_commands_data)
data["multicommand"] = True
return data
@@ -244,28 +233,6 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
ignore_errors: bool = False,
timeout: int = 10,
**kwargs,
) -> dict:
try:
return await self._send_privileged_command(
command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
)
except APIError as e:
if not e.message == "can't access write cmd":
raise
try:
await self.open_api()
except Exception as e:
raise APIError("Failed to open whatsminer API.") from e
return await self._send_privileged_command(
command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
)
async def _send_privileged_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
timeout: int = 10,
**kwargs,
) -> dict:
logging.debug(
f"{self} - (Send Privileged Command) - {command} " + f"with args {kwargs}"
@@ -279,7 +246,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
logging.debug(f"{self} - (Send Privileged Command) - Sending")
try:
data = await self._send_bytes(enc_command, timeout=timeout)
data = await self._send_bytes(enc_command, timeout)
except (asyncio.CancelledError, asyncio.TimeoutError):
if ignore_errors:
return {}
@@ -292,13 +259,13 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
data = self._load_api_data(data)
try:
data = parse_btminer_priviledge_data(self.token, data)
data = parse_btminer_priviledge_data(self.current_token, data)
except Exception as e:
logging.info(f"{str(self.ip)}: {e}")
if not ignore_errors:
# if it fails to validate, it is likely an error
validation = validate_command_output(data)
validation = self._validate_command_output(data)
if not validation[0]:
raise APIError(validation[1])
@@ -315,11 +282,11 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
</details>
"""
logging.debug(f"{self} - (Get Token) - Getting token")
if self.token:
if self.token["timestamp"] > datetime.datetime.now() - datetime.timedelta(
minutes=30
):
return self.token
if self.current_token:
if self.current_token[
"timestamp"
] > datetime.datetime.now() - datetime.timedelta(minutes=30):
return self.current_token
# get the token
data = await self.send_command("get_token")
@@ -339,43 +306,15 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
host_sign = tmp[3]
# set the current token
self.token = {
self.current_token = {
"host_sign": host_sign,
"host_passwd_md5": host_passwd_md5,
"timestamp": datetime.datetime.now(),
}
logging.debug(f"{self} - (Get Token) - Gathered token data: {self.token}")
return self.token
async def open_api(self):
async with httpx.AsyncClient() as c:
stage1_req = (
await c.post(
"https://wmt.pyasic.org/v1/stage1",
json={"ip": str(self.ip)},
follow_redirects=True,
)
).json()
stage1_res = binascii.hexlify(
await self._send_bytes(binascii.unhexlify(stage1_req), port=8889)
)
stage2_req = (
await c.post(
"https://wmt.pyasic.org/v1/stage2",
json={
"ip": str(self.ip),
"stage1_result": stage1_res.decode("utf-8"),
},
)
).json()
for command in stage2_req:
try:
await self._send_bytes(
binascii.unhexlify(command), timeout=3, port=8889
)
except asyncio.TimeoutError:
pass
return True
logging.debug(
f"{self} - (Get Token) - Gathered token data: {self.current_token}"
)
return self.current_token
#### PRIVILEGED COMMANDS ####
# Please read the top of this file to learn
@@ -534,56 +473,11 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
"""
return await self.send_privileged_command("set_low_power")
async def set_high_power(self) -> dict:
"""Set low power mode on the miner using the API.
<details>
<summary>Expand</summary>
Set low power mode on the miner using the API, only works after
changing the password of the miner using the Whatsminer tool.
Returns:
A reply informing of the status of setting low power mode.
</details>
"""
return await self.send_privileged_command("set_high_power")
async def set_normal_power(self) -> dict:
"""Set low power mode on the miner using the API.
<details>
<summary>Expand</summary>
Set low power mode on the miner using the API, only works after
changing the password of the miner using the Whatsminer tool.
Returns:
A reply informing of the status of setting low power mode.
</details>
"""
return await self.send_privileged_command("set_normal_power")
async def update_firmware(self, firmware: bytes) -> bool:
"""Upgrade the firmware running on the miner and using the firmware passed in bytes.
<details>
<summary>Expand</summary>
Set the LED on the miner using the API, only works after
changing the password of the miner using the Whatsminer tool.
Parameters:
firmware (bytes): The firmware binary data to be uploaded.
Returns:
A boolean indicating the success of the firmware upgrade.
Raises:
APIError: If the miner is not ready for firmware update.
</details>
"""
ready = await self.send_privileged_command("upgrade_firmware")
if not ready.get("Msg") == "ready":
raise APIError(f"Not ready for firmware update: {self}")
file_size = struct.pack("<I", len(firmware))
await self._send_bytes(file_size + firmware)
return True
async def update_firmware(self): # noqa - static
"""Not implemented."""
# to be determined if this will be added later
# requires a file stream in bytes
return NotImplementedError
async def reboot(self, timeout: int = 10) -> dict:
"""Reboot the miner using the API.
@@ -680,10 +574,10 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
A reply informing of the status of setting the frequency.
</details>
"""
if not -100 < percent < 100:
if not -10 < percent < 100:
raise APIError(
f"Frequency % is outside of the allowed "
f"range. Please set a % between -100 and "
f"range. Please set a % between -10 and "
f"100"
)
return await self.send_privileged_command(
@@ -791,7 +685,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
)
return await self.send_privileged_command("set_power_pct", percent=str(percent))
async def pre_power_on(self, complete: bool, msg: PrePowerOnMessage) -> dict:
async def pre_power_on(self, complete: bool, msg: str) -> dict:
"""Configure or check status of pre power on.
<details>
@@ -802,14 +696,16 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
Parameters:
complete: check whether pre power on is complete.
msg: the message to check.
msg: ## the message to check.
* `wait for adjust temp`
* `adjust complete`
* `adjust continue`
Returns:
A reply informing of the status of pre power on.
</details>
"""
if msg not in ("wait for adjust temp", "adjust complete", "adjust continue"):
if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue":
raise APIError(
"Message is incorrect, please choose one of "
'["wait for adjust temp", '
@@ -825,34 +721,6 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
)
### ADDED IN V2.0.5 Whatsminer API ###
@api_min_version("2.0.5")
async def set_power_pct_v2(self, percent: int) -> dict:
"""Set the power percentage of the miner based on current power. Used for temporary adjustment. Added in API v2.0.5.
<details>
<summary>Expand</summary>
Set the power percentage of the miner, only works after changing
the password of the miner using the Whatsminer tool.
Parameters:
percent: The power percentage to set.
Returns:
A reply informing of the status of setting the power percentage.
</details>
"""
if not 0 < percent < 100:
raise APIError(
f"Power PCT % is outside of the allowed "
f"range. Please set a % between 0 and "
f"100"
)
return await self.send_privileged_command(
"set_power_pct_v2", percent=str(percent)
)
@api_min_version("2.0.5")
async def set_temp_offset(self, temp_offset: int) -> dict:
"""Set the offset of miner hash board target temperature.
@@ -1102,315 +970,3 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
</details>
"""
return await self.send_command("get_error_code", allow_warning=False)
class BTMinerV3RPCAPI(BaseMinerRPCAPI):
def __init__(self, ip: str, port: int = 4433, api_ver: str = "0.0.0"):
super().__init__(ip, port, api_ver=api_ver)
self.salt = None
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.
"""
commands = self._check_commands(*commands)
data = await self._send_split_multicommand(*commands)
data["multicommand"] = True
return data
async def send_command(
self, command: str, parameters: Any = None, **kwargs
) -> dict:
if ":" in command:
parameters = command.split(":")[1]
command = command.split(":")[0]
cmd = {"cmd": command}
if parameters is not None:
cmd["param"] = parameters
if command.startswith("set."):
salt = await self.get_salt()
ts = int(datetime.datetime.now().timestamp())
cmd["ts"] = ts
token_str = cmd["cmd"] + self.pwd + salt + str(ts)
token_hashed = bytearray(
base64.b64encode(hashlib.sha256(token_str.encode("utf-8")).digest())
)
token_hashed[8] = 0
cmd["account"] = "super"
cmd["token"] = token_hashed.decode("ascii")
# send the command
ser = json.dumps(cmd).encode("utf-8")
header = struct.pack("<I", len(ser))
return json.loads(
await self._send_bytes(header + json.dumps(cmd).encode("utf-8"))
)
async def _send_bytes(
self,
data: bytes,
*,
port: int = None,
timeout: int = 100,
) -> bytes:
if port is None:
port = self.port
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), port)
# handle OSError 121
except OSError as e:
if e.errno == 121:
logging.warning(
f"{self} - ([Hidden] Send Bytes) - Semaphore timeout expired."
)
return b"{}"
# send the command
try:
data_task = asyncio.create_task(self._read_bytes(reader, timeout=timeout))
logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing")
writer.write(data)
logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
await writer.drain()
await data_task
ret_data = data_task.result()
except TimeoutError:
logging.warning(f"{self} - ([Hidden] Send Bytes) - Read timeout expired.")
return b"{}"
# close the connection
logging.debug(f"{self} - ([Hidden] Send Bytes) - Closing")
writer.close()
await writer.wait_closed()
return ret_data
def _check_commands(self, *commands) -> list:
return_commands = []
for command in commands:
if command.startswith("get.") or command.startswith("set."):
return_commands.append(command)
else:
warnings.warn(
f"""Removing incorrect command: {command}
If you are sure you want to use this command please use API.send_command("{command}", ignore_errors=True) instead.""",
APIWarning,
)
return return_commands
async def _read_bytes(self, reader: asyncio.StreamReader, timeout: int) -> bytes:
ret_data = b""
# loop to receive all the data
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
try:
header = await reader.readexactly(4)
length = struct.unpack("<I", header)[0]
ret_data = await reader.readexactly(length)
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except Exception as e:
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
return ret_data
async def get_salt(self) -> str:
if self.salt is not None:
return self.salt
data = await self.send_command("get.device.info", "salt")
self.salt = data["msg"]["salt"]
return self.salt
async def get_miner_report(self) -> AsyncGenerator[dict, None]:
if self.writer is None:
await self.connect()
result = asyncio.Queue()
async def callback(data: dict):
await result.put(data)
cb_fn = callback
try:
self.cmd_callbacks["get.miner.report"].add(cb_fn)
while True:
yield await result.get()
if self.writer.is_closing():
break
finally:
self.cmd_callbacks["get.miner.report"].remove(cb_fn)
async def get_system_setting(self) -> dict | None:
return await self.send_command("get.system.setting")
async def get_miner_status_summary(self) -> dict | None:
return await self.send_command("get.miner.status", parameters="summary")
async def get_miner_status_edevs(self) -> dict | None:
return await self.send_command("get.miner.status", parameters="edevs")
async def get_miner_status_pools(self) -> dict | None:
return await self.send_command("get.miner.status", parameters="pools")
async def get_miner_history(self) -> dict | None:
data = await self.send_command(
"get.miner.history",
parameters={
"start": "1",
"stop": str(datetime.datetime.now().timestamp()),
},
)
ret = {}
result = data.get("msg")
if result is not None:
unparsed = result["Data"].strip()
for item in unparsed.split(" "):
list_item = item.split(",")
timestamp = int(list_item.pop(0))
ret[timestamp] = list_item
return ret
async def get_psu_command(self):
return await self.send_command("get.psu.command")
async def get_miner_setting(self) -> dict | None:
return await self.send_command("get.miner.setting")
async def get_device_info(self) -> dict | None:
return await self.send_command("get.device.info")
async def get_log_download(self) -> dict | None:
return await self.send_command("get.log.download")
async def get_fan_setting(self) -> dict | None:
return await self.send_command("get.fan.setting")
async def set_system_reboot(self) -> dict | None:
return await self.send_command("set.system.reboot")
async def set_system_factory_reset(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.system.factory_reset")
async def set_system_update_firmware(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.system.update_firmware")
async def set_system_net_config(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.system.net_config")
async def set_system_led(self, leds: list | None = None) -> dict | None:
if leds is None:
return await self.send_command("set.system.led", parameters="auto")
else:
return await self.send_command("set.system.led", parameters=leds)
async def set_system_time_randomized(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.system.time_randomized")
async def set_system_timezone(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.system.timezone")
async def set_system_hostname(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.system.hostname")
async def set_system_webpools(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.system.webpools")
async def set_miner_target_freq(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.target_freq")
async def set_miner_heat_mode(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.heat_mode")
async def set_system_ntp_server(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.system.ntp_server")
async def set_miner_service(self, value: str) -> dict | None:
return await self.send_command("set.miner.service", parameters=value)
async def set_miner_power_mode(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.power_mode")
async def set_miner_cointype(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.cointype")
async def set_miner_pools(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.pools")
async def set_miner_fastboot(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.fastboot")
async def set_miner_power_percent(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.power_percent")
async def set_miner_pre_power_on(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.pre_power_on")
async def set_miner_restore_setting(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.restore_setting")
async def set_miner_report(self, frequency: int = 1) -> dict | None:
return await self.send_command(
"set.miner.report", parameters={"gap": frequency}
)
async def set_miner_power_limit(self, power: int) -> dict | None:
return await self.send_command("set.miner.power_limit", parameters=power)
async def set_miner_upfreq_speed(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.miner.upfreq_speed")
async def set_log_upload(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.log.upload")
async def set_user_change_passwd(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.user.change_passwd")
async def set_user_permission(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.user.permission")
async def set_fan_temp_offset(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.fan.temp_offset")
async def set_fan_poweroff_cool(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.fan.poweroff_cool")
async def set_fan_zero_speed(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.fan.zero_speed")
async def set_shell_debug(self, *args, **kwargs) -> dict | None:
raise NotImplementedError
return await self.send_command("set.shell.debug")

View File

@@ -14,10 +14,12 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.rpc.base import BaseMinerRPCAPI
import logging
from pyasic.API import APIError, BaseMinerAPI
class CGMinerRPCAPI(BaseMinerRPCAPI):
class CGMinerAPI(BaseMinerAPI):
"""An abstraction of the CGMiner API.
Each method corresponds to an API command in GGMiner.
@@ -29,8 +31,46 @@ class CGMinerRPCAPI(BaseMinerRPCAPI):
function handles sending a command to the miner asynchronously, and
as such is the base for many of the functions in this class, which
rely on it to send the command for them.
Parameters:
ip: The IP of the miner to reference the API on.
port: The port to reference the API on. Default is 4028.
"""
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028):
super().__init__(ip, port)
self.api_ver = api_ver
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesn't work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
data = await self._x19_multicommand(*command.split("+"))
data["multicommand"] = True
return data
async def _x19_multicommand(self, *commands) -> dict:
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
)
return data
async def version(self) -> dict:
"""Get miner version info.
<details>
@@ -519,8 +559,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI):
<summary>Expand</summary>
Parameters:
which: Which device to zero. Setting this to 'all' zeros all devices.
Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
which: Which device to zero. Setting this to 'all' zeros all devices. Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
summary: Whether or not to show a full summary.

View File

@@ -14,10 +14,10 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.rpc.cgminer import CGMinerRPCAPI
from pyasic.API import BaseMinerAPI
class UnknownRPCAPI(CGMinerRPCAPI):
class UnknownAPI(BaseMinerAPI):
"""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
@@ -25,6 +25,52 @@ class UnknownRPCAPI(CGMinerRPCAPI):
with as many APIs as possible.
"""
def __init__(self, ip, api_ver: str = "0.0.0", port: int = 4028):
super().__init__(ip, port)
self.api_ver = api_ver
async def asccount(self) -> dict:
return await self.send_command("asccount")
async def asc(self, n: int) -> dict:
return await self.send_command("asc", parameters=n)
async def devdetails(self) -> dict:
return await self.send_command("devdetails")
async def devs(self) -> dict:
return await self.send_command("devs")
async def edevs(self, old: bool = False) -> dict:
if old:
return await self.send_command("edevs", parameters="old")
else:
return await self.send_command("edevs")
async def pools(self) -> dict:
return await self.send_command("pools")
async def summary(self) -> dict:
return await self.send_command("summary")
async def stats(self) -> dict:
return await self.send_command("stats")
async def version(self) -> dict:
return await self.send_command("version")
async def estats(self) -> dict:
return await self.send_command("estats")
async def check(self) -> dict:
return await self.send_command("check")
async def coin(self) -> dict:
return await self.send_command("coin")
async def lcd(self) -> dict:
return await self.send_command("lcd")
async def switchpool(self, n: int) -> dict:
# BOS has not implemented this yet, they will in the future
raise NotImplementedError

View File

@@ -13,16 +13,45 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import importlib.metadata
from pyasic import settings
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 MinerData
from pyasic.data import (
BraiinsOSError,
InnosiliconError,
MinerData,
WhatsminerError,
X19Error,
)
from pyasic.errors import APIError, APIWarning
from pyasic.miners import *
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.rpc import *
from pyasic.ssh import *
from pyasic.web import *
from pyasic.settings import PyasicSettings
__version__ = importlib.metadata.version("pyasic")
__all__ = [
"BMMinerAPI",
"BOSMinerAPI",
"BTMinerAPI",
"CGMinerAPI",
"UnknownAPI",
"MinerConfig",
"MinerData",
"BraiinsOSError",
"InnosiliconError",
"WhatsminerError",
"X19Error",
"APIError",
"APIWarning",
"get_miner",
"AnyMiner",
"MinerFactory",
"MinerListener",
"MinerNetwork",
"PyasicSettings",
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,153 +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 __future__ import annotations
from enum import Enum
from pydantic import BaseModel
class MinerConfigOption(Enum):
@classmethod
def from_dict(cls, dict_conf: dict | None):
return cls.default()
def as_am_modern(self) -> dict:
return self.value.as_am_modern()
def as_hiveon_modern(self) -> dict:
return self.value.as_hiveon_modern()
def as_am_old(self) -> dict:
return self.value.as_am_old()
def as_wm(self) -> dict:
return self.value.as_wm()
def as_inno(self) -> dict:
return self.value.as_inno()
def as_goldshell(self) -> dict:
return self.value.as_goldshell()
def as_avalon(self) -> dict:
return self.value.as_avalon()
def as_bosminer(self) -> dict:
return self.value.as_bosminer()
def as_boser(self) -> dict:
return self.value.as_boser
def as_epic(self) -> dict:
return self.value.as_epic()
def as_vnish(self) -> dict:
return self.value.as_vnish()
def as_auradine(self) -> dict:
return self.value.as_auradine()
def as_mara(self) -> dict:
return self.value.as_mara()
def as_espminer(self) -> dict:
return self.value.as_espminer()
def as_luxos(self) -> dict:
return self.value.as_luxos()
def as_elphapex(self) -> dict:
return self.value.as_elphapex()
def __call__(self, *args, **kwargs):
return self.value(*args, **kwargs)
@classmethod
def default(cls):
pass
def __getitem__(self, item):
try:
return getattr(self, item)
except AttributeError:
raise KeyError
class MinerConfigValue(BaseModel):
@classmethod
def from_dict(cls, dict_conf: dict | None):
return cls()
def as_dict(self) -> dict:
return self.model_dump()
def as_am_modern(self) -> dict:
return {}
def as_hiveon_modern(self) -> dict:
return {}
def as_am_old(self) -> dict:
return {}
def as_wm(self) -> dict:
return {}
def as_btminer_v3(self) -> dict:
return {}
def as_inno(self) -> dict:
return {}
def as_goldshell(self) -> dict:
return {}
def as_avalon(self) -> dict:
return {}
def as_bosminer(self) -> dict:
return {}
def as_boser(self) -> dict:
return {}
def as_epic(self) -> dict:
return {}
def as_vnish(self) -> dict:
return {}
def as_auradine(self) -> dict:
return {}
def as_mara(self) -> dict:
return {}
def as_espminer(self) -> dict:
return {}
def as_luxos(self) -> dict:
return {}
def as_elphapex(self) -> dict:
return {}
def __getitem__(self, item):
try:
return getattr(self, item)
except AttributeError:
raise KeyError

View File

@@ -1,421 +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 __future__ import annotations
from typing import TypeVar, Union
from pydantic import Field
from pyasic.config.base import MinerConfigOption, MinerConfigValue
class FanModeNormal(MinerConfigValue):
mode: str = Field(init=False, default="normal")
minimum_fans: int = 1
minimum_speed: int = 0
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "FanModeNormal":
cls_conf = {}
if dict_conf.get("minimum_fans") is not None:
cls_conf["minimum_fans"] = dict_conf["minimum_fans"]
if dict_conf.get("minimum_speed") is not None:
cls_conf["minimum_speed"] = dict_conf["minimum_speed"]
return cls(**cls_conf)
@classmethod
def from_vnish(cls, web_cooling_settings: dict) -> "FanModeNormal":
cls_conf = {}
if web_cooling_settings.get("fan_min_count") is not None:
cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"]
if web_cooling_settings.get("fan_min_duty") is not None:
cls_conf["minimum_speed"] = web_cooling_settings["fan_min_duty"]
return cls(**cls_conf)
@classmethod
def from_bosminer(cls, toml_fan_conf: dict):
cls_conf = {}
if toml_fan_conf.get("min_fans") is not None:
cls_conf["minimum_fans"] = toml_fan_conf["min_fans"]
return cls(**cls_conf)
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": False, "bitmain-fan-pwn": "100"}
def as_hiveon_modern(self) -> dict:
return {"bitmain-fan-ctrl": False, "bitmain-fan-pwn": "100"}
def as_elphapex(self) -> dict:
return {"fc-fan-ctrl": False, "fc-fan-pwn": "100"}
def as_bosminer(self) -> dict:
return {
"temp_control": {"mode": "auto"},
"fan_control": {"min_fans": self.minimum_fans},
}
def as_epic(self) -> dict:
return {
"fans": {
"Auto": {
"Idle Speed": (
self.minimum_speed if not self.minimum_speed == 0 else 100
)
}
}
}
def as_mara(self) -> dict:
return {
"general-config": {"environment-profile": "AirCooling"},
"advance-config": {
"override-fan-control": False,
"fan-fixed-percent": 0,
},
}
def as_espminer(self) -> dict:
return {"autoFanspeed": 1}
def as_luxos(self) -> dict:
return {"fanset": {"speed": -1, "min_fans": self.minimum_fans}}
def as_vnish(self) -> dict:
return {
"cooling": {
"fan_min_count": self.minimum_fans,
"fan_min_duty": self.minimum_speed,
"mode": {
"name": "auto",
"param": None, # Target temp, must be set later...
},
}
}
class FanModeManual(MinerConfigValue):
mode: str = Field(init=False, default="manual")
speed: int = 100
minimum_fans: int = 1
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "FanModeManual":
cls_conf = {}
if dict_conf.get("speed") is not None:
cls_conf["speed"] = dict_conf["speed"]
if dict_conf.get("minimum_fans") is not None:
cls_conf["minimum_fans"] = dict_conf["minimum_fans"]
return cls(**cls_conf)
@classmethod
def from_bosminer(cls, toml_fan_conf: dict) -> "FanModeManual":
cls_conf = {}
if toml_fan_conf.get("min_fans") is not None:
cls_conf["minimum_fans"] = toml_fan_conf["min_fans"]
if toml_fan_conf.get("speed") is not None:
cls_conf["speed"] = toml_fan_conf["speed"]
return cls(**cls_conf)
@classmethod
def from_vnish(cls, web_cooling_settings: dict) -> "FanModeManual":
cls_conf = {}
if web_cooling_settings.get("fan_min_count") is not None:
cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"]
if web_cooling_settings["mode"].get("param") is not None:
cls_conf["speed"] = web_cooling_settings["mode"]["param"]
return cls(**cls_conf)
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": str(self.speed)}
def as_hiveon_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": str(self.speed)}
def as_elphapex(self) -> dict:
return {"fc-fan-ctrl": True, "fc-fan-pwm": str(self.speed)}
def as_bosminer(self) -> dict:
return {
"temp_control": {"mode": "manual"},
"fan_control": {"min_fans": self.minimum_fans, "speed": self.speed},
}
def as_auradine(self) -> dict:
return {"fan": {"percentage": self.speed}}
def as_epic(self) -> dict:
return {"fans": {"Manual": {"speed": self.speed}}}
def as_mara(self) -> dict:
return {
"general-config": {"environment-profile": "AirCooling"},
"advance-config": {
"override-fan-control": True,
"fan-fixed-percent": self.speed,
},
}
def as_espminer(self) -> dict:
return {"autoFanspeed": 0, "fanspeed": self.speed}
def as_luxos(self) -> dict:
return {"fanset": {"speed": self.speed, "min_fans": self.minimum_fans}}
def as_vnish(self) -> dict:
return {
"cooling": {
"fan_min_count": self.minimum_fans,
"fan_min_duty": self.speed,
"mode": {
"name": "manual",
"param": self.speed, # Speed value
},
}
}
class FanModeImmersion(MinerConfigValue):
mode: str = Field(init=False, default="immersion")
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "FanModeImmersion":
return cls()
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": "0"}
def as_hiveon_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": "0"}
def as_elphapex(self) -> dict:
return {"fc-fan-ctrl": True, "fc-fan-pwm": "0"}
def as_bosminer(self) -> dict:
return {
"fan_control": {"min_fans": 0},
}
def as_auradine(self) -> dict:
return {"fan": {"percentage": 0}}
def as_mara(self) -> dict:
return {"general-config": {"environment-profile": "OilImmersionCooling"}}
def as_luxos(self) -> dict:
return {"fanset": {"speed": 0, "min_fans": 0}}
def as_vnish(self) -> dict:
return {"cooling": {"mode": {"name": "immers"}}}
class FanModeConfig(MinerConfigOption):
normal = FanModeNormal
manual = FanModeManual
immersion = FanModeImmersion
@classmethod
def default(cls):
return cls.normal()
@classmethod
def from_dict(cls, dict_conf: dict | None):
if dict_conf is None:
return cls.default()
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
cls_attr = getattr(cls, mode)
if cls_attr is not None:
return cls_attr().from_dict(dict_conf)
@classmethod
def from_am_modern(cls, web_conf: dict):
if web_conf.get("bitmain-fan-ctrl") is not None:
fan_manual = web_conf["bitmain-fan-ctrl"]
if fan_manual:
speed = int(web_conf["bitmain-fan-pwm"])
if speed == 0:
return cls.immersion()
return cls.manual(speed=speed)
else:
return cls.normal()
else:
return cls.default()
@classmethod
def from_hiveon_modern(cls, web_conf: dict):
if web_conf.get("bitmain-fan-ctrl") is not None:
fan_manual = web_conf["bitmain-fan-ctrl"]
if fan_manual:
speed = int(web_conf["bitmain-fan-pwm"])
if speed == 0:
return cls.immersion()
return cls.manual(speed=speed)
else:
return cls.normal()
else:
return cls.default()
@classmethod
def from_elphapex(cls, web_conf: dict):
if web_conf.get("fc-fan-ctrl") is not None:
fan_manual = web_conf["fc-fan-ctrl"]
if fan_manual:
speed = int(web_conf["fc-fan-pwm"])
if speed == 0:
return cls.immersion()
return cls.manual(speed=speed)
else:
return cls.normal()
else:
return cls.default()
@classmethod
def from_epic(cls, web_conf: dict):
try:
fan_mode = web_conf["Fans"]["Fan Mode"]
if fan_mode.get("Manual") is not None:
return cls.manual(speed=fan_mode.get("Manual"))
else:
return cls.normal()
except KeyError:
return cls.default()
@classmethod
def from_bosminer(cls, toml_conf: dict):
try:
mode = toml_conf["temp_control"]["mode"]
fan_config = toml_conf.get("fan_control", {})
if mode == "auto":
return cls.normal().from_bosminer(fan_config)
elif mode == "manual":
if toml_conf.get("fan_control"):
return cls.manual().from_bosminer(fan_config)
return cls.manual()
elif mode == "disabled":
return cls.immersion()
except KeyError:
pass
try:
min_fans = toml_conf["fan_control"]["min_fans"]
except KeyError:
return cls.default()
if min_fans == 0:
return cls.immersion()
return cls.normal(minimum_fans=min_fans)
@classmethod
def from_vnish(cls, web_settings: dict):
try:
mode = web_settings["miner"]["cooling"]["mode"]["name"]
except LookupError:
return cls.default()
if mode == "auto":
return cls.normal().from_vnish(web_settings["miner"]["cooling"])
elif mode == "manual":
return cls.manual().from_vnish(web_settings["miner"]["cooling"])
elif mode == "immers":
return cls.immersion()
@classmethod
def from_boser(cls, grpc_miner_conf: dict):
try:
temperature_conf = grpc_miner_conf["temperature"]
except LookupError:
return cls.default()
keys = temperature_conf.keys()
if "auto" in keys:
if "minimumRequiredFans" in keys:
return cls.normal(minimum_fans=temperature_conf["minimumRequiredFans"])
return cls.normal()
if "manual" in keys:
conf = {}
if "fanSpeedRatio" in temperature_conf["manual"].keys():
conf["speed"] = int(temperature_conf["manual"]["fanSpeedRatio"])
if "minimumRequiredFans" in keys:
conf["minimum_fans"] = int(temperature_conf["minimumRequiredFans"])
return cls.manual(**conf)
if "disabled" in keys:
conf = {}
if "fanSpeedRatio" in temperature_conf["disabled"].keys():
conf["speed"] = int(temperature_conf["disabled"]["fanSpeedRatio"])
return cls.manual(**conf)
return cls.default()
@classmethod
def from_auradine(cls, web_fan: dict):
try:
fan_data = web_fan["Fan"][0]
fan_1_max = fan_data["Max"]
fan_1_target = fan_data["Target"]
return cls.manual(speed=round((fan_1_target / fan_1_max) * 100))
except LookupError:
pass
return cls.default()
@classmethod
def from_mara(cls, web_config: dict):
try:
mode = web_config["general-config"]["environment-profile"]
if mode == "AirCooling":
if web_config["advance-config"]["override-fan-control"]:
return cls.manual(
speed=web_config["advance-config"]["fan-fixed-percent"]
)
return cls.normal()
return cls.immersion()
except LookupError:
pass
return cls.default()
@classmethod
def from_espminer(cls, web_system_info: dict):
if web_system_info["autofanspeed"] == 1:
return cls.normal()
else:
return cls.manual(speed=web_system_info["fanspeed"])
@classmethod
def from_luxos(cls, rpc_fans: dict, rpc_tempctrl: dict):
try:
mode = rpc_tempctrl["TEMPCTRL"][0]["Mode"]
if mode == "Manual":
speed = rpc_fans["FANS"][0]["Speed"]
min_fans = rpc_fans["FANCTRL"][0]["MinFans"]
if min_fans == 0 and speed == 0:
return cls.immersion()
return cls.manual(
speed=speed,
minimum_fans=min_fans,
)
return cls.normal(
minimum_fans=rpc_fans["FANCTRL"][0]["MinFans"],
)
except LookupError:
pass
return cls.default()
FanMode = TypeVar(
"FanMode",
bound=Union[FanModeNormal, FanModeManual, FanModeImmersion],
)

View File

@@ -1,873 +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 __future__ import annotations
from dataclasses import field
from typing import TypeVar, Union
from pyasic import settings
from pyasic.config.base import MinerConfigOption, MinerConfigValue
from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
DpsHashrateTarget,
DpsPowerTarget,
DpsTarget,
HashrateTargetMode,
PerformanceMode,
Power,
PowerTargetMode,
SaveAction,
SetDpsRequest,
SetPerformanceModeRequest,
TeraHashrate,
TunerPerformanceMode,
)
from .algo import TunerAlgo, TunerAlgoType
from .presets import MiningPreset
from .scaling import ScalingConfig
class MiningModeNormal(MinerConfigValue):
mode: str = field(init=False, default="normal")
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeNormal":
return cls()
def as_am_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "start", "set.miner.power_mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"mode": self.mode}}
def as_epic(self) -> dict:
return {"ptune": {"enabled": False}}
def as_goldshell(self) -> dict:
return {"settings": {"level": 0}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Stock",
}
}
def as_luxos(self) -> dict:
return {"autotunerset": {"enabled": False}}
def as_bosminer(self) -> dict:
return {"autotuning": {"enabled": True}}
class MiningModeSleep(MinerConfigValue):
mode: str = field(init=False, default="sleep")
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeSleep":
return cls()
def as_am_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "1"}
return {"miner-mode": 1}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "1"}
return {"miner-mode": 1}
def as_elphapex(self) -> dict:
return {"miner-mode": 1}
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "stop"}
def as_auradine(self) -> dict:
return {"mode": {"sleep": "on"}}
def as_epic(self) -> dict:
return {"ptune": {"algo": "Sleep", "target": 0}}
def as_goldshell(self) -> dict:
return {"settings": {"level": 3}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Sleep",
}
}
class MiningModeLPM(MinerConfigValue):
mode: str = field(init=False, default="low")
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeLPM":
return cls()
def as_am_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "3"}
return {"miner-mode": 3}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "3"}
return {"miner-mode": 3}
def as_elphapex(self) -> dict:
return {"miner-mode": 3}
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "start", "set.miner.power_mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"mode": "eco"}}
def as_goldshell(self) -> dict:
return {"settings": {"level": 1}}
class MiningModeHPM(MinerConfigValue):
mode: str = field(init=False, default="high")
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHPM":
return cls()
def as_am_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "start", "set.miner.power_mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"mode": "turbo"}}
class MiningModePowerTune(MinerConfigValue):
class Config:
arbitrary_types_allowed = True
mode: str = field(init=False, default="power_tuning")
power: int | None = None
algo: TunerAlgoType = field(default_factory=TunerAlgo.default)
scaling: ScalingConfig | None = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune":
cls_conf = {}
if dict_conf.get("power"):
cls_conf["power"] = dict_conf["power"]
if dict_conf.get("algo"):
cls_conf["algo"] = TunerAlgo.from_dict(dict_conf["algo"])
if dict_conf.get("scaling"):
cls_conf["scaling"] = ScalingConfig.from_dict(dict_conf["scaling"])
return cls(**cls_conf)
def as_am_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_wm(self) -> dict:
if self.power is not None:
return {"mode": self.mode, self.mode: {"wattage": self.power}}
return {}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "start", "set.miner.power_limit": self.power}
def as_bosminer(self) -> dict:
tuning_cfg = {"enabled": True, "mode": "power_target"}
if self.power is not None:
tuning_cfg["power_target"] = self.power
cfg = {"autotuning": tuning_cfg}
if self.scaling is not None:
scaling_cfg = {"enabled": True}
if self.scaling.step is not None:
scaling_cfg["power_step"] = self.scaling.step
if self.scaling.minimum is not None:
scaling_cfg["min_power_target"] = self.scaling.minimum
if self.scaling.shutdown is not None:
scaling_cfg = {**scaling_cfg, **self.scaling.shutdown.as_bosminer()}
cfg["performance_scaling"] = scaling_cfg
return cfg
def as_boser(self) -> dict:
cfg = {
"set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_AND_APPLY,
mode=PerformanceMode(
tuner_mode=TunerPerformanceMode(
power_target=PowerTargetMode(
power_target=Power(watt=self.power)
)
)
),
),
}
if self.scaling is not None:
sd_cfg = {}
if self.scaling.shutdown is not None:
sd_cfg = self.scaling.shutdown.as_boser()
power_target_kwargs = {}
if self.scaling.step is not None:
power_target_kwargs["power_step"] = Power(self.scaling.step)
if self.scaling.minimum is not None:
power_target_kwargs["min_power_target"] = Power(self.scaling.minimum)
cfg["set_dps"] = SetDpsRequest(
save_action=SaveAction.SAVE_AND_APPLY,
enable=True,
**sd_cfg,
target=DpsTarget(power_target=DpsPowerTarget(**power_target_kwargs)),
)
return cfg
def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "power", "power": self.power}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Auto",
"concorde": {
"mode-select": "PowerTarget",
"power-target": self.power,
},
}
}
def as_luxos(self) -> dict:
return {"autotunerset": {"enabled": True}}
class MiningModeHashrateTune(MinerConfigValue):
class Config:
arbitrary_types_allowed = True
mode: str = field(init=False, default="hashrate_tuning")
hashrate: int | None = None
algo: TunerAlgoType = field(default_factory=TunerAlgo.default)
scaling: ScalingConfig | None = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune":
cls_conf = {}
if dict_conf.get("hashrate"):
cls_conf["hashrate"] = dict_conf["hashrate"]
if dict_conf.get("algo"):
cls_conf["algo"] = TunerAlgo.from_dict(dict_conf["algo"])
if dict_conf.get("scaling"):
cls_conf["scaling"] = ScalingConfig.from_dict(dict_conf["scaling"])
return cls(**cls_conf)
def as_am_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_bosminer(self) -> dict:
conf = {"enabled": True, "mode": "hashrate_target"}
if self.hashrate is not None:
conf["hashrate_target"] = self.hashrate
return {"autotuning": conf}
def as_boser(self) -> dict:
cfg = {
"set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_AND_APPLY,
mode=PerformanceMode(
tuner_mode=TunerPerformanceMode(
hashrate_target=HashrateTargetMode(
hashrate_target=TeraHashrate(
terahash_per_second=self.hashrate
)
)
)
),
)
}
if self.scaling is not None:
sd_cfg = {}
if self.scaling.shutdown is not None:
sd_cfg = self.scaling.shutdown.as_boser()
hashrate_target_kwargs = {}
if self.scaling.step is not None:
hashrate_target_kwargs["hashrate_step"] = TeraHashrate(
self.scaling.step
)
if self.scaling.minimum is not None:
hashrate_target_kwargs["min_hashrate_target"] = TeraHashrate(
self.scaling.minimum
)
cfg["set_dps"] = SetDpsRequest(
save_action=SaveAction.SAVE_AND_APPLY,
enable=True,
**sd_cfg,
target=DpsTarget(
hashrate_target=DpsHashrateTarget(**hashrate_target_kwargs)
),
)
return cfg
def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "ths", "ths": self.hashrate}}
def as_epic(self) -> dict:
mode = {
"ptune": {
"algo": self.algo.as_epic(),
"target": self.hashrate,
}
}
if self.scaling is not None:
if self.scaling.minimum is not None:
mode["ptune"]["min_throttle"] = self.scaling.minimum
if self.scaling.step is not None:
mode["ptune"]["throttle_step"] = self.scaling.step
return mode
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Auto",
"concorde": {
"mode-select": "Hashrate",
"hash-target": self.hashrate,
},
}
}
def as_luxos(self) -> dict:
return {"autotunerset": {"enabled": True}}
class MiningModePreset(MinerConfigValue):
mode: str = field(init=False, default="preset")
active_preset: MiningPreset
available_presets: list[MiningPreset] = field(default_factory=list)
def as_vnish(self) -> dict:
return {"overclock": {**self.active_preset.as_vnish()}}
@classmethod
def from_vnish(
cls,
web_overclock_settings: dict,
web_presets: list[dict],
web_perf_summary: dict,
) -> "MiningModePreset":
active_preset = web_perf_summary.get("current_preset")
if active_preset is None:
for preset in web_presets:
if preset["name"] == web_overclock_settings["preset"]:
active_preset = preset
return cls(
active_preset=MiningPreset.from_vnish(active_preset),
available_presets=[MiningPreset.from_vnish(p) for p in web_presets],
)
@classmethod
def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> "MiningModePreset":
active_preset = cls.get_active_preset_from_luxos(rpc_config, rpc_profiles)
return cls(
active_preset=active_preset,
available_presets=[
MiningPreset.from_luxos(p) for p in rpc_profiles["PROFILES"]
],
)
@classmethod
def get_active_preset_from_luxos(
cls, rpc_config: dict, rpc_profiles: dict
) -> MiningPreset:
active_preset = None
active_profile = rpc_config["CONFIG"][0]["Profile"]
for profile in rpc_profiles["PROFILES"]:
if profile["Profile Name"] == active_profile:
active_preset = profile
return MiningPreset.from_luxos(active_preset)
class ManualBoardSettings(MinerConfigValue):
freq: float
volt: float
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "ManualBoardSettings":
return cls(freq=dict_conf["freq"], volt=dict_conf["volt"])
def as_am_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_vnish(self) -> dict:
return {"freq": self.freq}
class MiningModeManual(MinerConfigValue):
mode: str = field(init=False, default="manual")
global_freq: float
global_volt: float
boards: dict[int, ManualBoardSettings] = field(default_factory=dict)
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeManual":
return cls(
global_freq=dict_conf["global_freq"],
global_volt=dict_conf["global_volt"],
boards={i: ManualBoardSettings.from_dict(dict_conf[i]) for i in dict_conf},
)
def as_am_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_vnish(self) -> dict:
chains = [b.as_vnish() for b in self.boards.values() if b.freq != 0]
return {
"overclock": {
"chains": chains if chains != [] else None,
"globals": {
"freq": int(self.global_freq),
"volt": int(self.global_volt),
},
}
}
@classmethod
def from_vnish(cls, web_overclock_settings: dict) -> "MiningModeManual":
# will raise KeyError if it cant find the settings, values cannot be empty
voltage = web_overclock_settings["globals"]["volt"]
freq = web_overclock_settings["globals"]["freq"]
boards = {
idx: ManualBoardSettings(
freq=board["freq"],
volt=voltage if not board["freq"] == 0 else 0,
)
for idx, board in enumerate(web_overclock_settings["chains"])
}
return cls(global_freq=freq, global_volt=voltage, boards=boards)
@classmethod
def from_epic(cls, epic_conf: dict) -> "MiningModeManual":
voltage = 0
freq = 0
if epic_conf.get("HwConfig") is not None:
freq = epic_conf["HwConfig"]["Boards Target Clock"][0]["Data"]
if epic_conf.get("Power Supply Stats") is not None:
voltage = epic_conf["Power Supply Stats"]["Target Voltage"]
boards = {}
if epic_conf.get("HBs") is not None:
boards = {
board["Index"]: ManualBoardSettings(
freq=board["Core Clock Avg"], volt=board["Input Voltage"]
)
for board in epic_conf["HBs"]
}
return cls(global_freq=freq, global_volt=voltage, boards=boards)
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Fixed",
"fixed": {
"frequency": str(self.global_freq),
"voltage": self.global_volt,
},
}
}
class MiningModeConfig(MinerConfigOption):
normal = MiningModeNormal
low = MiningModeLPM
high = MiningModeHPM
sleep = MiningModeSleep
power_tuning = MiningModePowerTune
hashrate_tuning = MiningModeHashrateTune
preset = MiningModePreset
manual = MiningModeManual
@classmethod
def default(cls):
return cls.normal()
@classmethod
def from_dict(cls, dict_conf: dict | None):
if dict_conf is None:
return cls.default()
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
cls_attr = getattr(cls, mode)
if cls_attr is not None:
return cls_attr().from_dict(dict_conf)
@classmethod
def from_am_modern(cls, web_conf: dict):
if web_conf.get("bitmain-work-mode") is not None:
work_mode = web_conf["bitmain-work-mode"]
if work_mode == "":
return cls.default()
if int(work_mode) == 0:
return cls.normal()
elif int(work_mode) == 1:
return cls.sleep()
elif int(work_mode) == 3:
return cls.low()
return cls.default()
@classmethod
def from_hiveon_modern(cls, web_conf: dict):
if web_conf.get("bitmain-work-mode") is not None:
work_mode = web_conf["bitmain-work-mode"]
if work_mode == "":
return cls.default()
if int(work_mode) == 0:
return cls.normal()
elif int(work_mode) == 1:
return cls.sleep()
elif int(work_mode) == 3:
return cls.low()
return cls.default()
@classmethod
def from_elphapex(cls, web_conf: dict):
if web_conf.get("fc-work-mode") is not None:
work_mode = web_conf["fc-work-mode"]
if work_mode == "":
return cls.default()
if int(work_mode) == 0:
return cls.normal()
elif int(work_mode) == 1:
return cls.sleep()
elif int(work_mode) == 3:
return cls.low()
return cls.default()
@classmethod
def from_epic(cls, web_conf: dict):
try:
tuner_running = web_conf["PerpetualTune"]["Running"]
if tuner_running:
algo_info = web_conf["PerpetualTune"]["Algorithm"]
if algo_info.get("VoltageOptimizer") is not None:
scaling_cfg = None
if "Throttle Step" in algo_info["VoltageOptimizer"]:
scaling_cfg = ScalingConfig(
minimum=algo_info["VoltageOptimizer"].get(
"Min Throttle Target"
),
step=algo_info["VoltageOptimizer"].get("Throttle Step"),
)
return cls.hashrate_tuning(
hashrate=algo_info["VoltageOptimizer"].get("Target"),
algo=TunerAlgo.voltage_optimizer(),
scaling=scaling_cfg,
)
elif algo_info.get("BoardTune") is not None:
scaling_cfg = None
if "Throttle Step" in algo_info["BoardTune"]:
scaling_cfg = ScalingConfig(
minimum=algo_info["BoardTune"].get("Min Throttle Target"),
step=algo_info["BoardTune"].get("Throttle Step"),
)
return cls.hashrate_tuning(
hashrate=algo_info["BoardTune"].get("Target"),
algo=TunerAlgo.board_tune(),
scaling=scaling_cfg,
)
else:
return cls.hashrate_tuning(
hashrate=algo_info["ChipTune"].get("Target"),
algo=TunerAlgo.chip_tune(),
)
else:
return MiningModeManual.from_epic(web_conf)
except KeyError:
return cls.default()
@classmethod
def from_bosminer(cls, toml_conf: dict):
if toml_conf.get("autotuning") is None:
return cls.default()
autotuning_conf = toml_conf["autotuning"]
if autotuning_conf.get("enabled") is None:
return cls.default()
if not autotuning_conf["enabled"]:
return cls.default()
if autotuning_conf.get("psu_power_limit") is not None:
# old autotuning conf
return cls.power_tuning(
power=autotuning_conf["psu_power_limit"],
scaling=ScalingConfig.from_bosminer(toml_conf, mode="power"),
)
if autotuning_conf.get("mode") is not None:
# new autotuning conf
mode = autotuning_conf["mode"]
if mode == "power_target":
if autotuning_conf.get("power_target") is not None:
return cls.power_tuning(
power=autotuning_conf["power_target"],
scaling=ScalingConfig.from_bosminer(toml_conf, mode="power"),
)
return cls.power_tuning(
scaling=ScalingConfig.from_bosminer(toml_conf, mode="power"),
)
if mode == "hashrate_target":
if autotuning_conf.get("hashrate_target") is not None:
return cls.hashrate_tuning(
hashrate=autotuning_conf["hashrate_target"],
scaling=ScalingConfig.from_bosminer(toml_conf, mode="hashrate"),
)
return cls.hashrate_tuning(
scaling=ScalingConfig.from_bosminer(toml_conf, mode="hashrate"),
)
return cls.default()
@classmethod
def from_vnish(
cls, web_settings: dict, web_presets: list[dict], web_perf_summary: dict
):
try:
mode_settings = web_settings["miner"]["overclock"]
except KeyError:
return cls.default()
if mode_settings["preset"] == "disabled":
return MiningModeManual.from_vnish(mode_settings)
else:
return MiningModePreset.from_vnish(
mode_settings, web_presets, web_perf_summary
)
@classmethod
def from_boser(cls, grpc_miner_conf: dict):
try:
tuner_conf = grpc_miner_conf["tuner"]
if not tuner_conf.get("enabled", False):
return cls.default()
except LookupError:
return cls.default()
if tuner_conf.get("tunerMode") is not None:
if tuner_conf["tunerMode"] == 1:
if tuner_conf.get("powerTarget") is not None:
return cls.power_tuning(
power=tuner_conf["powerTarget"]["watt"],
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="power"),
)
return cls.power_tuning(
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="power")
)
if tuner_conf["tunerMode"] == 2:
if tuner_conf.get("hashrateTarget") is not None:
return cls.hashrate_tuning(
hashrate=int(tuner_conf["hashrateTarget"]["terahashPerSecond"]),
scaling=ScalingConfig.from_boser(
grpc_miner_conf, mode="hashrate"
),
)
return cls.hashrate_tuning(
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="hashrate"),
)
if tuner_conf.get("powerTarget") is not None:
return cls.power_tuning(
power=tuner_conf["powerTarget"]["watt"],
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="power"),
)
if tuner_conf.get("hashrateTarget") is not None:
return cls.hashrate_tuning(
hashrate=int(tuner_conf["hashrateTarget"]["terahashPerSecond"]),
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="hashrate"),
)
return cls.default()
@classmethod
def from_auradine(cls, web_mode: dict):
try:
mode_data = web_mode["Mode"][0]
if mode_data.get("Sleep") == "on":
return cls.sleep()
if mode_data.get("Mode") == "normal":
return cls.normal()
if mode_data.get("Mode") == "eco":
return cls.low()
if mode_data.get("Mode") == "turbo":
return cls.high()
if mode_data.get("Ths") is not None:
return cls.hashrate_tuning(hashrate=mode_data["Ths"])
if mode_data.get("Power") is not None:
return cls.power_tuning(power=mode_data["Power"])
except LookupError:
return cls.default()
@classmethod
def from_btminer_v3(cls, rpc_device_info: dict, rpc_settings: dict):
try:
is_mining = rpc_device_info["msg"]["miner"]["working"] == "true"
if not is_mining:
return cls.sleep()
power_limit = rpc_settings["msg"]["power-limit"]
if not power_limit == 0:
return cls.power_tuning(power=power_limit)
power_mode = rpc_settings["msg"]["power-mode"]
if power_mode == "normal":
return cls.normal()
if power_mode == "high":
return cls.high()
if power_mode == "low":
return cls.low()
except LookupError:
return cls.default()
@classmethod
def from_mara(cls, web_config: dict):
try:
mode = web_config["mode"]["work-mode-selector"]
if mode == "Fixed":
fixed_conf = web_config["mode"]["fixed"]
return cls.manual(
global_freq=int(fixed_conf["frequency"]),
global_volt=fixed_conf["voltage"],
)
elif mode == "Stock":
return cls.normal()
elif mode == "Sleep":
return cls.sleep()
elif mode == "Auto":
auto_conf = web_config["mode"]["concorde"]
auto_mode = auto_conf["mode-select"]
if auto_mode == "Hashrate":
return cls.hashrate_tuning(hashrate=auto_conf["hash-target"])
elif auto_mode == "PowerTarget":
return cls.power_tuning(power=auto_conf["power-target"])
except LookupError:
pass
return cls.default()
@classmethod
def from_luxos(cls, rpc_config: dict, rpc_profiles: dict):
preset_info = MiningModePreset.from_luxos(rpc_config, rpc_profiles)
return cls.preset(
active_preset=preset_info.active_preset,
available_presets=preset_info.available_presets,
)
MiningMode = TypeVar(
"MiningMode",
bound=Union[
MiningModeNormal,
MiningModeHPM,
MiningModeLPM,
MiningModeSleep,
MiningModeManual,
MiningModePowerTune,
MiningModeHashrateTune,
MiningModePreset,
],
)

View File

@@ -1,66 +0,0 @@
from __future__ import annotations
from dataclasses import field
from typing import TypeVar, Union
from pyasic.config.base import MinerConfigOption, MinerConfigValue
class StandardTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard")
def as_epic(self) -> str:
return VOptAlgo().as_epic()
class VOptAlgo(MinerConfigValue):
mode: str = field(init=False, default="voltage_optimizer")
def as_epic(self) -> str:
return "VoltageOptimizer"
class BoardTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="board_tune")
def as_epic(self) -> str:
return "BoardTune"
class ChipTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="chip_tune")
def as_epic(self) -> str:
return "ChipTune"
class TunerAlgo(MinerConfigOption):
standard = StandardTuneAlgo
voltage_optimizer = VOptAlgo
board_tune = BoardTuneAlgo
chip_tune = ChipTuneAlgo
@classmethod
def default(cls) -> TunerAlgoType:
return cls.standard()
@classmethod
def from_dict(cls, dict_conf: dict | None) -> TunerAlgoType:
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
cls_attr = getattr(cls, mode)
if cls_attr is not None:
return cls_attr().from_dict(dict_conf)
TunerAlgoType = TypeVar(
"TunerAlgoType",
bound=Union[
StandardTuneAlgo,
VOptAlgo,
BoardTuneAlgo,
ChipTuneAlgo,
],
)

View File

@@ -1,54 +0,0 @@
from pyasic.config.base import MinerConfigValue
class MiningPreset(MinerConfigValue):
name: str | None = None
power: int | None = None
hashrate: int | None = None
tuned: bool | None = None
modded_psu: bool | None = None
frequency: int | None = None
voltage: float | None = None
def as_vnish(self) -> dict:
if self.name is not None:
return {"preset": self.name}
return {}
@classmethod
def from_vnish(cls, web_preset: dict):
name = web_preset["name"]
hr_power_split = web_preset["pretty"].split("~")
if len(hr_power_split) == 1:
power = None
hashrate = None
else:
power = hr_power_split[0].replace("watt", "").strip()
hashrate = (
hr_power_split[1]
.replace("TH", "")
.replace("GH", "")
.replace("MH", "")
.replace(" LC", "")
.strip()
)
tuned = web_preset["status"] == "tuned"
modded_psu = web_preset["modded_psu_required"]
return cls(
name=name,
power=power,
hashrate=hashrate,
tuned=tuned,
modded_psu=modded_psu,
)
@classmethod
def from_luxos(cls, profile: dict):
return cls(
name=profile["Profile Name"],
power=profile["Watts"],
hashrate=round(profile["Hashrate"]),
tuned=profile["IsTuned"],
frequency=profile["Frequency"],
voltage=profile["Voltage"],
)

View File

@@ -1,129 +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 __future__ import annotations
from pyasic.config.base import MinerConfigValue
class ScalingShutdown(MinerConfigValue):
enabled: bool = False
duration: int | None = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "ScalingShutdown":
return cls(
enabled=dict_conf.get("enabled", False), duration=dict_conf.get("duration")
)
@classmethod
def from_bosminer(cls, power_scaling_conf: dict):
sd_enabled = power_scaling_conf.get("shutdown_enabled")
if sd_enabled is not None:
return cls(
enabled=sd_enabled, duration=power_scaling_conf.get("shutdown_duration")
)
return None
@classmethod
def from_boser(cls, power_scaling_conf: dict):
sd_enabled = power_scaling_conf.get("shutdownEnabled")
if sd_enabled is not None:
try:
return cls(
enabled=sd_enabled,
duration=power_scaling_conf["shutdownDuration"]["hours"],
)
except KeyError:
return cls(enabled=sd_enabled)
return None
def as_bosminer(self) -> dict:
cfg = {"shutdown_enabled": self.enabled}
if self.duration is not None:
cfg["shutdown_duration"] = self.duration
return cfg
def as_boser(self) -> dict:
return {"enable_shutdown": self.enabled, "shutdown_duration": self.duration}
class ScalingConfig(MinerConfigValue):
step: int | None = None
minimum: int | None = None
shutdown: ScalingShutdown | None = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "ScalingConfig":
cls_conf = {
"step": dict_conf.get("step"),
"minimum": dict_conf.get("minimum"),
}
shutdown = dict_conf.get("shutdown")
if shutdown is not None:
cls_conf["shutdown"] = ScalingShutdown.from_dict(shutdown)
return cls(**cls_conf)
@classmethod
def from_bosminer(cls, toml_conf: dict, mode: str | None = None):
if mode == "power":
return cls._from_bosminer_power(toml_conf)
if mode == "hashrate":
# not implemented yet
pass
@classmethod
def _from_bosminer_power(cls, toml_conf: dict):
power_scaling = toml_conf.get("power_scaling")
if power_scaling is None:
power_scaling = toml_conf.get("performance_scaling")
if power_scaling is not None:
enabled = power_scaling.get("enabled")
if not enabled:
return None
power_step = power_scaling.get("power_step")
min_power = power_scaling.get("min_psu_power_limit")
if min_power is None:
min_power = power_scaling.get("min_power_target")
sd_mode = ScalingShutdown.from_bosminer(power_scaling)
return cls(step=power_step, minimum=min_power, shutdown=sd_mode)
@classmethod
def from_boser(cls, grpc_miner_conf: dict, mode: str | None = None):
if mode == "power":
return cls._from_boser_power(grpc_miner_conf)
if mode == "hashrate":
# not implemented yet
pass
@classmethod
def _from_boser_power(cls, grpc_miner_conf: dict):
try:
dps_conf = grpc_miner_conf["dps"]
if not dps_conf.get("enabled", False):
return None
except LookupError:
return None
conf = {"shutdown": ScalingShutdown.from_boser(dps_conf)}
if dps_conf.get("minPowerTarget") is not None:
conf["minimum"] = dps_conf["minPowerTarget"]["watt"]
if dps_conf.get("powerStep") is not None:
conf["step"] = dps_conf["powerStep"]["watt"]
return cls(**conf)

View File

@@ -1,737 +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 __future__ import annotations
import random
import string
from typing import List
from pydantic import Field
from pyasic.config.base import MinerConfigValue
from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
PoolConfiguration,
PoolGroupConfiguration,
Quota,
SaveAction,
SetPoolGroupsRequest,
)
class Pool(MinerConfigValue):
url: str
user: str
password: str
def as_am_modern(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"pass": self.password,
}
def as_hiveon_modern(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"pass": self.password,
}
def as_elphapex(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"pass": self.password,
}
def as_wm(self, idx: int = 1, user_suffix: str | None = None) -> dict:
return {
f"pool_{idx}": self.url,
f"worker_{idx}": f"{self.user}{user_suffix or ''}",
f"passwd_{idx}": self.password,
}
def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
return {
f"pool": self.url,
f"worker": f"{self.user}{user_suffix or ''}",
f"passwd": self.password,
}
def as_am_old(self, idx: int = 1, user_suffix: str | None = None) -> dict:
return {
f"_ant_pool{idx}url": self.url,
f"_ant_pool{idx}user": f"{self.user}{user_suffix or ''}",
f"_ant_pool{idx}pw": self.password,
}
def as_goldshell(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"pass": self.password,
}
def as_avalon(self, user_suffix: str | None = None) -> str:
return ",".join([self.url, f"{self.user}{user_suffix or ''}", self.password])
def as_inno(self, idx: int = 1, user_suffix: str | None = None) -> dict:
return {
f"Pool{idx}": self.url,
f"UserName{idx}": f"{self.user}{user_suffix or ''}",
f"Password{idx}": self.password,
}
def as_bosminer(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"password": self.password,
}
def as_auradine(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"pass": self.password,
}
def as_epic(self, user_suffix: str | None = None):
return {
"pool": self.url,
"login": f"{self.user}{user_suffix or ''}",
"password": self.password,
}
def as_mara(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"pass": self.password,
}
def as_espminer(self, user_suffix: str | None = None) -> dict:
return {
"stratumURL": self.url,
"stratumUser": f"{self.user}{user_suffix or ''}",
"stratumPassword": self.password,
}
def as_boser(self, user_suffix: str | None = None) -> PoolConfiguration:
return PoolConfiguration(
url=self.url,
user=f"{self.user}{user_suffix or ''}",
password=self.password,
enabled=True,
)
def as_vnish(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"pass": self.password,
}
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "Pool":
return cls(
url=dict_conf["url"], user=dict_conf["user"], password=dict_conf["password"]
)
@classmethod
def from_api(cls, api_pool: dict) -> "Pool":
return cls(url=api_pool["URL"], user=api_pool["User"], password="x")
@classmethod
def from_btminer_v3(cls, api_pool: dict) -> "Pool":
return cls(url=api_pool["url"], user=api_pool["account"], password="x")
@classmethod
def from_epic(cls, api_pool: dict) -> "Pool":
return cls(
url=api_pool["pool"], user=api_pool["login"], password=api_pool["password"]
)
@classmethod
def from_am_modern(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_hiveon_modern(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_elphapex(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
# TODO: check if this is accurate, user/username, pass/password
@classmethod
def from_goldshell(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_inno(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_bosminer(cls, toml_pool_conf: dict) -> "Pool":
return cls(
url=toml_pool_conf["url"],
user=toml_pool_conf["user"],
password=toml_pool_conf.get("password", ""),
)
@classmethod
def from_vnish(cls, web_pool: dict) -> "Pool":
return cls(
url="stratum+tcp://" + web_pool["url"],
user=web_pool["user"],
password=web_pool["pass"],
)
@classmethod
def from_boser(cls, grpc_pool: dict) -> "Pool":
return cls(
url=grpc_pool["url"],
user=grpc_pool["user"],
password=grpc_pool["password"],
)
@classmethod
def from_mara(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"],
user=web_pool["user"],
password=web_pool["pass"],
)
@classmethod
def from_espminer(cls, web_system_info: dict) -> "Pool":
url = f"stratum+tcp://{web_system_info['stratumURL']}:{web_system_info['stratumPort']}"
return cls(
url=url,
user=web_system_info["stratumUser"],
password=web_system_info.get("stratumPassword", ""),
)
@classmethod
def from_luxos(cls, rpc_pools: dict) -> "Pool":
return cls.from_api(rpc_pools)
@classmethod
def from_iceriver(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["addr"],
user=web_pool["user"],
password=web_pool["pass"],
)
class PoolGroup(MinerConfigValue):
pools: list[Pool] = Field(default_factory=list)
quota: int = 1
name: str | None = None
def __post_init__(self):
if self.name is None:
self.name = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(6)
) # generate random pool group name in case it isn't set
def as_am_modern(self, user_suffix: str | None = None) -> list:
pools = []
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.append(self.pools[idx].as_am_modern(user_suffix=user_suffix))
else:
pools.append(Pool(url="", user="", password="").as_am_modern())
idx += 1
return pools
def as_hiveon_modern(self, user_suffix: str | None = None) -> list:
pools = []
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.append(self.pools[idx].as_hiveon_modern(user_suffix=user_suffix))
else:
pools.append(Pool(url="", user="", password="").as_hiveon_modern())
idx += 1
return pools
def as_elphapex(self, user_suffix: str | None = None) -> list:
pools = []
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.append(self.pools[idx].as_elphapex(user_suffix=user_suffix))
else:
pools.append(Pool(url="", user="", password="").as_elphapex())
idx += 1
return pools
def as_wm(self, user_suffix: str | None = None) -> dict:
pools = {}
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.update(
**self.pools[idx].as_wm(idx=idx + 1, user_suffix=user_suffix)
)
else:
pools.update(**Pool(url="", user="", password="").as_wm(idx=idx + 1))
idx += 1
return pools
def as_btminer_v3(self, user_suffix: str | None = None) -> list:
return [pool.as_btminer_v3(user_suffix) for pool in self.pools[:3]]
def as_am_old(self, user_suffix: str | None = None) -> dict:
pools = {}
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.update(
**self.pools[idx].as_am_old(idx=idx + 1, user_suffix=user_suffix)
)
else:
pools.update(
**Pool(url="", user="", password="").as_am_old(idx=idx + 1)
)
idx += 1
return pools
def as_goldshell(self, user_suffix: str | None = None) -> list:
return [pool.as_goldshell(user_suffix) for pool in self.pools]
def as_avalon(self, user_suffix: str | None = None) -> str:
if len(self.pools) > 0:
return self.pools[0].as_avalon(user_suffix=user_suffix)
return Pool(url="", user="", password="").as_avalon()
def as_inno(self, user_suffix: str | None = None) -> dict:
pools = {}
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.update(
**self.pools[idx].as_inno(idx=idx + 1, user_suffix=user_suffix)
)
else:
pools.update(**Pool(url="", user="", password="").as_inno(idx=idx + 1))
idx += 1
return pools
def as_bosminer(self, user_suffix: str | None = None) -> dict:
if len(self.pools) > 0:
conf = {
"name": self.name,
"pool": [
pool.as_bosminer(user_suffix=user_suffix) for pool in self.pools
],
}
if self.quota is not None:
conf["quota"] = self.quota
return conf
return {"name": "Group", "pool": []}
def as_auradine(self, user_suffix: str | None = None) -> list:
return [p.as_auradine(user_suffix=user_suffix) for p in self.pools]
def as_epic(self, user_suffix: str | None = None) -> list:
return [p.as_epic(user_suffix=user_suffix) for p in self.pools]
def as_mara(self, user_suffix: str | None = None) -> list:
return [p.as_mara(user_suffix=user_suffix) for p in self.pools]
def as_espminer(self, user_suffix: str | None = None) -> dict:
return self.pools[0].as_espminer(user_suffix=user_suffix)
def as_boser(self, user_suffix: str | None = None) -> PoolGroupConfiguration:
return PoolGroupConfiguration(
name=self.name,
quota=Quota(value=self.quota),
pools=[p.as_boser() for p in self.pools],
)
def as_vnish(self, user_suffix: str | None = None) -> dict:
return {"pools": [p.as_vnish(user_suffix=user_suffix) for p in self.pools]}
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "PoolGroup":
cls_conf = {}
if dict_conf.get("quota") is not None:
cls_conf["quota"] = dict_conf["quota"]
if dict_conf.get("name") is not None:
cls_conf["name"] = dict_conf["name"]
cls_conf["pools"] = [Pool.from_dict(p) for p in dict_conf["pools"]]
return cls(**cls_conf)
@classmethod
def from_api(cls, api_pool_list: list) -> "PoolGroup":
pools = []
for pool in api_pool_list:
pools.append(Pool.from_api(pool))
return cls(pools=pools)
@classmethod
def from_btminer_v3(cls, api_pool_list: list) -> "PoolGroup":
pools = []
for pool in api_pool_list:
pools.append(Pool.from_btminer_v3(pool))
return cls(pools=pools)
@classmethod
def from_epic(cls, api_pool_list: list) -> "PoolGroup":
pools = []
for pool in api_pool_list:
pools.append(Pool.from_epic(pool))
return cls(pools=pools)
@classmethod
def from_am_modern(cls, web_pool_list: list) -> "PoolGroup":
pools = []
for pool in web_pool_list:
pools.append(Pool.from_am_modern(pool))
return cls(pools=pools)
@classmethod
def from_hiveon_modern(cls, web_pool_list: list) -> "PoolGroup":
pools = []
for pool in web_pool_list:
pools.append(Pool.from_hiveon_modern(pool))
return cls(pools=pools)
@classmethod
def from_elphapex(cls, web_pool_list: list) -> "PoolGroup":
pools = []
for pool in web_pool_list:
pools.append(Pool.from_elphapex(pool))
return cls(pools=pools)
@classmethod
def from_goldshell(cls, web_pools: list) -> "PoolGroup":
return cls(pools=[Pool.from_goldshell(p) for p in web_pools])
@classmethod
def from_inno(cls, web_pools: list) -> "PoolGroup":
return cls(pools=[Pool.from_inno(p) for p in web_pools])
@classmethod
def from_bosminer(cls, toml_group_conf: dict) -> "PoolGroup":
if toml_group_conf.get("pool") is not None:
return cls(
name=toml_group_conf["name"],
quota=toml_group_conf.get("quota", 1),
pools=[Pool.from_bosminer(p) for p in toml_group_conf["pool"]],
)
return cls()
@classmethod
def from_vnish(cls, web_settings_pools: dict) -> "PoolGroup":
return cls(
pools=[Pool.from_vnish(p) for p in web_settings_pools if p["url"] != ""]
)
@classmethod
def from_boser(cls, grpc_pool_group: dict) -> "PoolGroup":
try:
return cls(
pools=[Pool.from_boser(p) for p in grpc_pool_group["pools"]],
name=grpc_pool_group["name"],
quota=(
grpc_pool_group["quota"]["value"]
if grpc_pool_group.get("quota") is not None
else 1
),
)
except LookupError:
return cls()
@classmethod
def from_mara(cls, web_config_pools: dict) -> "PoolGroup":
return cls(pools=[Pool.from_mara(pool_conf) for pool_conf in web_config_pools])
@classmethod
def from_espminer(cls, web_system_info: dict) -> "PoolGroup":
return cls(pools=[Pool.from_espminer(web_system_info)])
@classmethod
def from_iceriver(cls, web_userpanel: dict) -> "PoolGroup":
return cls(
pools=[
Pool.from_iceriver(web_pool)
for web_pool in web_userpanel["data"]["pools"]
]
)
class PoolConfig(MinerConfigValue):
groups: List[PoolGroup] = Field(default_factory=list)
@classmethod
def default(cls) -> "PoolConfig":
return cls()
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "PoolConfig":
if dict_conf is None:
return cls.default()
return cls(groups=[PoolGroup.from_dict(g) for g in dict_conf["groups"]])
@classmethod
def simple(cls, pools: list[Pool | dict[str, str]]) -> "PoolConfig":
group_pools = []
for pool in pools:
if isinstance(pool, dict):
pool = Pool(**pool)
group_pools.append(pool)
return cls(groups=[PoolGroup(pools=group_pools)])
def as_am_modern(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_am_modern(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_am_modern()}
def as_hiveon_modern(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_hiveon_modern(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_hiveon_modern()}
def as_elphapex(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_elphapex(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_elphapex()}
def as_wm(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_wm()}
def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_btminer_v3(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_btminer_v3()}
def as_am_old(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return self.groups[0].as_am_old(user_suffix=user_suffix)
return PoolGroup().as_am_old()
def as_goldshell(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_goldshell(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_goldshell()}
def as_avalon(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_avalon(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_avalon()}
def as_inno(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return self.groups[0].as_inno(user_suffix=user_suffix)
return PoolGroup().as_inno()
def as_bosminer(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {
"group": [g.as_bosminer(user_suffix=user_suffix) for g in self.groups]
}
return {"group": [PoolGroup().as_bosminer()]}
def as_boser(self, user_suffix: str | None = None) -> dict:
return {
"set_pool_groups": SetPoolGroupsRequest(
save_action=SaveAction.SAVE_AND_APPLY,
pool_groups=[g.as_boser(user_suffix=user_suffix) for g in self.groups],
)
}
def as_auradine(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {
"updatepools": {
"pools": self.groups[0].as_auradine(user_suffix=user_suffix)
}
}
return {"updatepools": {"pools": PoolGroup().as_auradine()}}
def as_epic(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {
"pools": {
"coin": "Btc",
"stratum_configs": self.groups[0].as_epic(user_suffix=user_suffix),
"unique_id": False,
}
}
return {
"pools": {
"coin": "Btc",
"stratum_configs": [PoolGroup().as_epic()],
"unique_id": False,
}
}
def as_mara(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_mara(user_suffix=user_suffix)}
return {"pools": []}
def as_espminer(self, user_suffix: str | None = None) -> dict:
return self.groups[0].as_espminer(user_suffix=user_suffix)
def as_luxos(self, user_suffix: str | None = None) -> dict:
return {}
def as_vnish(self, user_suffix: str | None = None) -> dict:
return self.groups[0].as_vnish(user_suffix=user_suffix)
@classmethod
def from_api(cls, api_pools: dict) -> "PoolConfig":
try:
pool_data = api_pools["POOLS"]
except KeyError:
return PoolConfig.default()
pool_data = sorted(pool_data, key=lambda x: int(x["POOL"]))
return cls(groups=[PoolGroup.from_api(pool_data)])
@classmethod
def from_btminer_v3(cls, rpc_pools: dict) -> "PoolConfig":
try:
pool_data = rpc_pools["pools"]
except KeyError:
return PoolConfig.default()
pool_data = sorted(pool_data, key=lambda x: int(x["id"]))
return cls(groups=[PoolGroup.from_btminer_v3(pool_data)])
@classmethod
def from_epic(cls, web_conf: dict) -> "PoolConfig":
pool_data = web_conf["StratumConfigs"]
return cls(groups=[PoolGroup.from_epic(pool_data)])
@classmethod
def from_am_modern(cls, web_conf: dict) -> "PoolConfig":
try:
pool_data = web_conf["pools"]
except KeyError:
return cls(groups=[])
return cls(groups=[PoolGroup.from_am_modern(pool_data)])
@classmethod
def from_hiveon_modern(cls, web_conf: dict) -> "PoolConfig":
try:
pool_data = web_conf["pools"]
except KeyError:
return cls(groups=[])
return cls(groups=[PoolGroup.from_hiveon_modern(pool_data)])
@classmethod
def from_elphapex(cls, web_conf: dict) -> "PoolConfig":
pool_data = web_conf["pools"]
return cls(groups=[PoolGroup.from_elphapex(pool_data)])
@classmethod
def from_goldshell(cls, web_pools: list) -> "PoolConfig":
return cls(groups=[PoolGroup.from_goldshell(web_pools)])
@classmethod
def from_goldshell_byte(cls, web_pools: list) -> "PoolConfig":
return cls(
groups=[
PoolGroup.from_goldshell(g["pools"])
for g in web_pools
if len(g["pools"]) > 0
]
)
@classmethod
def from_inno(cls, web_pools: list) -> "PoolConfig":
return cls(groups=[PoolGroup.from_inno(web_pools)])
@classmethod
def from_bosminer(cls, toml_conf: dict) -> "PoolConfig":
if toml_conf.get("group") is None:
return cls()
return cls(groups=[PoolGroup.from_bosminer(g) for g in toml_conf["group"]])
@classmethod
def from_vnish(cls, web_settings: dict) -> "PoolConfig":
try:
return cls(groups=[PoolGroup.from_vnish(web_settings["miner"]["pools"])])
except LookupError:
return cls()
@classmethod
def from_boser(cls, grpc_miner_conf: dict) -> "PoolConfig":
try:
return cls(
groups=[
PoolGroup.from_boser(group)
for group in grpc_miner_conf["poolGroups"]
]
)
except LookupError:
return cls()
@classmethod
def from_mara(cls, web_config: dict) -> "PoolConfig":
return cls(groups=[PoolGroup.from_mara(web_config["pools"])])
@classmethod
def from_espminer(cls, web_system_info: dict) -> "PoolConfig":
return cls(groups=[PoolGroup.from_espminer(web_system_info)])
@classmethod
def from_iceriver(cls, web_userpanel: dict) -> "PoolConfig":
return cls(groups=[PoolGroup.from_iceriver(web_userpanel)])
@classmethod
def from_luxos(cls, rpc_groups: dict, rpc_pools: dict) -> "PoolConfig":
return cls(
groups=[
PoolGroup(
pools=[
Pool.from_luxos(pool)
for pool in rpc_pools["POOLS"]
if pool["GROUP"] == group["GROUP"]
],
name=group["Name"],
quota=group["Quota"],
)
for group in rpc_groups["GROUPS"]
]
)

View File

@@ -1,155 +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 __future__ import annotations
from pyasic.config.base import MinerConfigValue
class TemperatureConfig(MinerConfigValue):
target: int | None = None
hot: int | None = None
danger: int | None = None
@classmethod
def default(cls):
return cls()
def as_bosminer(self) -> dict:
temp_cfg = {}
if self.target is not None:
temp_cfg["target_temp"] = self.target
if self.hot is not None:
temp_cfg["hot_temp"] = self.hot
if self.danger is not None:
temp_cfg["dangerous_temp"] = self.danger
if len(temp_cfg) == 0:
return {}
return {"temp_control": temp_cfg}
def as_epic(self) -> dict:
temps_config = {"temps": {}, "fans": {"Auto": {}}}
if self.target is not None:
temps_config["fans"]["Auto"]["Target Temperature"] = self.target
else:
temps_config["fans"]["Auto"]["Target Temperature"] = 60
if self.danger is not None:
temps_config["temps"]["critical"] = self.danger
if self.hot is not None:
temps_config["temps"]["shutdown"] = self.hot
return temps_config
def as_luxos(self) -> dict:
return {"tempctrlset": [self.target or "", self.hot or "", self.danger or ""]}
def as_vnish(self) -> dict:
return {"misc": {"restart_temp": self.danger}}
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "TemperatureConfig":
return cls(
target=dict_conf.get("target"),
hot=dict_conf.get("hot"),
danger=dict_conf.get("danger"),
)
@classmethod
def from_bosminer(cls, toml_conf: dict) -> "TemperatureConfig":
temp_control = toml_conf.get("temp_control")
if temp_control is not None:
return cls(
target=temp_control.get("target_temp"),
hot=temp_control.get("hot_temp"),
danger=temp_control.get("dangerous_temp"),
)
return cls()
@classmethod
def from_epic(cls, web_conf: dict) -> "TemperatureConfig":
try:
dangerous_temp = web_conf["Misc"]["Critical Temp"]
except KeyError:
dangerous_temp = None
try:
hot_temp = web_conf["Misc"]["Shutdown Temp"]
except KeyError:
hot_temp = None
# Need to do this in two blocks to avoid KeyError if one is missing
try:
target_temp = web_conf["Fans"]["Fan Mode"]["Auto"]["Target Temperature"]
except KeyError:
target_temp = None
return cls(target=target_temp, hot=hot_temp, danger=dangerous_temp)
@classmethod
def from_vnish(cls, web_settings: dict) -> "TemperatureConfig":
try:
dangerous_temp = web_settings["misc"]["restart_temp"]
except KeyError:
dangerous_temp = None
try:
if web_settings["miner"]["cooling"]["mode"]["name"] == "auto":
return cls(
target=web_settings["miner"]["cooling"]["mode"]["param"],
danger=dangerous_temp,
)
except KeyError:
pass
return cls()
@classmethod
def from_boser(cls, grpc_miner_conf: dict) -> "TemperatureConfig":
try:
temperature_conf = grpc_miner_conf["temperature"]
except KeyError:
return cls.default()
root_key = None
for key in ["auto", "manual", "disabled"]:
if key in temperature_conf.keys():
root_key = key
break
if root_key is None:
return cls.default()
conf = {}
keys = temperature_conf[root_key].keys()
if "targetTemperature" in keys:
conf["target"] = int(
temperature_conf[root_key]["targetTemperature"]["degreeC"]
)
if "hotTemperature" in keys:
conf["hot"] = int(temperature_conf[root_key]["hotTemperature"]["degreeC"])
if "dangerousTemperature" in keys:
conf["danger"] = int(
temperature_conf[root_key]["dangerousTemperature"]["degreeC"]
)
return cls(**conf)
return cls.default()
@classmethod
def from_luxos(cls, rpc_tempctrl: dict) -> "TemperatureConfig":
try:
tempctrl_config = rpc_tempctrl["TEMPCTRL"][0]
return cls(
target=tempctrl_config.get("Target"),
hot=tempctrl_config.get("Hot"),
danger=tempctrl_config.get("Dangerous"),
)
except LookupError:
pass
return cls.default()

View File

@@ -13,138 +13,163 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import copy
import json
import logging
import time
from dataclasses import asdict, dataclass, field, fields
from datetime import datetime, timezone
from typing import Any
from typing import List, Union
from pydantic import BaseModel, Field, computed_field
from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune
from pyasic.data.pools import PoolMetrics, Scheme
from pyasic.device.algorithm.hashrate import AlgoHashRateType
from .boards import HashBoard
from .device import DeviceInfo
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
from .error_codes.base import BaseMinerError
from .fans import Fan
class MinerData(BaseModel):
@dataclass
class HashBoard:
"""A Dataclass to standardize hashboard data.
Attributes:
slot: The slot of the board as an int.
hashrate: The hashrate of the board in TH/s as a float.
temp: The temperature of the PCB as an int.
chip_temp: The temperature of the chips as an int.
chips: The chip count of the board as an int.
expected_chips: The ideal chip count of the board as an int.
missing: Whether the board is returned from the miners data as a bool.
"""
slot: int = 0
hashrate: float = 0.0
temp: int = -1
chip_temp: int = -1
chips: int = 0
expected_chips: int = 0
missing: bool = True
@dataclass
class Fan:
"""A Dataclass to standardize fan data.
Attributes:
speed: The speed of the fan.
"""
speed: int = -1
@dataclass
class MinerData:
"""A Dataclass to standardize data returned from miners (specifically `AnyMiner().get_data()`)
Attributes:
ip: The IP of the miner as a str.
datetime: The time and date this data was generated.
uptime: The uptime of the miner in seconds.
mac: The MAC address of the miner as a str.
device_info: Info about the device, such as model, make, and firmware.
model: The model of the miner as a str.
make: The make of the miner as a str.
firmware: The firmware on the miner as a str.
algo: The mining algorithm of the miner as a str.
api_ver: The current api version on the miner as a str.
fw_ver: The current firmware version on the miner as a str.
hostname: The network hostname of the miner as a str.
hashrate: The hashrate of the miner in TH/s as a float. Calculated automatically.
expected_hashrate: The factory nominal hashrate of the miner in TH/s as a float.
sticker_hashrate: The factory sticker hashrate of the miner as a float.
hashboards: A list of [`HashBoard`][pyasic.data.HashBoard]s on the miner with their statistics.
hashrate: The hashrate of the miner in TH/s as a float.
nominal_hashrate: The factory nominal hashrate of the miner in TH/s as a float.
left_board_hashrate: The hashrate of the left board of the miner in TH/s as a float.
center_board_hashrate: The hashrate of the center board of the miner in TH/s as a float.
right_board_hashrate: The hashrate of the right board of the miner in TH/s as a float.
temperature_avg: The average temperature across the boards. Calculated automatically.
env_temp: The environment temps as a float.
left_board_temp: The temp of the left PCB as an int.
left_board_chip_temp: The temp of the left board chips as an int.
center_board_temp: The temp of the center PCB as an int.
center_board_chip_temp: The temp of the center board chips as an int.
right_board_temp: The temp of the right PCB as an int.
right_board_chip_temp: The temp of the right board chips as an int.
wattage: Current power draw of the miner as an int.
voltage: Current output voltage of the PSU as an float.
wattage_limit: Power limit of the miner as an int.
fans: A list of fans on the miner with their speeds.
expected_fans: The number of fans expected on a miner.
fan_1: The speed of the first fan as an int.
fan_2: The speed of the second fan as an int.
fan_3: The speed of the third fan as an int.
fan_4: The speed of the fourth fan as an int.
fan_psu: The speed of the PSU on the fan if the miner collects it.
left_chips: The number of chips online in the left board as an int.
center_chips: The number of chips online in the left board as an int.
right_chips: The number of chips online in the left board as an int.
total_chips: The total number of chips on all boards. Calculated automatically.
expected_chips: The expected number of chips in the miner as an int.
percent_expected_chips: The percent of total chips out of the expected count. Calculated automatically.
percent_expected_hashrate: The percent of total hashrate out of the expected hashrate. Calculated automatically.
percent_expected_wattage: The percent of total wattage out of the expected wattage. Calculated automatically.
ideal_chips: The ideal number of chips in the miner as an int.
percent_ideal_chips: The percent of total chips out of the ideal count. Calculated automatically.
percent_ideal_hashrate: The percent of total hashrate out of the ideal hashrate. Calculated automatically.
percent_ideal_wattage: The percent of total wattage out of the ideal wattage. Calculated automatically.
nominal: Whether the number of chips in the miner is nominal. Calculated automatically.
config: The parsed config of the miner, using [`MinerConfig`][pyasic.config.MinerConfig].
pool_split: The pool split as a str.
pool_1_url: The first pool url on the miner as a str.
pool_1_user: The first pool user on the miner as a str.
pool_2_url: The second pool url on the miner as a str.
pool_2_user: The second pool user on the miner as a str.
errors: A list of errors on the miner.
fault_light: Whether 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.
efficiency_fract: Same as efficiency, but is not rounded to integer. Calculated automatically.
is_mining: Whether the miner is mining.
pools: A list of PoolMetrics instances, each representing metrics for a pool.
"""
# general
ip: str
raw_datetime: datetime = Field(
exclude=True, default_factory=datetime.now(timezone.utc).astimezone, repr=False
)
# about
device_info: DeviceInfo | None = None
mac: str | None = None
api_ver: str | None = None
fw_ver: str | None = None
hostname: str | None = None
# hashrate
raw_hashrate: AlgoHashRateType = Field(exclude=True, default=None, repr=False)
# sticker
sticker_hashrate: AlgoHashRateType | None = None
# expected
expected_hashrate: AlgoHashRateType | None = None
expected_hashboards: int | None = None
expected_chips: int | None = None
expected_fans: int | None = None
# temperature
env_temp: float | None = None
# power
wattage: int | None = None
voltage: float | None = None
raw_wattage_limit: int | None = Field(exclude=True, default=None, repr=False)
# fans
fans: list[Fan] = Field(default_factory=list)
fan_psu: int | None = None
# boards
hashboards: list[HashBoard] = Field(default_factory=list)
# config
config: MinerConfig | None = None
fault_light: bool | None = None
# errors
errors: list[BaseMinerError] = Field(default_factory=list)
# mining state
is_mining: bool = True
uptime: int | None = None
# pools
pools: list[PoolMetrics] = Field(default_factory=list)
datetime: datetime = None
mac: str = "00:00:00:00:00:00"
model: str = "Unknown"
make: str = "Unknown"
api_ver: str = "Unknown"
fw_ver: str = "Unknown"
hostname: str = "Unknown"
hashrate: float = field(init=False)
_hashrate: float = 0
nominal_hashrate: float = 0
hashboards: List[HashBoard] = field(default_factory=list)
ideal_hashboards: int = 1
left_board_hashrate: float = field(init=False)
center_board_hashrate: float = field(init=False)
right_board_hashrate: float = field(init=False)
temperature_avg: int = field(init=False)
env_temp: float = -1.0
left_board_temp: int = field(init=False)
left_board_chip_temp: int = field(init=False)
center_board_temp: int = field(init=False)
center_board_chip_temp: int = field(init=False)
right_board_temp: int = field(init=False)
right_board_chip_temp: int = field(init=False)
wattage: int = -1
wattage_limit: int = -1
fans: List[Fan] = field(default_factory=list)
fan_1: int = field(init=False)
fan_2: int = field(init=False)
fan_3: int = field(init=False)
fan_4: int = field(init=False)
fan_psu: int = -1
left_chips: int = field(init=False)
center_chips: int = field(init=False)
right_chips: int = field(init=False)
total_chips: int = field(init=False)
ideal_chips: int = 1
percent_ideal_chips: float = field(init=False)
percent_ideal_hashrate: float = field(init=False)
percent_ideal_wattage: float = field(init=False)
nominal: int = field(init=False)
pool_split: str = "0"
pool_1_url: str = "Unknown"
pool_1_user: str = "Unknown"
pool_2_url: str = ""
pool_2_user: str = ""
errors: List[
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
] = field(default_factory=list)
fault_light: Union[bool, None] = None
efficiency: int = field(init=False)
@classmethod
def fields(cls) -> set:
all_fields = set(cls.model_fields.keys())
all_fields.update(set(cls.model_computed_fields.keys()))
return all_fields
def fields(cls):
return [f.name for f in fields(cls)]
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __post_init__(self):
self.datetime = datetime.now(timezone.utc).astimezone()
def __getitem__(self, item: str):
def __getitem__(self, item):
try:
return getattr(self, item)
except AttributeError:
@@ -161,19 +186,19 @@ class MinerData(BaseModel):
def __floordiv__(self, other):
cp = copy.deepcopy(self)
for key in self.fields():
for key in self:
item = getattr(self, key)
if isinstance(item, int):
setattr(cp, key, item // other)
if isinstance(item, float):
setattr(cp, key, item / other)
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.fields():
for key in self:
item = getattr(self, key)
other_item = getattr(other, key)
if item is None:
@@ -193,164 +218,277 @@ class MinerData(BaseModel):
setattr(cp, key, item & other_item)
return cp
@computed_field # type: ignore[misc]
@property
def hashrate(self) -> AlgoHashRateType | None:
def hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) > 0:
hr_data = []
for item in self.hashboards:
if item.hashrate is not None:
hr_data.append(item.hashrate)
if len(hr_data) > 0:
return sum(hr_data, start=self.device_info.algo.hashrate(rate=0))
return self.raw_hashrate
return round(sum(map(lambda x: x.hashrate, self.hashboards)), 2)
return self._hashrate
@hashrate.setter
def hashrate(self, val):
self.raw_hashrate = val
self._hashrate = val
@computed_field # type: ignore[misc]
@property
def wattage_limit(self) -> int | None:
if self.config is not None:
if isinstance(self.config.mining_mode, MiningModePowerTune):
return self.config.mining_mode.power
return self.raw_wattage_limit
def fan_1(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 0:
return self.fans[0].speed
@wattage_limit.setter
def wattage_limit(self, val: int):
self.raw_wattage_limit = val
@fan_1.setter
def fan_1(self, val):
pass
@computed_field # type: ignore[misc]
@property
def total_chips(self) -> int | None:
if len(self.hashboards) > 0:
chip_data = []
for item in self.hashboards:
if item.chips is not None:
chip_data.append(item.chips)
if len(chip_data) > 0:
return sum(chip_data)
return None
def fan_2(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 1:
return self.fans[1].speed
@computed_field # type: ignore[misc]
@property
def nominal(self) -> bool | None:
if self.total_chips is None or self.expected_chips is None:
return None
return self.expected_chips == self.total_chips
@fan_2.setter
def fan_2(self, val):
pass
@computed_field # type: ignore[misc]
@property
def percent_expected_chips(self) -> int | None:
if self.total_chips is None or self.expected_chips is None:
return None
if self.total_chips == 0 or self.expected_chips == 0:
def fan_3(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 2:
return self.fans[2].speed
@fan_3.setter
def fan_3(self, val):
pass
@property
def fan_4(self): # noqa - Skip PyCharm inspection
if len(self.fans) > 3:
return self.fans[3].speed
@fan_4.setter
def fan_4(self, val):
pass
@property
def total_chips(self): # noqa - Skip PyCharm inspection
return sum([hb.chips for hb in self.hashboards])
@total_chips.setter
def total_chips(self, val):
pass
@property
def left_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[0].chips
return 0
@left_chips.setter
def left_chips(self, val):
pass
@property
def center_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].chips
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[1].chips
return 0
@center_chips.setter
def center_chips(self, val):
pass
@property
def right_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].chips
if len(self.hashboards) == 3:
return self.hashboards[2].chips
if len(self.hashboards) > 3:
return self.hashboards[-1:][0].chips
return 0
@right_chips.setter
def right_chips(self, val):
pass
@property
def left_board_hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[0].hashrate
return 0
@left_board_hashrate.setter
def left_board_hashrate(self, val):
pass
@property
def center_board_hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].hashrate
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[1].hashrate
return 0
@center_board_hashrate.setter
def center_board_hashrate(self, val):
pass
@property
def right_board_hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].hashrate
if len(self.hashboards) == 3:
return self.hashboards[2].hashrate
if len(self.hashboards) > 3:
return self.hashboards[-1:][0].hashrate
return 0
@right_board_hashrate.setter
def right_board_hashrate(self, val):
pass
@property
def left_board_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[0].temp
return 0
@left_board_temp.setter
def left_board_temp(self, val):
pass
@property
def center_board_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].temp
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[1].temp
return 0
@center_board_temp.setter
def center_board_temp(self, val):
pass
@property
def right_board_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].temp
if len(self.hashboards) == 3:
return self.hashboards[2].temp
if len(self.hashboards) > 3:
return self.hashboards[-1:][0].temp
return 0
@right_board_temp.setter
def right_board_temp(self, val):
pass
@property
def left_board_chip_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[0].chip_temp
return 0
@left_board_chip_temp.setter
def left_board_chip_temp(self, val):
pass
@property
def center_board_chip_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 1:
return self.hashboards[0].chip_temp
if len(self.hashboards) in [2, 3, 4]:
return self.hashboards[1].chip_temp
return 0
@center_board_chip_temp.setter
def center_board_chip_temp(self, val):
pass
@property
def right_board_chip_temp(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) == 2:
return self.hashboards[1].chip_temp
if len(self.hashboards) == 3:
return self.hashboards[2].chip_temp
if len(self.hashboards) > 3:
return self.hashboards[-1:][0].chip_temp
return 0
@right_board_chip_temp.setter
def right_board_chip_temp(self, val):
pass
@property
def nominal(self): # noqa - Skip PyCharm inspection
return self.ideal_chips == self.total_chips
@nominal.setter
def nominal(self, val):
pass
@property
def percent_ideal_chips(self): # noqa - Skip PyCharm inspection
if self.total_chips == 0 or self.ideal_chips == 0:
return 0
return round((self.total_chips / self.expected_chips) * 100)
return round((self.total_chips / self.ideal_chips) * 100)
@percent_ideal_chips.setter
def percent_ideal_chips(self, val):
pass
@computed_field # type: ignore[misc]
@property
def percent_expected_hashrate(self) -> int | None:
if self.hashrate is None or self.expected_hashrate is None:
return None
try:
return round((self.hashrate / self.expected_hashrate) * 100)
except ZeroDivisionError:
def percent_ideal_hashrate(self): # noqa - Skip PyCharm inspection
if self.hashrate == 0 or self.nominal_hashrate == 0:
return 0
return round((self.hashrate / self.nominal_hashrate) * 100)
@percent_ideal_hashrate.setter
def percent_ideal_hashrate(self, val):
pass
@computed_field # type: ignore[misc]
@property
def percent_expected_wattage(self) -> int | None:
if self.wattage_limit is None or self.wattage is None:
return None
try:
return round((self.wattage / self.wattage_limit) * 100)
except ZeroDivisionError:
def percent_ideal_wattage(self): # noqa - Skip PyCharm inspection
if self.wattage_limit == 0 or self.wattage == 0:
return 0
return round((self.wattage / self.wattage_limit) * 100)
@percent_ideal_wattage.setter
def percent_ideal_wattage(self, val):
pass
@computed_field # type: ignore[misc]
@property
def temperature_avg(self) -> int | None:
def temperature_avg(self): # noqa - Skip PyCharm inspection
total_temp = 0
temp_count = 0
for hb in self.hashboards:
if hb.temp is not None:
if hb.temp and not hb.temp == -1:
total_temp += hb.temp
temp_count += 1
if not temp_count > 0:
return None
return 0
return round(total_temp / temp_count)
@computed_field # type: ignore[misc]
@temperature_avg.setter
def temperature_avg(self, val):
pass
@property
def efficiency(self) -> int | None:
efficiency = self._efficiency(0)
if efficiency is None:
return None
else:
return int(efficiency)
def efficiency(self): # noqa - Skip PyCharm inspection
if self.hashrate == 0 or self.wattage == -1:
return 0
return round(self.wattage / self.hashrate)
@computed_field # type: ignore[misc]
@property
def efficiency_fract(self) -> float | None:
return self._efficiency(2)
def _efficiency(self, ndigits: int) -> float | None:
if self.hashrate is None or self.wattage is None:
return None
try:
return round(self.wattage / float(self.hashrate), ndigits)
except ZeroDivisionError:
return 0.0
@computed_field # type: ignore[misc]
@property
def datetime(self) -> str:
return self.raw_datetime.isoformat()
@computed_field # type: ignore[misc]
@property
def timestamp(self) -> int:
return int(time.mktime(self.raw_datetime.timetuple()))
@computed_field # type: ignore[misc]
@property
def make(self) -> str | None:
if self.device_info.make is not None:
return str(self.device_info.make)
@computed_field # type: ignore[misc]
@property
def model(self) -> str | None:
if self.device_info.model is not None:
return str(self.device_info.model)
@computed_field # type: ignore[misc]
@property
def firmware(self) -> str | None:
if self.device_info.firmware is not None:
return str(self.device_info.firmware)
@computed_field # type: ignore[misc]
@property
def algo(self) -> str | None:
if self.device_info.algo is not None:
return str(self.device_info.algo)
def keys(self) -> list:
return list(self.model_fields.keys())
@efficiency.setter
def efficiency(self, val):
pass
def asdict(self) -> dict:
return self.model_dump()
def as_dict(self) -> dict:
"""Get this dataclass as a dictionary.
Returns:
A dictionary version of this class.
"""
return self.asdict()
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
return asdict(self)
def as_json(self) -> str:
"""Get this dataclass as JSON.
@@ -358,7 +496,10 @@ class MinerData(BaseModel):
Returns:
A JSON version of this class.
"""
return self.model_dump_json()
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.
@@ -366,7 +507,9 @@ class MinerData(BaseModel):
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"])
@@ -374,9 +517,7 @@ class MinerData(BaseModel):
data_list = [str(data[item]) for item in data]
return ",".join(data_list)
def as_influxdb(
self, measurement_name: str = "miner_data", level_delimiter: str = "."
) -> str:
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:
@@ -385,127 +526,37 @@ class MinerData(BaseModel):
Returns:
A influxdb line protocol version of this class.
"""
def serialize_int(key: str, value: int) -> str:
return f"{key}={value}"
def serialize_float(key: str, value: float) -> str:
return f"{key}={value}"
def serialize_str(key: str, value: str) -> str:
return f'{key}="{value}"'
def serialize_algo_hash_rate(key: str, value: AlgoHashRateType) -> str:
return f"{key}={round(float(value), 2)}"
def serialize_list(key: str, value: list[Any]) -> str | None:
if len(value) == 0:
return None
list_field_data = []
for idx, list_field_val in enumerate(value):
item_serialization_func = serialization_map.get(
type(list_field_val), lambda _k, _v: None
)
item_serialized = item_serialization_func(
f"{key}{level_delimiter}{idx}", list_field_val
)
if item_serialized is not None:
list_field_data.append(item_serialized)
continue
for dt in serialization_map_instance:
if item_serialized is None:
if isinstance(list_field_val, dt):
item_serialized = serialization_map_instance[dt](
f"{key}{level_delimiter}{idx}", list_field_val
)
if item_serialized is not None:
list_field_data.append(item_serialized)
return ",".join(list_field_data)
def serialize_miner_error(key: str, value: BaseMinerError):
return value.as_influxdb(key, level_delimiter=level_delimiter)
def serialize_fan(key: str, value: Fan) -> str:
return f"{key}{level_delimiter}speed={value.speed}"
def serialize_hashboard(key: str, value: HashBoard) -> str:
return value.as_influxdb(key, level_delimiter=level_delimiter)
def serialize_bool(key: str, value: bool):
return f"{key}={str(value).lower()}"
def serialize_pool_metrics(key: str, value: PoolMetrics):
return value.as_influxdb(key, level_delimiter=level_delimiter)
include = [
"uptime",
"expected_hashrate",
"hashrate",
"hashboards",
"temperature_avg",
"env_temp",
"wattage",
"wattage_limit",
"voltage",
"fans",
"expected_fans",
"fan_psu",
"total_chips",
"expected_chips",
"efficiency",
"fault_light",
"is_mining",
"errors",
"pools",
]
serialization_map_instance = {
AlgoHashRateType: serialize_algo_hash_rate,
BaseMinerError: serialize_miner_error,
}
serialization_map = {
int: serialize_int,
float: serialize_float,
str: serialize_str,
bool: serialize_bool,
list: serialize_list,
Fan: serialize_fan,
HashBoard: serialize_hashboard,
PoolMetrics: serialize_pool_metrics,
}
tag_data = [
measurement_name,
f"ip={str(self.ip)}",
f"mac={str(self.mac)}",
f"make={str(self.make)}",
f"model={str(self.model)}",
f"firmware={str(self.firmware)}",
f"algo={str(self.algo)}",
]
logging.debug(f"MinerData - (To InfluxDB) - Dumping InfluxDB data")
tag_data = [measurement_name]
field_data = []
for field in include:
field_val = getattr(self, field)
serialization_func = serialization_map.get(
type(field_val), lambda _k, _v: None
)
serialized = serialization_func(field, field_val)
if serialized is not None:
field_data.append(serialized)
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
for datatype in serialization_map_instance:
if serialized is None:
if isinstance(field_val, datatype):
serialized = serialization_map_instance[datatype](
field, field_val
)
if serialized is not None:
field_data.append(serialized)
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).replace(" ", "\\ ")
field_str = ",".join(field_data).replace(" ", "\\ ")
timestamp = str(self.timestamp * 10**9)
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

@@ -1,136 +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 __future__ import annotations
from typing import Any
from pydantic import BaseModel
from pyasic.device.algorithm.hashrate import AlgoHashRateType
class HashBoard(BaseModel):
"""A Dataclass to standardize hashboard data.
Attributes:
slot: The slot of the board as an int.
hashrate: The hashrate of the board in TH/s as a float.
inlet_temp: Inlet temperature for hydro asics as an int
outlet_temp: Outlet temperature for hydro asics as an int
temp: The temperature of the PCB as an int.
chip_temp: The temperature of the chips as an int.
chips: The chip count of the board as an int.
expected_chips: The expected chip count of the board as an int.
serial_number: The serial number of the board.
missing: Whether the board is returned from the miners data as a bool.
tuned: Whether the board is tuned as a bool.
active: Whether the board is currently tuning as a bool.
voltage: Current input voltage of the board as a float.
"""
slot: int = 0
hashrate: AlgoHashRateType | None = None
inlet_temp: float | None = None
outlet_temp: float | None = None
temp: float | None = None
chip_temp: float | None = None
chips: int | None = None
expected_chips: int | None = None
serial_number: str | None = None
missing: bool = True
tuned: bool | None = None
active: bool | None = None
voltage: float | None = None
@classmethod
def fields(cls) -> set:
all_fields = set(cls.model_fields.keys())
all_fields.update(set(cls.model_computed_fields.keys()))
return all_fields
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"{item}")
def as_influxdb(self, key_root: str, level_delimiter: str = ".") -> str:
def serialize_int(key: str, value: int) -> str:
return f"{key}={value}"
def serialize_float(key: str, value: float) -> str:
return f"{key}={value}"
def serialize_str(key: str, value: str) -> str:
return f'{key}="{value}"'
def serialize_algo_hash_rate(key: str, value: AlgoHashRateType) -> str:
return f"{key}={round(float(value), 2)}"
def serialize_bool(key: str, value: bool):
return f"{key}={str(value).lower()}"
serialization_map_instance = {
AlgoHashRateType: serialize_algo_hash_rate,
}
serialization_map = {
int: serialize_int,
float: serialize_float,
str: serialize_str,
bool: serialize_bool,
}
include = [
"hashrate",
"temp",
"chip_temp",
"chips",
"expected_chips",
"tuned",
"active",
"voltage",
]
field_data = []
for field in include:
field_val = getattr(self, field)
serialization_func = serialization_map.get(
type(field_val), lambda _k, _v: None
)
serialized = serialization_func(
f"{key_root}{level_delimiter}{field}", field_val
)
if serialized is not None:
field_data.append(serialized)
continue
for datatype in serialization_map_instance:
if serialized is None:
if isinstance(field_val, datatype):
serialized = serialization_map_instance[datatype](
f"{key_root}{level_delimiter}{field}", field_val
)
if serialized is not None:
field_data.append(serialized)
return ",".join(field_data)

View File

@@ -1,31 +0,0 @@
from pydantic import BaseModel, ConfigDict, field_serializer
from pyasic.device.algorithm import MinerAlgoType
from pyasic.device.firmware import MinerFirmware
from pyasic.device.makes import MinerMake
from pyasic.device.models import MinerModelType
class DeviceInfo(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
make: MinerMake | None = None
model: MinerModelType | None = None
firmware: MinerFirmware | None = None
algo: type[MinerAlgoType] | None = None
@field_serializer("make")
def serialize_make(self, make: MinerMake, _info):
return str(make)
@field_serializer("model")
def serialize_model(self, model: MinerModelType, _info):
return str(model)
@field_serializer("firmware")
def serialize_firmware(self, firmware: MinerFirmware, _info):
return str(firmware)
@field_serializer("algo")
def serialize_algo(self, algo: MinerAlgoType, _info):
return str(algo)

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