Compare commits

..

1 Commits

Author SHA1 Message Date
UpstreamData
b252da6c5a version: bump version number. Pin httpx to 0.25.0 min. 2023-11-12 18:36:24 -07:00
390 changed files with 12458 additions and 15047 deletions

281
README.md
View File

@@ -1,249 +1,142 @@
# pyasic # pyasic
*A simplified and standardized interface for Bitcoin ASICs.* *A simplified and standardized interface for Bitcoin ASICs.*
[![PyPI - Version](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pyasic)](https://pypi.org/project/pyasic/) [![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/) Supported miners are listed in the docs, [here](https://pyasic.readthedocs.io/en/latest/miners/supported_types/).
[![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) ## Installation
[![Read The Docs - Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/) You can install pyasic directly from pip with the command `pip install pyasic`.
[![License - Apache 2.0](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
--- For those of you who aren't comfortable with code and developer tools, there are windows builds of GUI applications that use this library [here](https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing).
## Intro
Welcome to `pyasic`! `pyasic` uses an asynchronous method of communicating with ASIC miners on your network, which makes it super fast. ## 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.
[Click here to view supported miner types](miners/supported_types.md) <br>
--- This repo uses poetry for dependencies, which can be installed by following the guide on their website [here](https://python-poetry.org/docs/#installation).
## 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. 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.
##### Scanning for miners Finally, initialize pre-commit hooks with `poetry run pre-commit install`.
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
### Documentation Testing
Testing the documentation can be done by running `poetry run mkdocs serve`, whcih will serve the documentation locally on port 8000.
async def scan_miners(): # define async scan function to allow awaiting ## Interfacing with miners programmatically
# 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 There are 2 main ways to get a miner (and the functions attached to it), via scanning or via the `MinerFactory()`.
# this will return the correct type of miners if they are supported with all functionality.
miners = await network.scan()
print(miners)
if __name__ == "__main__": #### Scanning for miners
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
```python ```python
import asyncio import asyncio
from pyasic import get_miner
async def gather_miner_data(): from pyasic.network import MinerNetwork
miner = await get_miner("192.168.1.75")
if miner is not None:
miner_data = await miner.get_data() # define asynchronous function to scan for miners
print(miner_data) # all data from the dataclass async def scan_and_get_data():
print(miner_data.hashrate) # hashrate of the miner in TH/s # 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__": if __name__ == "__main__":
asyncio.run(gather_miner_data()) asyncio.run(scan_and_get_data())
```
---
##### Multiple miners
You can do something similar with multiple miners, with only needing to make a small change to get all the data at once.
```python
import asyncio # asyncio for handling the async part
from pyasic.network import MinerNetwork # miner network handles the scanning
async def gather_miner_data(): # define async scan function to allow awaiting
network = MinerNetwork.from_subnet("192.168.1.50/24")
miners = await network.scan()
# we need to asyncio.gather() all the miners get_data() functions to make them run together
all_miner_data = await asyncio.gather(*[miner.get_data() for miner in miners])
for miner_data in all_miner_data:
print(miner_data) # print out all the data one by one
if __name__ == "__main__":
asyncio.run(gather_miner_data())
``` ```
---
## Miner control
`pyasic` exposes a standard interface for each miner using control functions. #### Getting a miner if you know the IP
Every miner class in `pyasic` must implement all the control functions defined in `BaseMiner`.
These functions are
`check_light`,
`fault_light_off`,
`fault_light_on`,
`get_config`,
`get_data`,
`get_errors`,
`get_hostname`,
`get_model`,
`reboot`,
`restart_backend`,
`stop_mining`,
`resume_mining`,
`is_mining`,
`send_config`, and
`set_power_limit`.
##### Usage
```python ```python
import asyncio import asyncio
from pyasic import get_miner from pyasic import get_miner
async def set_fault_light(): # define asynchronous function to get miner and data
miner = await get_miner("192.168.1.20") 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 # Get data from the miner
await miner.fault_light_on() data = await miner.get_data()
print(data)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(set_fault_light()) asyncio.run(get_miner_data("192.168.1.69"))
``` ```
--- ### Advanced data gathering
## Helper dataclasses
##### `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 #### List available API commands
`MinerData` is a return from the [`get_data()`](#get-data) function, and is used to have a consistent dataset across all returns.
You can call `MinerData.as_dict()` to get the dataclass as a dictionary, and there are many other helper functions contained in the class to convert to different data formats.
`MinerData` instances can also be added to each other to combine their data and can be divided by a number to divide all their data, allowing you to get average data from many miners by doing -
```python
from pyasic import MinerData
# examples of miner data
d1 = MinerData("192.168.1.1")
d2 = MinerData("192.168.1.2")
list_of_miner_data = [d1, d2]
average_data = sum(list_of_miner_data, start=MinerData("0.0.0.0"))/len(list_of_miner_data)
```
---
##### MinerConfig
`MinerConfig` is `pyasic`'s way to represent a configuration file from a miner.
It is designed to unionize the configuration of all supported miner types, and is the return from [`get_config()`](#get-config).
Each miner has a unique way to convert the `MinerConfig` to their specific type, there are helper functions in the class.
In most cases these helper functions should not be used, as [`send_config()`](#send-config) takes a [`MinerConfig` and will do the conversion to the right type for you.
You can use the `MinerConfig` as follows:
```python ```python
import asyncio import asyncio
from pyasic import get_miner from pyasic import get_miner
async def set_fault_light(): async def get_api_commands(miner_ip: str):
miner = await get_miner("192.168.1.20") # Get the miner
miner = await get_miner(miner_ip)
# get config # List all available commands
cfg = await miner.get_config() # 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__": if __name__ == "__main__":
asyncio.run(set_fault_light()) asyncio.run(get_api_commands("192.168.1.69"))
``` ```
--- #### Use miner API commands to gather data
## 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: 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 ```python
from pyasic import settings import asyncio
settings.update("default_antminer_password", "my_pwd") from pyasic import get_miner
```
##### Default values:
```
"network_ping_retries": 1,
"network_ping_timeout": 3,
"network_scan_threads": 300,
"factory_get_retries": 1,
"factory_get_timeout": 3,
"get_data_retries": 1,
"api_function_timeout": 5,
"default_whatsminer_password": "admin",
"default_innosilicon_password": "admin",
"default_antminer_password": "root",
"default_bosminer_password": "root",
"default_vnish_password": "admin",
"default_goldshell_password": "123456789",
# ADVANCED async def get_api_commands(miner_ip: str):
# Only use this if you know what you are doing # Get the miner
"socket_linger_time": 1000, 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"))
``` ```

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

@@ -0,0 +1,27 @@
# 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]
#### [LUXMiner API][pyasic.API.luxminer.LUXMinerAPI]
#### [Unknown API][pyasic.API.unknown.UnknownAPI]
<br>
## BaseMinerAPI
::: pyasic.API.BaseMinerAPI
handler: python
options:
heading_level: 4

View File

@@ -1,7 +1,6 @@
# pyasic # pyasic
## ePIC Backend ## BFGMinerAPI
::: pyasic.API.bfgminer.BFGMinerAPI
::: pyasic.miners.backends.epic.ePIC
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false

View File

@@ -1,6 +1,6 @@
# pyasic # pyasic
## BMMinerRPCAPI ## BMMinerAPI
::: pyasic.rpc.bmminer.BMMinerRPCAPI ::: pyasic.API.bmminer.BMMinerAPI
handler: python handler: python
options: options:
show_root_heading: false 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 # pyasic
## BTMinerRPCAPI ## BTMinerAPI
::: pyasic.rpc.btminer.BTMinerRPCAPI ::: pyasic.API.btminer.BTMinerAPI
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false

View File

@@ -1,6 +1,6 @@
# pyasic # pyasic
## CGMinerRPCAPI ## CGMinerAPI
::: pyasic.rpc.cgminer.CGMinerRPCAPI ::: pyasic.API.cgminer.CGMinerAPI
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false

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

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

View File

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

View File

@@ -6,3 +6,19 @@
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 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 # pyasic
## Miner Data ## Miner Data
::: pyasic.data.MinerData ::: pyasic.data.MinerData
handler: python handler: python
options: options:
@@ -13,10 +13,3 @@
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## Fan Data
::: pyasic.data.Fan
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -3,7 +3,7 @@ import importlib
import os import os
import warnings import warnings
from pyasic.miners.factory import MINER_CLASSES, MinerTypes from pyasic.miners.miner_factory import MINER_CLASSES, MinerTypes
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
@@ -31,8 +31,6 @@ def backend_str(backend: MinerTypes) -> str:
return "Stock Firmware Avalonminers" return "Stock Firmware Avalonminers"
case MinerTypes.VNISH: case MinerTypes.VNISH:
return "Vnish Firmware Miners" return "Vnish Firmware Miners"
case MinerTypes.EPIC:
return "ePIC Firmware Miners"
case MinerTypes.BRAIINS_OS: case MinerTypes.BRAIINS_OS:
return "BOS+ Firmware Miners" return "BOS+ Firmware Miners"
case MinerTypes.HIVEON: case MinerTypes.HIVEON:

View File

@@ -1,31 +1,25 @@
# pyasic # 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.*
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![pypi](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)
[![python](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![Read the Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![GitHub](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
[![PyPI - Version](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pyasic)](https://pypi.org/project/pyasic/)
[![Python - Supported Versions](https://img.shields.io/pypi/pyversions/pyasic.svg)](https://pypi.org/project/pyasic/)
[![CodeFactor - Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
[![Commit Activity - master](https://img.shields.io/github/commit-activity/y/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/commits/master/)
[![Code Style - Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Read The Docs - Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![License - Apache 2.0](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
---
## Intro ## 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)
--- 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.
##### Scanning for miners <br>
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. ## 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 ```python
import asyncio # asyncio for handling the async part import asyncio # asyncio for handling the async part
from pyasic.network import MinerNetwork # miner network handles the scanning from pyasic.network import MinerNetwork # miner network handles the scanning
@@ -34,20 +28,21 @@ from pyasic.network import MinerNetwork # miner network handles the scanning
async def scan_miners(): # define async scan function to allow awaiting async def scan_miners(): # define async scan function to allow awaiting
# create a miner network # create a miner network
# you can pass in any IP and it will use that in a subnet with a /24 mask (255 IPs). # 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 network = MinerNetwork("192.168.1.50") # this uses the 192.168.1.0-255 network
# scan for miners asynchronously # scan for miners asynchronously
# this will return the correct type of miners if they are supported with all functionality. # this will return the correct type of miners if they are supported with all functionality.
miners = await network.scan() miners = await network.scan_network_for_miners()
print(miners) print(miners)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(scan_miners()) # run the scan asynchronously with asyncio.run() asyncio.run(scan_miners()) # 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]. ## Creating miners based on IP
If you already know the IP address of your miner or miners, you can use the [`MinerFactory`][pyasic.miners.miner_factory.MinerFactory] to communicate and identify the miners, or an abstraction of its functionality, [`get_miner()`][pyasic.miners.get_miner].
The function [`get_miner()`][pyasic.miners.get_miner] will return any miner it found at the IP address specified, or an `UnknownMiner` if it cannot identify the miner. The function [`get_miner()`][pyasic.miners.get_miner] will return any miner it found at the IP address specified, or an `UnknownMiner` if it cannot identify the miner.
```python ```python
import asyncio # asyncio for handling the async part import asyncio # asyncio for handling the async part
@@ -63,8 +58,6 @@ async def get_miners(): # define async scan function to allow awaiting
print(miner_1, miner_2) print(miner_1, miner_2)
# can also gather these, since they are async # 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")] tasks = [get_miner("192.168.1.75"), get_miner("192.168.1.76")]
miners = await asyncio.gather(*tasks) miners = await asyncio.gather(*tasks)
print(miners) print(miners)
@@ -74,14 +67,13 @@ if __name__ == "__main__":
asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run() asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run()
``` ```
--- <br>
## Data gathering
--- ## 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()`.
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. 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]. 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 ```python
import asyncio import asyncio
from pyasic import get_miner from pyasic import get_miner
@@ -96,8 +88,7 @@ async def gather_miner_data():
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(gather_miner_data()) 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. You can do something similar with multiple miners, with only needing to make a small change to get all the data at once.
```python ```python
import asyncio # asyncio for handling the async part import asyncio # asyncio for handling the async part
@@ -105,8 +96,8 @@ from pyasic.network import MinerNetwork # miner network handles the scanning
async def gather_miner_data(): # define async scan function to allow awaiting async def gather_miner_data(): # define async scan function to allow awaiting
network = MinerNetwork.from_subnet("192.168.1.50/24") network = MinerNetwork("192.168.1.50")
miners = await network.scan() miners = await network.scan_network_for_miners()
# we need to asyncio.gather() all the miners get_data() functions to make them run together # 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]) all_miner_data = await asyncio.gather(*[miner.get_data() for miner in miners])
@@ -118,56 +109,157 @@ if __name__ == "__main__":
asyncio.run(gather_miner_data()) asyncio.run(gather_miner_data())
``` ```
--- <br>
## Miner control
--- ## Controlling miners via pyasic
`pyasic` exposes a standard interface for each miner using control functions. Every miner class in pyasic must implement all the control functions defined in [`BaseMiner`][pyasic.miners.BaseMiner].
Every miner class in `pyasic` must implement all the control functions defined in [`MinerProtocol`][pyasic.miners.base.MinerProtocol].
These functions are These functions are
[`check_light`][pyasic.miners.base.MinerProtocol.check_light], [`check_light`](#check-light),
[`fault_light_off`][pyasic.miners.base.MinerProtocol.fault_light_off], [`fault_light_off`](#fault-light-off),
[`fault_light_on`][pyasic.miners.base.MinerProtocol.fault_light_on], [`fault_light_on`](#fault-light-on),
[`get_config`][pyasic.miners.base.MinerProtocol.get_config], [`get_config`](#get-config),
[`get_data`][pyasic.miners.base.MinerProtocol.get_data], [`get_data`](#get-data),
[`get_errors`][pyasic.miners.base.MinerProtocol.get_errors], [`get_errors`](#get-errors),
[`get_hostname`][pyasic.miners.base.MinerProtocol.get_hostname], [`get_hostname`](#get-hostname),
[`get_model`][pyasic.miners.base.MinerProtocol.get_model], [`get_model`](#get-model),
[`reboot`][pyasic.miners.base.MinerProtocol.reboot], [`reboot`](#reboot),
[`restart_backend`][pyasic.miners.base.MinerProtocol.restart_backend], [`restart_backend`](#restart-backend),
[`stop_mining`][pyasic.miners.base.MinerProtocol.stop_mining], [`stop_mining`](#stop-mining),
[`resume_mining`][pyasic.miners.base.MinerProtocol.resume_mining], [`resume_mining`](#resume-mining),
[`is_mining`][pyasic.miners.base.MinerProtocol.is_mining], [`is_mining`](#is-mining),
[`send_config`][pyasic.miners.base.MinerProtocol.send_config], and [`send_config`](#send-config), and
[`set_power_limit`][pyasic.miners.base.MinerProtocol.set_power_limit]. [`set_power_limit`](#set-power-limit).
##### Usage <br>
```python
import asyncio
from pyasic import get_miner
### Check Light
::: pyasic.miners.BaseMiner.check_light
handler: python
options:
heading_level: 4
async def set_fault_light(): <br>
miner = await get_miner("192.168.1.20")
# call control function ### Fault Light Off
await miner.fault_light_on() ::: pyasic.miners.BaseMiner.fault_light_off
handler: python
options:
heading_level: 4
if __name__ == "__main__": <br>
asyncio.run(set_fault_light())
```
--- ### Fault Light On
## Helper dataclasses ::: pyasic.miners.BaseMiner.fault_light_on
--- 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 Config
::: pyasic.miners.BaseMiner.get_config
handler: python
options:
heading_level: 4
--- <br>
##### [`MinerData`][pyasic.data.MinerData] ### Get Data
::: pyasic.miners.BaseMiner.get_data
handler: python
options:
heading_level: 4
<br>
### Get Errors
::: pyasic.miners.BaseMiner.get_errors
handler: python
options:
heading_level: 4
<br>
### Get Hostname
::: pyasic.miners.BaseMiner.get_hostname
handler: python
options:
heading_level: 4
<br>
### Get Model
::: pyasic.miners.BaseMiner.get_model
handler: python
options:
heading_level: 4
<br>
### Reboot
::: pyasic.miners.BaseMiner.reboot
handler: python
options:
heading_level: 4
<br>
### Restart Backend
::: pyasic.miners.BaseMiner.restart_backend
handler: python
options:
heading_level: 4
<br>
### Stop Mining
::: pyasic.miners.BaseMiner.stop_mining
handler: python
options:
heading_level: 4
<br>
### Resume Mining
::: pyasic.miners.BaseMiner.resume_mining
handler: python
options:
heading_level: 4
<br>
### Is Mining
::: pyasic.miners.BaseMiner.is_mining
handler: python
options:
heading_level: 4
<br>
### Send Config
::: pyasic.miners.BaseMiner.send_config
handler: python
options:
heading_level: 4
<br>
### Set Power Limit
::: pyasic.miners.BaseMiner.set_power_limit
handler: python
options:
heading_level: 4
<br>
## [`MinerConfig`][pyasic.config.MinerConfig] and [`MinerData`][pyasic.data.MinerData]
Pyasic implements a few dataclasses as helpers to make data return types consistent across different miners and miner APIs. The different fields of these dataclasses can all be viewed with the classmethod `cls.fields()`.
<br>
### [`MinerData`][pyasic.data.MinerData]
[`MinerData`][pyasic.data.MinerData] is a return from the [`get_data()`](#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.
@@ -186,40 +278,19 @@ list_of_miner_data = [d1, d2]
average_data = sum(list_of_miner_data, start=MinerData("0.0.0.0"))/len(list_of_miner_data) average_data = sum(list_of_miner_data, start=MinerData("0.0.0.0"))/len(list_of_miner_data)
``` ```
---
##### [`MinerConfig`][pyasic.config.MinerConfig] <br>
[`MinerConfig`][pyasic.config.MinerConfig] is `pyasic`'s way to represent a configuration file from a miner. ### [`MinerConfig`][pyasic.config.MinerConfig]
It is designed to unionize the configuration of all supported miner types, and is the return from [`get_config()`](#get-config).
[`MinerConfig`][pyasic.config.MinerConfig] is pyasic's way to represent a configuration file from a miner.
It is 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. Each miner has a unique way to convert the [`MinerConfig`][pyasic.config.MinerConfig] to their specific type, there are helper functions in the class.
In most cases these helper functions should not be used, as [`send_config()`](#send-config) takes a [`MinerConfig`][pyasic.config.MinerConfig] and will do the conversion to the right type for you. In most cases these helper functions should not be used, as [`send_config()`](#send-config) takes a [`MinerConfig`][pyasic.config.MinerConfig] and will do the conversion to the right type for you.
You can use the [`MinerConfig`][pyasic.config.MinerConfig] as follows:
```python
import asyncio
from pyasic import get_miner
async def set_fault_light():
miner = await get_miner("192.168.1.20")
# get config
cfg = await miner.get_config()
# send config
await miner.send_config(cfg)
if __name__ == "__main__":
asyncio.run(set_fault_light())
```
---
## Settings ## 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`][pyasic.settings] module, used as follows:
`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 ```python
from pyasic import settings from pyasic import settings
@@ -227,23 +298,17 @@ from pyasic import settings
settings.update("default_antminer_password", "my_pwd") settings.update("default_antminer_password", "my_pwd")
``` ```
##### Default values: Here are of all the settings, and their default values:
``` ```
"network_ping_retries": 1, "network_ping_retries": 1,
"network_ping_timeout": 3, "network_ping_timeout": 3,
"network_scan_threads": 300, "network_scan_threads": 300,
"factory_get_retries": 1, "factory_get_retries": 1,
"factory_get_timeout": 3,
"get_data_retries": 1, "get_data_retries": 1,
"api_function_timeout": 5,
"default_whatsminer_password": "admin", "default_whatsminer_password": "admin",
"default_innosilicon_password": "admin", "default_innosilicon_password": "admin",
"default_antminer_password": "root", "default_antminer_password": "root",
"default_bosminer_password": "root", "default_bosminer_password": "root",
"default_vnish_password": "admin", "default_vnish_password": "admin",
"default_goldshell_password": "123456789", "default_goldshell_password": "123456789",
# ADVANCED
# Only use this if you know what you are doing
"socket_linger_time": 1000,
``` ```

View File

@@ -29,20 +29,6 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## S19i
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19i
handler: python
options:
show_root_heading: false
heading_level: 4
## S19+
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j No PIC ## S19j No PIC
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19jNoPIC ::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19jNoPIC
handler: python handler: python
@@ -127,13 +113,6 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## S19j Pro (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## T19 (BOS) ## T19 (BOS)
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19 ::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
handler: python handler: python
@@ -197,38 +176,3 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## S19 (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19j
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 XP (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19XP
handler: python
options:
show_root_heading: false
heading_level: 4

View File

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

View File

@@ -1,17 +1,10 @@
# pyasic # pyasic
## Base Miner ## 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.BaseMiner
::: pyasic.miners.base.BaseMiner
handler: python
options:
heading_level: 4
::: pyasic.miners.base.MinerProtocol
handler: python handler: python
options: options:
heading_level: 4 heading_level: 4

View File

@@ -1,91 +0,0 @@
## Control functionality
### Check Light
::: pyasic.miners.base.MinerProtocol.check_light
handler: python
options:
heading_level: 4
### Fault Light Off
::: pyasic.miners.base.MinerProtocol.fault_light_off
handler: python
options:
heading_level: 4
### Fault Light On
::: pyasic.miners.base.MinerProtocol.fault_light_on
handler: python
options:
heading_level: 4
### Get Config
::: pyasic.miners.base.MinerProtocol.get_config
handler: python
options:
heading_level: 4
### Get Data
::: pyasic.miners.base.MinerProtocol.get_data
handler: python
options:
heading_level: 4
### Get Errors
::: pyasic.miners.base.MinerProtocol.get_errors
handler: python
options:
heading_level: 4
### Get Hostname
::: pyasic.miners.base.MinerProtocol.get_hostname
handler: python
options:
heading_level: 4
### Get Model
::: pyasic.miners.base.MinerProtocol.get_model
handler: python
options:
heading_level: 4
### Reboot
::: pyasic.miners.base.MinerProtocol.reboot
handler: python
options:
heading_level: 4
### Restart Backend
::: pyasic.miners.base.MinerProtocol.restart_backend
handler: python
options:
heading_level: 4
### Stop Mining
::: pyasic.miners.base.MinerProtocol.stop_mining
handler: python
options:
heading_level: 4
### Resume Mining
::: pyasic.miners.base.MinerProtocol.resume_mining
handler: python
options:
heading_level: 4
### Is Mining
::: pyasic.miners.base.MinerProtocol.is_mining
handler: python
options:
heading_level: 4
### Send Config
::: pyasic.miners.base.MinerProtocol.send_config
handler: python
options:
heading_level: 4
### Set Power Limit
::: pyasic.miners.base.MinerProtocol.set_power_limit
handler: python
options:
heading_level: 4

View File

@@ -2,22 +2,23 @@
## X5 Models ## X5 Models
## CK5 ## CK5
::: pyasic.miners.goldshell.bfgminer.X5.CK5.GoldshellCK5 ::: pyasic.miners.goldshell.bfgminer.X5.CK5.BFGMinerCK5
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## HS5 ## HS5
::: pyasic.miners.goldshell.bfgminer.X5.HS5.GoldshellHS5 ::: pyasic.miners.goldshell.bfgminer.X5.HS5.BFGMinerHS5
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## KD5 ## KD5
::: pyasic.miners.goldshell.bfgminer.X5.KD5.GoldshellKD5 ::: pyasic.miners.goldshell.bfgminer.X5.KD5.BFGMinerKD5
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4

View File

@@ -2,8 +2,9 @@
## XMax Models ## XMax Models
## KD Max ## KD Max
::: pyasic.miners.goldshell.bfgminer.XMax.KDMax.KDMax ::: pyasic.miners.goldshell.bfgminer.XMax.KDMax.BFGMinerKDMax
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4

View File

@@ -2,8 +2,9 @@
## A10X Models ## A10X Models
## A10X ## A10X
::: pyasic.miners.innosilicon.cgminer.A10X.A10X.InnosiliconA10X ::: pyasic.miners.innosilicon.cgminer.A10X.A10X.CGMinerA10X
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4

View File

@@ -2,8 +2,9 @@
## T3X Models ## T3X Models
## T3H+ ## T3H+
::: pyasic.miners.innosilicon.cgminer.T3X.T3H.InnosiliconT3HPlus ::: pyasic.miners.innosilicon.cgminer.T3X.T3H.CGMinerT3HPlus
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4

View File

@@ -1,7 +1,7 @@
# pyasic # pyasic
## Miner Factory ## Miner Factory
[`MinerFactory`][pyasic.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. [`MinerFactory`][pyasic.miners.miner_factory.MinerFactory] is the way to create miner types in `pyasic`. The most important method is [`get_miner()`][pyasic.get_miner], which is mapped to [`pyasic.get_miner()`][pyasic.get_miner], and should be used from there.
The instance used for [`pyasic.get_miner()`][pyasic.get_miner] is `pyasic.miner_factory`. The instance used for [`pyasic.get_miner()`][pyasic.get_miner] is `pyasic.miner_factory`.
@@ -9,7 +9,7 @@ The instance used for [`pyasic.get_miner()`][pyasic.get_miner] is `pyasic.miner_
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()`. 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 ::: pyasic.miners.miner_factory.MinerFactory
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false
@@ -25,12 +25,12 @@ Finally, there is functionality to get multiple miners without using `asyncio.ga
<br> <br>
## AnyMiner ## AnyMiner
::: pyasic.miners.base.AnyMiner ::: pyasic.miners.miner_factory.AnyMiner
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
[`AnyMiner`][pyasic.miners.base.AnyMiner] is a placeholder type variable used for typing returns of functions. [`AnyMiner`][pyasic.miners.miner_factory.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.BaseMiner], 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. and is used to specify a function returning some arbitrary type of miner class instance.

View File

@@ -10,6 +10,9 @@ details {
padding-top:0px; padding-top:0px;
padding-bottom:0px; padding-bottom:0px;
} }
ul {
margin:0px;
}
</style> </style>
<details> <details>
@@ -70,8 +73,6 @@ details {
<li><a href="../antminer/X19#s19l">S19L</a></li> <li><a href="../antminer/X19#s19l">S19L</a></li>
<li><a href="../antminer/X19#s19-pro">S19 Pro</a></li> <li><a href="../antminer/X19#s19-pro">S19 Pro</a></li>
<li><a href="../antminer/X19#s19j">S19j</a></li> <li><a href="../antminer/X19#s19j">S19j</a></li>
<li><a href="../antminer/X19#s19i">S19i</a></li>
<li><a href="../antminer/X19#s19_1">S19+</a></li>
<li><a href="../antminer/X19#s19j-no-pic">S19j No PIC</a></li> <li><a href="../antminer/X19#s19j-no-pic">S19j No PIC</a></li>
<li><a href="../antminer/X19#s19-pro_1">S19 Pro+</a></li> <li><a href="../antminer/X19#s19-pro_1">S19 Pro+</a></li>
<li><a href="../antminer/X19#s19j-pro">S19j Pro</a></li> <li><a href="../antminer/X19#s19j-pro">S19j Pro</a></li>
@@ -93,8 +94,6 @@ details {
<li><a href="../whatsminer/M2X#m20s-v10">M20S V10</a></li> <li><a href="../whatsminer/M2X#m20s-v10">M20S V10</a></li>
<li><a href="../whatsminer/M2X#m20s-v20">M20S V20</a></li> <li><a href="../whatsminer/M2X#m20s-v20">M20S V20</a></li>
<li><a href="../whatsminer/M2X#m20s-v30">M20S V30</a></li> <li><a href="../whatsminer/M2X#m20s-v30">M20S V30</a></li>
<li><a href="../whatsminer/M2X#m20p-v10">M20P V10</a></li>
<li><a href="../whatsminer/M2X#m20p-v30">M20P V30</a></li>
<li><a href="../whatsminer/M2X#m20s_1-v30">M20S+ V30</a></li> <li><a href="../whatsminer/M2X#m20s_1-v30">M20S+ V30</a></li>
<li><a href="../whatsminer/M2X#m21-v10">M21 V10</a></li> <li><a href="../whatsminer/M2X#m21-v10">M21 V10</a></li>
<li><a href="../whatsminer/M2X#m21s-v20">M21S V20</a></li> <li><a href="../whatsminer/M2X#m21s-v20">M21S V20</a></li>
@@ -109,8 +108,6 @@ details {
<ul> <ul>
<li><a href="../whatsminer/M3X#m30-v10">M30 V10</a></li> <li><a href="../whatsminer/M3X#m30-v10">M30 V10</a></li>
<li><a href="../whatsminer/M3X#m30-v20">M30 V20</a></li> <li><a href="../whatsminer/M3X#m30-v20">M30 V20</a></li>
<li><a href="../whatsminer/M3X#m30k-v10">M30K V10</a></li>
<li><a href="../whatsminer/M3X#m30l-v10">M30L V10</a></li>
<li><a href="../whatsminer/M3X#m30s-v10">M30S V10</a></li> <li><a href="../whatsminer/M3X#m30s-v10">M30S V10</a></li>
<li><a href="../whatsminer/M3X#m30s-v20">M30S V20</a></li> <li><a href="../whatsminer/M3X#m30s-v20">M30S V20</a></li>
<li><a href="../whatsminer/M3X#m30s-v30">M30S V30</a></li> <li><a href="../whatsminer/M3X#m30s-v30">M30S V30</a></li>
@@ -160,7 +157,6 @@ details {
<li><a href="../whatsminer/M3X#m30s_1-ve100">M30S+ VE100</a></li> <li><a href="../whatsminer/M3X#m30s_1-ve100">M30S+ VE100</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vf20">M30S+ VF20</a></li> <li><a href="../whatsminer/M3X#m30s_1-vf20">M30S+ VF20</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vf30">M30S+ VF30</a></li> <li><a href="../whatsminer/M3X#m30s_1-vf30">M30S+ VF30</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vg20">M30S+ VG20</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vg30">M30S+ VG30</a></li> <li><a href="../whatsminer/M3X#m30s_1-vg30">M30S+ VG30</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vg40">M30S+ VG40</a></li> <li><a href="../whatsminer/M3X#m30s_1-vg40">M30S+ VG40</a></li>
<li><a href="../whatsminer/M3X#m30s_1-vg50">M30S+ VG50</a></li> <li><a href="../whatsminer/M3X#m30s_1-vg50">M30S+ VG50</a></li>
@@ -194,9 +190,6 @@ details {
<li><a href="../whatsminer/M3X#m30s_1_1-vj30">M30S++ VJ30</a></li> <li><a href="../whatsminer/M3X#m30s_1_1-vj30">M30S++ VJ30</a></li>
<li><a href="../whatsminer/M3X#m31-v10">M31 V10</a></li> <li><a href="../whatsminer/M3X#m31-v10">M31 V10</a></li>
<li><a href="../whatsminer/M3X#m31-v20">M31 V20</a></li> <li><a href="../whatsminer/M3X#m31-v20">M31 V20</a></li>
<li><a href="../whatsminer/M3X#m31h-v10">M31H V10</a></li>
<li><a href="../whatsminer/M3X#m31h-v40">M31H V40</a></li>
<li><a href="../whatsminer/M3X#m30l-v10">M30L V10</a></li>
<li><a href="../whatsminer/M3X#m31s-v10">M31S V10</a></li> <li><a href="../whatsminer/M3X#m31s-v10">M31S V10</a></li>
<li><a href="../whatsminer/M3X#m31s-v20">M31S V20</a></li> <li><a href="../whatsminer/M3X#m31s-v20">M31S V20</a></li>
<li><a href="../whatsminer/M3X#m31s-v30">M31S V30</a></li> <li><a href="../whatsminer/M3X#m31s-v30">M31S V30</a></li>
@@ -212,6 +205,7 @@ details {
<li><a href="../whatsminer/M3X#m31se-v10">M31SE V10</a></li> <li><a href="../whatsminer/M3X#m31se-v10">M31SE V10</a></li>
<li><a href="../whatsminer/M3X#m31se-v20">M31SE V20</a></li> <li><a href="../whatsminer/M3X#m31se-v20">M31SE V20</a></li>
<li><a href="../whatsminer/M3X#m31se-v30">M31SE V30</a></li> <li><a href="../whatsminer/M3X#m31se-v30">M31SE V30</a></li>
<li><a href="../whatsminer/M3X#m31h-v40">M31H V40</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v10">M31S+ V10</a></li> <li><a href="../whatsminer/M3X#m31s_1-v10">M31S+ V10</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v20">M31S+ V20</a></li> <li><a href="../whatsminer/M3X#m31s_1-v20">M31S+ V20</a></li>
<li><a href="../whatsminer/M3X#m31s_1-v30">M31S+ V30</a></li> <li><a href="../whatsminer/M3X#m31s_1-v30">M31S+ V30</a></li>
@@ -238,7 +232,6 @@ details {
<li><a href="../whatsminer/M3X#m33-v20">M33 V20</a></li> <li><a href="../whatsminer/M3X#m33-v20">M33 V20</a></li>
<li><a href="../whatsminer/M3X#m33-v30">M33 V30</a></li> <li><a href="../whatsminer/M3X#m33-v30">M33 V30</a></li>
<li><a href="../whatsminer/M3X#m33s-vg30">M33S VG30</a></li> <li><a href="../whatsminer/M3X#m33s-vg30">M33S VG30</a></li>
<li><a href="../whatsminer/M3X#m33s_1-vg20">M33S+ VG20</a></li>
<li><a href="../whatsminer/M3X#m33s_1-vh20">M33S+ VH20</a></li> <li><a href="../whatsminer/M3X#m33s_1-vh20">M33S+ VH20</a></li>
<li><a href="../whatsminer/M3X#m33s_1-vh30">M33S+ VH30</a></li> <li><a href="../whatsminer/M3X#m33s_1-vh30">M33S+ VH30</a></li>
<li><a href="../whatsminer/M3X#m33s_1_1-vh20">M33S++ VH20</a></li> <li><a href="../whatsminer/M3X#m33s_1_1-vh20">M33S++ VH20</a></li>
@@ -248,15 +241,12 @@ details {
<li><a href="../whatsminer/M3X#m36s-ve10">M36S VE10</a></li> <li><a href="../whatsminer/M3X#m36s-ve10">M36S VE10</a></li>
<li><a href="../whatsminer/M3X#m36s_1-vg30">M36S+ VG30</a></li> <li><a href="../whatsminer/M3X#m36s_1-vg30">M36S+ VG30</a></li>
<li><a href="../whatsminer/M3X#m36s_1_1-vh30">M36S++ VH30</a></li> <li><a href="../whatsminer/M3X#m36s_1_1-vh30">M36S++ VH30</a></li>
<li><a href="../whatsminer/M3X#m39-v10">M39 V10</a></li>
<li><a href="../whatsminer/M3X#m39-v20">M39 V20</a></li> <li><a href="../whatsminer/M3X#m39-v20">M39 V20</a></li>
<li><a href="../whatsminer/M3X#m39-v30">M39 V30</a></li>
</ul> </ul>
</details> </details>
<details> <details>
<summary>M5X Series:</summary> <summary>M5X Series:</summary>
<ul> <ul>
<li><a href="../whatsminer/M5X#m50-ve30">M50 VE30</a></li>
<li><a href="../whatsminer/M5X#m50-vg30">M50 VG30</a></li> <li><a href="../whatsminer/M5X#m50-vg30">M50 VG30</a></li>
<li><a href="../whatsminer/M5X#m50-vh10">M50 VH10</a></li> <li><a href="../whatsminer/M5X#m50-vh10">M50 VH10</a></li>
<li><a href="../whatsminer/M5X#m50-vh20">M50 VH20</a></li> <li><a href="../whatsminer/M5X#m50-vh20">M50 VH20</a></li>
@@ -407,7 +397,6 @@ details {
<li><a href="../antminer/X19#s19j-bos">S19j (BOS)</a></li> <li><a href="../antminer/X19#s19j-bos">S19j (BOS)</a></li>
<li><a href="../antminer/X19#s19j-no-pic-bos">S19j No PIC (BOS)</a></li> <li><a href="../antminer/X19#s19j-no-pic-bos">S19j No PIC (BOS)</a></li>
<li><a href="../antminer/X19#s19j-pro-bos">S19j Pro (BOS)</a></li> <li><a href="../antminer/X19#s19j-pro-bos">S19j Pro (BOS)</a></li>
<li><a href="../antminer/X19#s19j-pro-bos">S19j Pro (BOS)</a></li>
<li><a href="../antminer/X19#t19-bos">T19 (BOS)</a></li> <li><a href="../antminer/X19#t19-bos">T19 (BOS)</a></li>
</ul> </ul>
</details> </details>
@@ -445,21 +434,6 @@ details {
</ul> </ul>
</details> </details>
<details> <details>
<summary>ePIC Firmware Miners:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-epic">S19 (ePIC)</a></li>
<li><a href="../antminer/X19#s19-pro-epic">S19 Pro (ePIC)</a></li>
<li><a href="../antminer/X19#s19j-epic">S19j (ePIC)</a></li>
<li><a href="../antminer/X19#s19j-pro-epic">S19j Pro (ePIC)</a></li>
<li><a href="../antminer/X19#s19-xp-epic">S19 XP (ePIC)</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>HiveOS Firmware Miners:</summary> <summary>HiveOS Firmware Miners:</summary>
<ul> <ul>
<details> <details>
@@ -480,4 +454,4 @@ details {
</ul> </ul>
</details> </details>
</ul> </ul>
</details> </details>

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1,27 +0,0 @@
# 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 [`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
## LUXMinerRPCAPI
::: pyasic.rpc.luxminer.LUXMinerRPCAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,24 +1,11 @@
# pyasic # pyasic
## settings ## settings
All settings here are global settings for all of pyasic. Set these settings with `update(key, value)`. ::: pyasic.settings
handler: python
Settings options: options:
show_root_heading: false
- `network_ping_retries` heading_level: 4
- `network_ping_timeout`
- `network_scan_threads`
- `factory_get_retries`
- `factory_get_timeout`
- `get_data_retries`
- `api_function_timeout`
- `default_whatsminer_password`
- `default_innosilicon_password`
- `default_antminer_password`
- `default_bosminer_password`
- `default_vnish_password`
- `default_goldshell_password`
- `socket_linger_time`
### get ### get

View File

@@ -4,24 +4,24 @@ nav:
- Introduction: "index.md" - Introduction: "index.md"
- Miners: - Miners:
- Supported Miners: "miners/supported_types.md" - Supported Miners: "miners/supported_types.md"
- Standard Functionality: "miners/functions.md"
- Miner Factory: "miners/miner_factory.md" - Miner Factory: "miners/miner_factory.md"
- Network: - Network:
- Miner Network: "network/miner_network.md" - Miner Network: "network/miner_network.md"
- Miner Network Range: "network/miner_network_range.md"
- Dataclasses: - Dataclasses:
- Miner Data: "data/miner_data.md" - Miner Data: "data/miner_data.md"
- Error Codes: "data/error_codes.md" - Error Codes: "data/error_codes.md"
- Miner Config: "config/miner_config.md" - Miner Config: "config/miner_config.md"
- Advanced: - Advanced:
- RPC APIs: - Miner APIs:
- Intro: "rpc/api.md" - Intro: "API/api.md"
- BFGMiner: "rpc/bfgminer.md" - BFGMiner: "API/bfgminer.md"
- BMMiner: "rpc/bmminer.md" - BMMiner: "API/bmminer.md"
- BOSMiner: "rpc/bosminer.md" - BOSMiner: "API/bosminer.md"
- BTMiner: "rpc/btminer.md" - BTMiner: "API/btminer.md"
- CGMiner: "rpc/cgminer.md" - CGMiner: "API/cgminer.md"
- LUXMiner: "rpc/luxminer.md" - LUXMiner: "API/luxminer.md"
- Unknown: "rpc/unknown.md" - Unknown: "API/unknown.md"
- Backends: - Backends:
- BMMiner: "miners/backends/bmminer.md" - BMMiner: "miners/backends/bmminer.md"
- BOSMiner: "miners/backends/bosminer.md" - BOSMiner: "miners/backends/bosminer.md"
@@ -30,7 +30,6 @@ nav:
- CGMiner: "miners/backends/cgminer.md" - CGMiner: "miners/backends/cgminer.md"
- LUXMiner: "miners/backends/luxminer.md" - LUXMiner: "miners/backends/luxminer.md"
- VNish: "miners/backends/vnish.md" - VNish: "miners/backends/vnish.md"
- ePIC: "miners/backends/epic.md"
- Hiveon: "miners/backends/hiveon.md" - Hiveon: "miners/backends/hiveon.md"
- Classes: - Classes:
- Antminer X3: "miners/antminer/X3.md" - Antminer X3: "miners/antminer/X3.md"

View File

@@ -23,26 +23,23 @@ import warnings
from typing import Union from typing import Union
from pyasic.errors import APIError, APIWarning from pyasic.errors import APIError, APIWarning
from pyasic.misc import validate_command_output
class BaseMinerRPCAPI: class BaseMinerAPI:
def __init__(self, ip: str, port: int = 4028, api_ver: str = "0.0.0") -> None: def __init__(self, ip: str, port: int = 4028) -> None:
# api port, should be 4028 # api port, should be 4028
self.port = port self.port = port
# ip address of the miner # ip address of the miner
self.ip = ipaddress.ip_address(ip) self.ip = ipaddress.ip_address(ip)
# api version if known
self.api_ver = api_ver
self.pwd = None self.pwd = "admin"
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls is BaseMinerRPCAPI: if cls is BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated") raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls) return object.__new__(cls)
def __repr__(self) -> str: def __repr__(self):
return f"{self.__class__.__name__}: {str(self.ip)}" return f"{self.__class__.__name__}: {str(self.ip)}"
async def send_command( async def send_command(
@@ -86,7 +83,7 @@ class BaseMinerRPCAPI:
data = self._load_api_data(data) data = self._load_api_data(data)
# check for if the user wants to allow errors to return # check for if the user wants to allow errors to return
validation = validate_command_output(data) validation = self._validate_command_output(data)
if not validation[0]: if not validation[0]:
if not ignore_errors: if not ignore_errors:
# validate the command succeeded # validate the command succeeded
@@ -111,41 +108,38 @@ class BaseMinerRPCAPI:
allow_warning: A boolean to supress APIWarnings. allow_warning: A boolean to supress APIWarnings.
""" """
# make sure we can actually run each command, otherwise they will fail while True:
commands = self._check_commands(*commands) # make sure we can actually run each command, otherwise they will fail
# standard multicommand format is "command1+command2" commands = self._check_commands(*commands)
# doesn't work for S19 which uses the backup _send_split_multicommand # standard multicommand format is "command1+command2"
command = "+".join(commands) # standard format doesn't work for X19
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError as e:
# try to identify the error
if e.message is not None:
if ":" in e.message:
err_command = e.message.split(":")[0]
if err_command in commands:
commands.remove(err_command)
continue
return {command: [{}] for command in commands}
logging.debug(f"{self} - (Multicommand) - Received data")
data["multicommand"] = True
return data
async def _handle_multicommand(self, command: str, allow_warning: bool = True):
try: try:
data = await self.send_command(command, allow_warning=allow_warning) data = await self.send_command(command, allow_warning=allow_warning)
if not "+" in command:
return {command: [data]}
return data
except APIError: except APIError:
data = await self._send_split_multicommand(*commands) if "+" in command:
data["multicommand"] = True return {command: [{}] for command in command.split("+")}
return data return {command: [{}]}
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:
try:
result = tasks[cmd].result()
if result is None or result == {}:
result = {}
data[cmd] = [result]
except APIError:
pass
return data
@property @property
def commands(self) -> list: def commands(self) -> list:
@@ -170,12 +164,12 @@ class BaseMinerRPCAPI:
func func
not in [ not in [
func func
for func in dir(BaseMinerRPCAPI) for func in dir(BaseMinerAPI)
if callable(getattr(BaseMinerRPCAPI, func)) if callable(getattr(BaseMinerAPI, func))
] ]
] ]
def _check_commands(self, *commands) -> list: def _check_commands(self, *commands):
allowed_commands = self.commands allowed_commands = self.commands
return_commands = [] return_commands = []
@@ -219,11 +213,11 @@ If you are sure you want to use this command please use API.send_command("{comma
# append that data if there is more, and then onto the main loop. # append that data if there is more, and then onto the main loop.
# the password timeout might need to be longer than 1, but it seems to work for now. # the password timeout might need to be longer than 1, but it seems to work for now.
ret_data = await asyncio.wait_for(reader.read(1), timeout=1) ret_data = await asyncio.wait_for(reader.read(1), timeout=1)
except asyncio.TimeoutError: except (asyncio.TimeoutError):
return b"{}" return b"{}"
try: try:
ret_data += await asyncio.wait_for(reader.read(4096), timeout=timeout) ret_data += await asyncio.wait_for(reader.read(4096), timeout=timeout)
except ConnectionAbortedError: except (ConnectionAbortedError):
return b"{}" return b"{}"
# loop to receive all the data # loop to receive all the data
@@ -249,6 +243,36 @@ If you are sure you want to use this command please use API.send_command("{comma
return ret_data 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 isinstance(data["STATUS"], str):
if data["STATUS"] in ["RESTART"]:
return True, None
elif isinstance(data["STATUS"], dict):
if data["STATUS"].get("STATUS") in ["S", "I"]:
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 @staticmethod
def _load_api_data(data: bytes) -> dict: def _load_api_data(data: bytes) -> dict:
# some json from the API returns with a null byte (\x00) on the end # some json from the API returns with a null byte (\x00) on the end

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 asyncio
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:
tasks = []
# send all commands individually
for cmd in commands:
tasks.append(
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
)
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
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}")

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

@@ -0,0 +1,731 @@
# ------------------------------------------------------------------------------
# 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 asyncio
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:
tasks = []
# send all commands individually
for cmd in commands:
tasks.append(
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
)
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
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. - # 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. """An abstraction of the BOSMiner API.
Each method corresponds to an API command in BOSMiner. 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 function handles sending a command to the miner asynchronously, and
as such is the base for many of the functions in this class, which as such is the base for many of the functions in this class, which
rely on it to send the command for them. 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: async def asccount(self) -> dict:
"""Get data on the number of ASC devices and their info. """Get data on the number of ASC devices and their info.
<details> <details>

View File

@@ -28,9 +28,9 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from passlib.handlers.md5_crypt import md5_crypt from passlib.handlers.md5_crypt import md5_crypt
from pyasic import settings from pyasic import settings
from pyasic.API import BaseMinerAPI
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.misc import api_min_version from pyasic.misc import api_min_version
from pyasic.rpc.base import BaseMinerRPCAPI
### IMPORTANT ### ### IMPORTANT ###
# you need to change the password of the miners using the Whatsminer # you need to change the password of the miners using the Whatsminer
@@ -48,10 +48,10 @@ PrePowerOnMessage = Union[
def _crypt(word: str, salt: str) -> str: 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 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. ValueError is raised.
Parameters: Parameters:
@@ -62,7 +62,7 @@ def _crypt(word: str, salt: str) -> str:
An MD5 hash of the word with the salt. An MD5 hash of the word with the salt.
""" """
# compile a standard format for 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 # check if the salt matches
match = standard_salt.match(salt) match = standard_salt.match(salt)
# if the matching fails, the salt is incorrect # if the matching fails, the salt is incorrect
@@ -89,7 +89,7 @@ def _add_to_16(string: str) -> bytes:
return str.encode(string) # return 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 returned from the BTMiner privileged API.
Parses data from the BTMiner privileged API using the token Parses data from the BTMiner privileged API using the token
@@ -159,7 +159,7 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
return api_packet_str.encode("utf-8") return api_packet_str.encode("utf-8")
class BTMinerRPCAPI(BaseMinerRPCAPI): class BTMinerAPI(BaseMinerAPI):
"""An abstraction of the API for MicroBT Whatsminers, BTMiner. """An abstraction of the API for MicroBT Whatsminers, BTMiner.
Each method corresponds to an API command in BMMiner. Each method corresponds to an API command in BMMiner.
@@ -180,12 +180,24 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
encoded using a token from the miner, all privileged commands do encoded using a token from the miner, all privileged commands do
this automatically for you and will decode the output to look like this automatically for you and will decode the output to look like
a normal output from a miner API. 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: def __init__(
super().__init__(ip, port, api_ver) self,
self.pwd = settings.get("default_whatsminer_rpc_password", "admin") ip: str,
self.token = None api_ver: str = "0.0.0",
port: int = 4028,
pwd: str = settings.get("default_whatsminer_password", "admin"),
):
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: async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner. """Creates and sends multiple commands as one command to the miner.
@@ -199,33 +211,28 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
# standard multicommand format is "command1+command2" # standard multicommand format is "command1+command2"
# commands starting with "get_" and the "status" command aren't supported, but we can fake that # commands starting with "get_" and the "status" command aren't supported, but we can fake that
split_commands = [] tasks = []
for command in list(commands): for command in list(commands):
if command.startswith("get_") or command == "status": if command.startswith("get_") or command == "status":
commands.remove(command) commands.remove(command)
# send seperately and append later # send seperately and append later
split_commands.append(command) tasks.append(
asyncio.create_task(
command = "+".join(commands) self._handle_multicommand(command, allow_warning=allow_warning)
tasks = []
if len(split_commands) > 0:
tasks.append(
asyncio.create_task(
self._send_split_multicommand(
*split_commands, allow_warning=allow_warning
) )
) )
)
command = "+".join(commands)
tasks.append( tasks.append(
asyncio.create_task(self.send_command(command, allow_warning=allow_warning)) asyncio.create_task(
self._handle_multicommand(command, allow_warning=allow_warning)
)
) )
try: all_data = await asyncio.gather(*tasks)
all_data = await asyncio.gather(*tasks)
except APIError: logging.debug(f"{self} - (Multicommand) - Received data")
return {}
data = {} data = {}
for item in all_data: for item in all_data:
@@ -266,7 +273,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
data = self._load_api_data(data) data = self._load_api_data(data)
try: try:
data = parse_btminer_priviledge_data(self.token, data) data = parse_btminer_priviledge_data(self.current_token, data)
except Exception as e: except Exception as e:
logging.info(f"{str(self.ip)}: {e}") logging.info(f"{str(self.ip)}: {e}")
@@ -289,11 +296,11 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
</details> </details>
""" """
logging.debug(f"{self} - (Get Token) - Getting token") logging.debug(f"{self} - (Get Token) - Getting token")
if self.token: if self.current_token:
if self.token["timestamp"] > datetime.datetime.now() - datetime.timedelta( if self.current_token[
minutes=30 "timestamp"
): ] > datetime.datetime.now() - datetime.timedelta(minutes=30):
return self.token return self.current_token
# get the token # get the token
data = await self.send_command("get_token") data = await self.send_command("get_token")
@@ -313,13 +320,15 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
host_sign = tmp[3] host_sign = tmp[3]
# set the current token # set the current token
self.token = { self.current_token = {
"host_sign": host_sign, "host_sign": host_sign,
"host_passwd_md5": host_passwd_md5, "host_passwd_md5": host_passwd_md5,
"timestamp": datetime.datetime.now(), "timestamp": datetime.datetime.now(),
} }
logging.debug(f"{self} - (Get Token) - Gathered token data: {self.token}") logging.debug(
return self.token f"{self} - (Get Token) - Gathered token data: {self.current_token}"
)
return self.current_token
#### PRIVILEGED COMMANDS #### #### PRIVILEGED COMMANDS ####
# Please read the top of this file to learn # Please read the top of this file to learn
@@ -478,34 +487,6 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
""" """
return await self.send_privileged_command("set_low_power") return await self.send_privileged_command("set_low_power")
async def set_high_power(self) -> dict:
"""Set low power mode on the miner using the API.
<details>
<summary>Expand</summary>
Set low power mode on the miner using the API, only works after
changing the password of the miner using the Whatsminer tool.
Returns:
A reply informing of the status of setting low power mode.
</details>
"""
return await self.send_privileged_command("set_high_power")
async def set_normal_power(self) -> dict:
"""Set low power mode on the miner using the API.
<details>
<summary>Expand</summary>
Set low power mode on the miner using the API, only works after
changing the password of the miner using the Whatsminer tool.
Returns:
A reply informing of the status of setting low power mode.
</details>
"""
return await self.send_privileged_command("set_normal_power")
async def update_firmware(self): # noqa - static async def update_firmware(self): # noqa - static
"""Not implemented.""" """Not implemented."""
# to be determined if this will be added later # to be determined if this will be added later

View File

@@ -13,11 +13,13 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import logging
from pyasic.rpc.base import BaseMinerRPCAPI from pyasic.API import APIError, BaseMinerAPI
class CGMinerRPCAPI(BaseMinerRPCAPI): class CGMinerAPI(BaseMinerAPI):
"""An abstraction of the CGMiner API. """An abstraction of the CGMiner API.
Each method corresponds to an API command in GGMiner. 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 function handles sending a command to the miner asynchronously, and
as such is the base for many of the functions in this class, which as such is the base for many of the functions in this class, which
rely on it to send the command for them. 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:
tasks = []
# send all commands individually
for cmd in commands:
tasks.append(
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
)
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data
async def version(self) -> dict: async def version(self) -> dict:
"""Get miner version info. """Get miner version info.
<details> <details>
@@ -519,8 +559,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI):
<summary>Expand</summary> <summary>Expand</summary>
Parameters: Parameters:
which: Which device to zero. Setting this to 'all' zeros all devices. 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.
Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
summary: Whether or not to show a full summary. summary: Whether or not to show a full summary.

View File

@@ -15,10 +15,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import Literal from typing import Literal
from pyasic.rpc.base import BaseMinerRPCAPI from pyasic.API import BaseMinerAPI
class LUXMinerRPCAPI(BaseMinerRPCAPI): class LUXMinerAPI(BaseMinerAPI):
"""An abstraction of the LUXMiner API. """An abstraction of the LUXMiner API.
Each method corresponds to an API command in LUXMiner. Each method corresponds to an API command in LUXMiner.
@@ -30,8 +30,16 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
function handles sending a command to the miner asynchronously, and function handles sending a command to the miner asynchronously, and
as such is the base for many of the functions in this class, which as such is the base for many of the functions in this class, which
rely on it to send the command for them. 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 addgroup(self, name: str, quota: int) -> dict: async def addgroup(self, name: str, quota: int) -> dict:
"""Add a pool group. """Add a pool group.
<details> <details>

View File

@@ -14,10 +14,10 @@
# limitations under the License. - # 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. """An abstraction of an API for a miner which is unknown.
This class is designed to try to be an intersection of as many miner APIs This class is designed to try to be an intersection of as many miner APIs
@@ -25,6 +25,52 @@ class UnknownRPCAPI(CGMinerRPCAPI):
with as many APIs as possible. 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: async def switchpool(self, n: int) -> dict:
# BOS has not implemented this yet, they will in the future # BOS has not implemented this yet, they will in the future
raise NotImplementedError raise NotImplementedError

View File

@@ -14,11 +14,45 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic import settings 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.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.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, miner_factory
from pyasic.miners.miner_listener import MinerListener
from pyasic.network import MinerNetwork from pyasic.network import MinerNetwork
from pyasic.rpc import *
from pyasic.ssh import * __all__ = [
from pyasic.web import * "BMMinerAPI",
"BOSMinerAPI",
"BTMinerAPI",
"CGMinerAPI",
"UnknownAPI",
"MinerConfig",
"MinerData",
"BraiinsOSError",
"InnosiliconError",
"WhatsminerError",
"X19Error",
"APIError",
"APIWarning",
"get_miner",
"AnyMiner",
"MinerFactory",
"miner_factory",
"MinerListener",
"MinerNetwork",
"settings",
]

View File

@@ -13,194 +13,664 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from dataclasses import asdict, dataclass, field
from pyasic.config.fans import FanModeConfig import logging
from pyasic.config.mining import MiningModeConfig import random
from pyasic.config.pools import PoolConfig import string
from pyasic.config.power_scaling import PowerScalingConfig import time
from pyasic.config.temperature import TemperatureConfig from dataclasses import asdict, dataclass, fields
from pyasic.misc import merge_dicts from enum import IntEnum
from typing import List, Literal
import toml
import yaml
class X19PowerMode(IntEnum):
Normal = 0
Sleep = 1
LPM = 3
@dataclass
class _Pool:
"""A dataclass for pool information.
Attributes:
url: URL of the pool.
username: Username on the pool.
password: Worker password on the pool.
"""
url: str = ""
username: str = ""
password: str = ""
@classmethod
def fields(cls):
return fields(cls)
def from_dict(self, data: dict):
"""Convert raw pool data as a dict to usable data and save it to this class.
Parameters:
data: The raw config data to convert.
"""
for key in data.keys():
if key == "url":
self.url = data[key]
if key in ["user", "username"]:
self.username = data[key]
if key in ["pass", "password"]:
self.password = data[key]
return self
def as_wm(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an Whatsminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_x19(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an X19 device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_x17(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an X5 device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_goldshell(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by a goldshell device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {
f"Pool": self.url,
f"UserName": username,
f"Password": self.password,
}
return pool
def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a string usable by an Avalonminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = ",".join([self.url, username, self.password])
return pool
def as_bos(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an BOSMiner device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "password": self.password}
return pool
@dataclass
class _PoolGroup:
"""A dataclass for pool group information.
Attributes:
quota: The group quota.
group_name: The name of the pool group.
pools: A list of pools in this group.
"""
quota: int = 1
group_name: str = None
pools: List[_Pool] = None
@classmethod
def fields(cls):
return fields(cls)
def __post_init__(self):
if not self.group_name:
self.group_name = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(6)
) # generate random pool group name in case it isn't set
def from_dict(self, data: dict):
"""Convert raw pool group data as a dict to usable data and save it to this class.
Parameters:
data: The raw config data to convert.
"""
pools = []
for key in data.keys():
if key in ["name", "group_name"]:
self.group_name = data[key]
if key == "quota":
self.quota = data[key]
if key in ["pools", "pool"]:
for pool in data[key]:
pools.append(_Pool().from_dict(pool))
self.pools = pools
return self
def as_x19(self, user_suffix: str = None) -> List[dict]:
"""Convert the data in this class to a list usable by an X19 device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = []
for pool in self.pools[:3]:
pools.append(pool.as_x19(user_suffix=user_suffix))
return pools
def as_x17(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by an X17 device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = {
"_ant_pool1url": "",
"_ant_pool1user": "",
"_ant_pool1pw": "",
"_ant_pool2url": "",
"_ant_pool2user": "",
"_ant_pool2pw": "",
"_ant_pool3url": "",
"_ant_pool3user": "",
"_ant_pool3pw": "",
}
for idx, pool in enumerate(self.pools[:3]):
pools[f"_ant_pool{idx+1}url"] = pool.as_x17(user_suffix=user_suffix)["url"]
pools[f"_ant_pool{idx+1}user"] = pool.as_x17(user_suffix=user_suffix)[
"user"
]
pools[f"_ant_pool{idx+1}pw"] = pool.as_x17(user_suffix=user_suffix)["pass"]
return pools
def as_goldshell(self, user_suffix: str = None) -> list:
"""Convert the data in this class to a list usable by a goldshell device.
Parameters:
user_suffix: The suffix to append to username.
"""
return [pool.as_goldshell(user_suffix=user_suffix) for pool in self.pools[:3]]
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = {
"Pool1": None,
"UserName1": None,
"Password1": None,
"Pool2": None,
"UserName2": None,
"Password2": None,
"Pool3": None,
"UserName3": None,
"Password3": None,
}
for idx, pool in enumerate(self.pools[:3]):
pool_data = pool.as_inno(user_suffix=user_suffix)
for key in pool_data:
pools[f"{key}{idx+1}"] = pool_data[key]
return pools
def as_wm(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by a Whatsminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = {}
for i in range(1, 4):
if i <= len(self.pools):
pool_wm = self.pools[i - 1].as_wm(user_suffix)
pools[f"pool_{i}"] = pool_wm["url"]
pools[f"worker_{i}"] = pool_wm["user"]
pools[f"passwd_{i}"] = pool_wm["pass"]
else:
pools[f"pool_{i}"] = ""
pools[f"worker_{i}"] = ""
pools[f"passwd_{i}"] = ""
return pools
def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a dict usable by an Avalonminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
pool = self.pools[0].as_avalon(user_suffix=user_suffix)
return pool
def as_bos(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an BOSMiner device.
Parameters:
user_suffix: The suffix to append to username.
"""
group = {
"name": self.group_name,
"quota": self.quota,
"pool": [pool.as_bos(user_suffix=user_suffix) for pool in self.pools],
}
return group
@dataclass @dataclass
class MinerConfig: class MinerConfig:
pools: PoolConfig = field(default_factory=PoolConfig.default) """A dataclass for miner configuration information.
fan_mode: FanModeConfig = field(default_factory=FanModeConfig.default)
temperature: TemperatureConfig = field(default_factory=TemperatureConfig.default) Attributes:
mining_mode: MiningModeConfig = field(default_factory=MiningModeConfig.default) pool_groups: A list of pool groups in this config.
power_scaling: PowerScalingConfig = field( temp_mode: The temperature control mode.
default_factory=PowerScalingConfig.default temp_target: The target temp.
) temp_hot: The hot temp (100% fans).
temp_dangerous: The dangerous temp (shutdown).
minimum_fans: The minimum numbers of fans to run the miner.
fan_speed: Manual fan speed to run the fan at (only if temp_mode == "manual").
asicboost: Whether or not to enable asicboost.
autotuning_enabled: Whether or not to enable autotuning.
autotuning_mode: Autotuning mode, either "wattage" or "hashrate".
autotuning_wattage: The wattage to use when autotuning.
autotuning_hashrate: The hashrate to use when autotuning.
dps_enabled: Whether or not to enable dynamic power scaling.
dps_power_step: The amount of power to reduce autotuning by when the miner reaches dangerous temp.
dps_min_power: The minimum power to reduce autotuning to.
dps_shutdown_enabled: Whether or not to shutdown the miner when `dps_min_power` is reached.
dps_shutdown_duration: The amount of time to shutdown for (in hours).
"""
pool_groups: List[_PoolGroup] = None
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
temp_target: float = 70.0
temp_hot: float = 80.0
temp_dangerous: float = 100.0
minimum_fans: int = None
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
asicboost: bool = None
miner_mode: IntEnum = X19PowerMode.Normal
autotuning_enabled: bool = True
autotuning_mode: Literal["power", "hashrate"] = None
autotuning_wattage: int = None
autotuning_hashrate: int = None
dps_enabled: bool = None
dps_power_step: int = None
dps_min_power: int = None
dps_shutdown_enabled: bool = None
dps_shutdown_duration: float = None
@classmethod
def fields(cls):
return fields(cls)
def as_dict(self) -> dict: def as_dict(self) -> dict:
return asdict(self) """Convert the data in this class to a dict."""
logging.debug(f"MinerConfig - (To Dict) - Dumping Dict config")
data_dict = asdict(self)
for key in asdict(self).keys():
if isinstance(data_dict[key], IntEnum):
data_dict[key] = data_dict[key].value
if data_dict[key] is None:
del data_dict[key]
return data_dict
def as_am_modern(self, user_suffix: str = None) -> dict: def as_toml(self) -> str:
return { """Convert the data in this class to toml."""
**self.fan_mode.as_am_modern(), logging.debug(f"MinerConfig - (To TOML) - Dumping TOML config")
"freq-level": "100", return toml.dumps(self.as_dict())
**self.mining_mode.as_am_modern(),
**self.pools.as_am_modern(user_suffix=user_suffix), def as_yaml(self) -> str:
**self.temperature.as_am_modern(), """Convert the data in this class to yaml."""
**self.power_scaling.as_am_modern(), logging.debug(f"MinerConfig - (To YAML) - Dumping YAML config")
} return yaml.dump(self.as_dict(), sort_keys=False)
def from_raw(self, data: dict):
"""Convert raw config data as a dict to usable data and save it to this class.
This should be able to handle any raw config file from any miner supported by pyasic.
Parameters:
data: The raw config data to convert.
"""
logging.debug(f"MinerConfig - (From Raw) - Loading raw config")
pool_groups = []
if isinstance(data, list):
# goldshell config list
data = {"pools": data}
for key in data.keys():
if key == "pools":
pool_groups.append(_PoolGroup().from_dict({"pools": data[key]}))
elif key == "group":
for group in data[key]:
pool_groups.append(_PoolGroup().from_dict(group))
if key == "bitmain-fan-ctrl":
if data[key]:
self.temp_mode = "manual"
if data.get("bitmain-fan-pwm"):
self.fan_speed = int(data["bitmain-fan-pwm"])
elif key == "bitmain-work-mode":
if data[key]:
self.miner_mode = X19PowerMode(int(data[key]))
elif key == "fan_control":
for _key in data[key]:
if _key == "min_fans":
self.minimum_fans = data[key][_key]
elif _key == "speed":
self.fan_speed = data[key][_key]
elif key == "temp_control":
for _key in data[key]:
if _key == "mode":
self.temp_mode = data[key][_key]
elif _key == "target_temp":
self.temp_target = data[key][_key]
elif _key == "hot_temp":
self.temp_hot = data[key][_key]
elif _key == "dangerous_temp":
self.temp_dangerous = data[key][_key]
if key == "hash_chain_global":
if data[key].get("asic_boost"):
self.asicboost = data[key]["asic_boost"]
if key == "autotuning":
for _key in data[key]:
if _key == "enabled":
self.autotuning_enabled = data[key][_key]
elif _key == "psu_power_limit":
self.autotuning_wattage = data[key][_key]
elif _key == "power_target":
self.autotuning_wattage = data[key][_key]
elif _key == "hashrate_target":
self.autotuning_hashrate = data[key][_key]
elif _key == "mode":
self.autotuning_mode = data[key][_key].replace("_target", "")
if key in ["power_scaling", "performance_scaling"]:
for _key in data[key]:
if _key == "enabled":
self.dps_enabled = data[key][_key]
elif _key == "power_step":
self.dps_power_step = data[key][_key]
elif _key in ["min_psu_power_limit", "min_power_target"]:
self.dps_min_power = data[key][_key]
elif _key == "shutdown_enabled":
self.dps_shutdown_enabled = data[key][_key]
elif _key == "shutdown_duration":
self.dps_shutdown_duration = data[key][_key]
self.pool_groups = pool_groups
return self
def from_api(self, pools: list):
"""Convert list output from the `AnyMiner.api.pools()` command into a usable data and save it to this class.
Parameters:
pools: The list of pool data to convert.
"""
logging.debug(f"MinerConfig - (From API) - Loading API config")
_pools = []
for pool in pools:
url = pool.get("URL")
user = pool.get("User")
_pools.append({"url": url, "user": user, "pass": "123"})
self.pool_groups = [_PoolGroup().from_dict({"pools": _pools})]
return self
def from_dict(self, data: dict):
"""Convert an output dict of this class back into usable data and save it to this class.
Parameters:
data: The dict config data to convert.
"""
logging.debug(f"MinerConfig - (From Dict) - Loading Dict config")
pool_groups = []
for group in data["pool_groups"]:
pool_groups.append(_PoolGroup().from_dict(group))
for key in data:
if (
hasattr(self, key)
and not key == "pool_groups"
and not key == "miner_mode"
):
setattr(self, key, data[key])
if key == "miner_mode":
self.miner_mode = X19PowerMode(data[key])
self.pool_groups = pool_groups
return self
def from_toml(self, data: str):
"""Convert output toml of this class back into usable data and save it to this class.
Parameters:
data: The toml config data to convert.
"""
logging.debug(f"MinerConfig - (From TOML) - Loading TOML config")
return self.from_dict(toml.loads(data))
def from_yaml(self, data: str):
"""Convert output yaml of this class back into usable data and save it to this class.
Parameters:
data: The yaml config data to convert.
"""
logging.debug(f"MinerConfig - (From YAML) - Loading YAML config")
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
def as_wm(self, user_suffix: str = None) -> dict: def as_wm(self, user_suffix: str = None) -> dict:
return { """Convert the data in this class to a config usable by a Whatsminer device.
**self.fan_mode.as_wm(),
**self.mining_mode.as_wm(),
**self.pools.as_wm(user_suffix=user_suffix),
**self.temperature.as_wm(),
**self.power_scaling.as_wm(),
}
def as_am_old(self, user_suffix: str = None) -> dict: Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Whatsminer) - Generating Whatsminer config")
return { return {
**self.fan_mode.as_am_old(), "pools": self.pool_groups[0].as_wm(user_suffix=user_suffix),
**self.mining_mode.as_am_old(), "wattage": self.autotuning_wattage,
**self.pools.as_am_old(user_suffix=user_suffix),
**self.temperature.as_am_old(),
**self.power_scaling.as_am_old(),
}
def as_goldshell(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_goldshell(),
**self.mining_mode.as_goldshell(),
**self.pools.as_goldshell(user_suffix=user_suffix),
**self.temperature.as_goldshell(),
**self.power_scaling.as_goldshell(),
}
def as_avalon(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_avalon(),
**self.mining_mode.as_avalon(),
**self.pools.as_avalon(user_suffix=user_suffix),
**self.temperature.as_avalon(),
**self.power_scaling.as_avalon(),
} }
def as_inno(self, user_suffix: str = None) -> dict: def as_inno(self, user_suffix: str = None) -> dict:
return { """Convert the data in this class to a config usable by an Innosilicon device.
**self.fan_mode.as_inno(),
**self.mining_mode.as_inno(), Parameters:
**self.pools.as_inno(user_suffix=user_suffix), user_suffix: The suffix to append to username.
**self.temperature.as_inno(), """
**self.power_scaling.as_inno(), logging.debug(f"MinerConfig - (As Inno) - Generating Innosilicon config")
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
def as_x19(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by an X19 device.
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As X19) - Generating X19 config")
cfg = {
"bitmain-fan-ctrl": False,
"bitmain-fan-pwn": "100",
"freq-level": "100",
"miner-mode": str(self.miner_mode.value),
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
} }
def as_bosminer(self, user_suffix: str = None) -> dict: if not self.temp_mode == "auto":
return { cfg["bitmain-fan-ctrl"] = True
**merge_dicts(self.fan_mode.as_bosminer(), self.temperature.as_bosminer()),
**self.mining_mode.as_bosminer(), if self.fan_speed:
**self.pools.as_bosminer(user_suffix=user_suffix), cfg["bitmain-fan-pwn"] = str(self.fan_speed)
**self.power_scaling.as_bosminer(),
return cfg
def as_x17(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by an X5 device.
Parameters:
user_suffix: The suffix to append to username.
"""
cfg = self.pool_groups[0].as_x17(user_suffix=user_suffix)
return cfg
def as_goldshell(self, user_suffix: str = None) -> list:
"""Convert the data in this class to a config usable by a goldshell device.
Parameters:
user_suffix: The suffix to append to username.
"""
cfg = self.pool_groups[0].as_goldshell(user_suffix=user_suffix)
return cfg
def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an Avalonminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Avalon) - Generating AvalonMiner config")
cfg = self.pool_groups[0].as_avalon(user_suffix=user_suffix)
return cfg
def as_bos(self, model: str = "S9", user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an BOSMiner device.
Parameters:
model: The model of the miner to be used in the format portion of the config.
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As BOS) - Generating BOSMiner config")
cfg = {
"format": {
"version": "1.2+",
"model": f"Antminer {model.replace('j', 'J')}",
"generator": "pyasic",
"timestamp": int(time.time()),
},
"group": [
group.as_bos(user_suffix=user_suffix) for group in self.pool_groups
],
"temp_control": {
"mode": self.temp_mode,
"target_temp": self.temp_target,
"hot_temp": self.temp_hot,
"dangerous_temp": self.temp_dangerous,
},
} }
def as_boser(self, user_suffix: str = None) -> dict: if self.autotuning_enabled or self.autotuning_wattage:
return { cfg["autotuning"] = {}
**self.fan_mode.as_boser(), if self.autotuning_enabled:
**self.temperature.as_boser(), cfg["autotuning"]["enabled"] = True
**self.mining_mode.as_boser(), else:
**self.pools.as_boser(user_suffix=user_suffix), cfg["autotuning"]["enabled"] = False
**self.power_scaling.as_boser(), if self.autotuning_mode:
} cfg["format"]["version"] = "2.0"
cfg["autotuning"]["mode"] = self.autotuning_mode + "_target"
if self.autotuning_wattage:
cfg["autotuning"]["power_target"] = self.autotuning_wattage
elif self.autotuning_hashrate:
cfg["autotuning"]["hashrate_target"] = self.autotuning_hashrate
else:
if self.autotuning_wattage:
cfg["autotuning"]["psu_power_limit"] = self.autotuning_wattage
def as_epic(self, user_suffix: str = None) -> dict: if self.asicboost:
return { cfg["hash_chain_global"] = {}
**merge_dicts(self.fan_mode.as_epic(), self.temperature.as_epic()), cfg["hash_chain_global"]["asic_boost"] = self.asicboost
**self.mining_mode.as_epic(),
**self.pools.as_epic(user_suffix=user_suffix),
**self.power_scaling.as_epic(),
}
def as_auradine(self, user_suffix: str = None) -> dict: if self.minimum_fans is not None or self.fan_speed is not None:
return { cfg["fan_control"] = {}
**self.fan_mode.as_auradine(), if self.minimum_fans is not None:
**self.temperature.as_auradine(), cfg["fan_control"]["min_fans"] = self.minimum_fans
**self.mining_mode.as_auradine(), if self.fan_speed is not None:
**self.pools.as_auradine(user_suffix=user_suffix), cfg["fan_control"]["speed"] = self.fan_speed
**self.power_scaling.as_auradine(),
}
@classmethod if any(
def from_dict(cls, dict_conf: dict) -> "MinerConfig": [
return cls( getattr(self, item)
pools=PoolConfig.from_dict(dict_conf.get("pools")), for item in [
mining_mode=MiningModeConfig.from_dict(dict_conf.get("mining_mode")), "dps_enabled",
fan_mode=FanModeConfig.from_dict(dict_conf.get("fan_mode")), "dps_power_step",
temperature=TemperatureConfig.from_dict(dict_conf.get("temperature")), "dps_min_power",
power_scaling=PowerScalingConfig.from_dict(dict_conf.get("power_scaling")), "dps_shutdown_enabled",
) "dps_shutdown_duration",
]
]
):
cfg["power_scaling"] = {}
if self.dps_enabled:
cfg["power_scaling"]["enabled"] = self.dps_enabled
if self.dps_power_step:
cfg["power_scaling"]["power_step"] = self.dps_power_step
if self.dps_min_power:
if cfg["format"]["version"] == "2.0":
cfg["power_scaling"]["min_power_target"] = self.dps_min_power
else:
cfg["power_scaling"]["min_psu_power_limit"] = self.dps_min_power
if self.dps_shutdown_enabled:
cfg["power_scaling"]["shutdown_enabled"] = self.dps_shutdown_enabled
if self.dps_shutdown_duration:
cfg["power_scaling"]["shutdown_duration"] = self.dps_shutdown_duration
@classmethod return toml.dumps(cfg)
def from_api(cls, api_pools: dict) -> "MinerConfig":
return cls(pools=PoolConfig.from_api(api_pools))
@classmethod
def from_am_modern(cls, web_conf: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_am_modern(web_conf),
mining_mode=MiningModeConfig.from_am_modern(web_conf),
fan_mode=FanModeConfig.from_am_modern(web_conf),
)
@classmethod
def from_am_old(cls, web_conf: dict) -> "MinerConfig":
return cls.from_am_modern(web_conf)
@classmethod
def from_goldshell(cls, web_conf: dict) -> "MinerConfig":
return cls(pools=PoolConfig.from_am_modern(web_conf))
@classmethod
def from_inno(cls, web_pools: list) -> "MinerConfig":
return cls(pools=PoolConfig.from_inno(web_pools))
@classmethod
def from_bosminer(cls, toml_conf: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_bosminer(toml_conf),
mining_mode=MiningModeConfig.from_bosminer(toml_conf),
fan_mode=FanModeConfig.from_bosminer(toml_conf),
temperature=TemperatureConfig.from_bosminer(toml_conf),
power_scaling=PowerScalingConfig.from_bosminer(toml_conf),
)
@classmethod
def from_boser(cls, grpc_miner_conf: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_boser(grpc_miner_conf),
mining_mode=MiningModeConfig.from_boser(grpc_miner_conf),
fan_mode=FanModeConfig.from_boser(grpc_miner_conf),
temperature=TemperatureConfig.from_boser(grpc_miner_conf),
power_scaling=PowerScalingConfig.from_boser(grpc_miner_conf),
)
@classmethod
def from_epic(cls, web_conf: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_epic(web_conf),
fan_mode=FanModeConfig.from_epic(web_conf),
temperature=TemperatureConfig.from_epic(web_conf),
mining_mode=MiningModeConfig.from_epic(web_conf),
)
@classmethod
def from_vnish(cls, web_settings: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_vnish(web_settings),
fan_mode=FanModeConfig.from_vnish(web_settings),
temperature=TemperatureConfig.from_vnish(web_settings),
mining_mode=MiningModeConfig.from_vnish(web_settings),
)
@classmethod
def from_auradine(cls, web_conf: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_api(web_conf["pools"]),
fan_mode=FanModeConfig.from_auradine(web_conf["fan"]),
mining_mode=MiningModeConfig.from_auradine(web_conf["mode"]),
)

View File

@@ -1,108 +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 asdict, dataclass
from enum import Enum
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_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 __call__(self, *args, **kwargs):
return self.value(*args, **kwargs)
@classmethod
def default(cls):
pass
@dataclass
class MinerConfigValue:
@classmethod
def from_dict(cls, dict_conf: dict | None):
return cls()
def as_dict(self) -> dict:
return asdict(self)
def as_am_modern(self) -> dict:
return {}
def as_am_old(self) -> dict:
return {}
def as_wm(self) -> dict:
return {}
def as_inno(self) -> dict:
return {}
def as_goldshell(self) -> dict:
return {}
def as_avalon(self) -> dict:
return {}
def as_bosminer(self) -> dict:
return {}
def as_boser(self) -> dict:
return {}
def as_epic(self) -> dict:
return {}
def as_vnish(self) -> dict:
return {}
def as_auradine(self) -> dict:
return {}

View File

@@ -1,235 +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 dataclass, field
from pyasic.config.base import MinerConfigOption, MinerConfigValue
@dataclass
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)
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": False, "bitmain-fan-pwn": "100"}
def as_bosminer(self) -> dict:
return {"temp_control": {"mode": "auto"}}
def as_epic(self) -> dict:
return {
"fans": {
"Auto": {
"Idle Speed": self.minimum_speed
if not self.minimum_speed == 0
else 100
}
}
}
@dataclass
class FanModeManual(MinerConfigValue):
mode: str = field(init=False, default="manual")
speed: int = 100
minimum_fans: int = 1
@classmethod
def from_dict(cls, dict_conf: 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-pwn": 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}}}
@dataclass
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-pwn": "0"}
def as_bosminer(self) -> dict:
return {"temp_control": {"mode": "disabled"}}
def as_auradine(self) -> dict:
return {"fan": {"percentage": 0}}
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:
return cls.manual(speed=web_conf["bitmain-fan-pwm"])
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):
if toml_conf.get("temp_control") is None:
return cls.default()
if toml_conf["temp_control"].get("mode") is None:
return cls.default()
mode = toml_conf["temp_control"]["mode"]
if mode == "auto":
return cls.normal()
elif mode == "manual":
if toml_conf.get("fan_control"):
return cls.manual().from_bosminer(toml_conf["fan_control"])
return cls.manual()
elif mode == "disabled":
return cls.immersion()
@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(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)
@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:
return cls.default()

View File

@@ -1,415 +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 dataclass, field
from pyasic.config.base import MinerConfigOption, MinerConfigValue
from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
HashrateTargetMode,
PerformanceMode,
Power,
PowerTargetMode,
SaveAction,
SetPerformanceModeRequest,
TeraHashrate,
TunerPerformanceMode,
)
@dataclass
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:
return {"miner-mode": "0"}
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"mode": self.mode}}
def as_epic(self) -> dict:
return {"ptune": {"enabled": False}}
@dataclass
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:
return {"miner-mode": "1"}
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"sleep": "on"}}
def as_epic(self) -> dict:
return {"ptune": {"algo": "Sleep", "target": 0}}
@dataclass
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:
return {"miner-mode": "3"}
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"mode": "eco"}}
@dataclass
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:
return {"miner-mode": "0"}
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"mode": "turbo"}}
class StandardPowerTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard")
def as_epic(self):
return VOptPowerTuneAlgo().as_epic()
class VOptPowerTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard")
def as_epic(self):
return "VoltageOptimizer"
class ChipTunePowerTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard")
def as_epic(self):
return "ChipTune"
class PowerTunerAlgo(MinerConfigOption):
standard = StandardPowerTuneAlgo
voltage_optimizer = VOptPowerTuneAlgo
chip_tune = ChipTunePowerTuneAlgo
@classmethod
def default(cls):
return cls.standard()
@dataclass
class MiningModePowerTune(MinerConfigValue):
mode: str = field(init=False, default="power_tuning")
power: int = None
algo: PowerTunerAlgo = field(default_factory=PowerTunerAlgo.default)
@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"] = dict_conf["algo"]
return cls(**cls_conf)
def as_am_modern(self) -> dict:
return {"miner-mode": "0"}
def as_wm(self) -> dict:
if self.power is not None:
return {"mode": self.mode, self.mode: {"wattage": self.power}}
return {}
def as_bosminer(self) -> dict:
return {"autotuning": {"enabled": True, "psu_power_limit": self.power}}
def as_boser(self) -> dict:
return {
"set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
mode=PerformanceMode(
tuner_mode=TunerPerformanceMode(
power_target=PowerTargetMode(
power_target=Power(watt=self.power)
)
)
),
),
}
def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "power", "power": self.power}}
def as_epic(self) -> dict:
return {"ptune": {**self.algo.as_epic(), "target": self.power}}
@dataclass
class MiningModeHashrateTune(MinerConfigValue):
mode: str = field(init=False, default="hashrate_tuning")
hashrate: int = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune":
return cls(dict_conf.get("hashrate"))
def as_am_modern(self) -> dict:
return {"miner-mode": "0"}
def as_boser(self) -> dict:
return {
"set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
mode=PerformanceMode(
tuner_mode=TunerPerformanceMode(
hashrate_target=HashrateTargetMode(
hashrate_target=TeraHashrate(
terahash_per_second=self.hashrate
)
)
)
),
)
}
def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "ths", "ths": self.hashrate}}
@dataclass
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:
return {"miner-mode": "0"}
@dataclass
class MiningModeManual(MinerConfigValue):
mode: str = field(init=False, default="manual")
global_freq: float
global_volt: float
boards: dict[int, ManualBoardSettings] = field(default_factory=dict)
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeManual":
return cls(
global_freq=dict_conf["global_freq"],
global_volt=dict_conf["global_volt"],
boards={i: ManualBoardSettings.from_dict(dict_conf[i]) for i in dict_conf},
)
def as_am_modern(self) -> dict:
return {"miner-mode": "0"}
@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)
class MiningModeConfig(MinerConfigOption):
normal = MiningModeNormal
low = MiningModeLPM
high = MiningModeHPM
sleep = MiningModeSleep
power_tuning = MiningModePowerTune
hashrate_tuning = MiningModeHashrateTune
manual = MiningModeManual
@classmethod
def default(cls):
return cls.normal()
@classmethod
def from_dict(cls, dict_conf: 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_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:
return cls.power_tuning(
power=algo_info["VoltageOptimizer"]["Target"],
algo=PowerTunerAlgo.voltage_optimizer,
)
else:
return cls.power_tuning(
power=algo_info["ChipTune"]["Target"],
algo=PowerTunerAlgo.chip_tune,
)
else:
return cls.normal()
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(autotuning_conf["psu_power_limit"])
if autotuning_conf.get("mode") is not None:
# new autotuning conf
mode = autotuning_conf["mode"]
if mode == "power_target":
if autotuning_conf.get("power_target") is not None:
return cls.power_tuning(autotuning_conf["power_target"])
return cls.power_tuning()
if mode == "hashrate_target":
if autotuning_conf.get("hashrate_target") is not None:
return cls.hashrate_tuning(autotuning_conf["hashrate_target"])
return cls.hashrate_tuning()
@classmethod
def from_vnish(cls, web_settings: 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 cls.power_tuning(int(mode_settings["preset"]))
@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(tuner_conf["powerTarget"]["watt"])
return cls.power_tuning()
if tuner_conf["tunerMode"] == 2:
if tuner_conf.get("hashrateTarget") is not None:
return cls.hashrate_tuning(
int(tuner_conf["hashrateTarget"]["terahashPerSecond"])
)
return cls.hashrate_tuning()
if tuner_conf.get("powerTarget") is not None:
return cls.power_tuning(tuner_conf["powerTarget"]["watt"])
if tuner_conf.get("hashrateTarget") is not None:
return cls.hashrate_tuning(
int(tuner_conf["hashrateTarget"]["terahashPerSecond"])
)
@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(mode_data["Ths"])
if mode_data.get("Power") is not None:
return cls.power_tuning(mode_data["Power"])
except LookupError:
return cls.default()

View File

@@ -1,491 +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 dataclasses import dataclass, field
from typing import List
from pyasic.config.base import MinerConfigValue
@dataclass
class Pool(MinerConfigValue):
url: str
user: str
password: str
def as_am_modern(self, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
"url": self.url,
"user": f"{self.user}{user_suffix}",
"pass": self.password,
}
return {"url": self.url, "user": self.user, "pass": self.password}
def as_wm(self, idx: int = 1, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
f"pool_{idx}": self.url,
f"worker_{idx}": f"{self.user}{user_suffix}",
f"passwd_{idx}": self.password,
}
return {
f"pool_{idx}": self.url,
f"worker_{idx}": self.user,
f"passwd_{idx}": self.password,
}
def as_am_old(self, idx: int = 1, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
f"_ant_pool{idx}url": self.url,
f"_ant_pool{idx}user": f"{self.user}{user_suffix}",
f"_ant_pool{idx}pw": self.password,
}
return {
f"_ant_pool{idx}url": self.url,
f"_ant_pool{idx}user": self.user,
f"_ant_pool{idx}pw": self.password,
}
def as_goldshell(self, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
"url": self.url,
"user": f"{self.user}{user_suffix}",
"pass": self.password,
}
return {"url": self.url, "user": self.user, "pass": self.password}
def as_avalon(self, user_suffix: str = None) -> str:
if user_suffix is not None:
return ",".join([self.url, f"{self.user}{user_suffix}", self.password])
return ",".join([self.url, self.user, self.password])
def as_inno(self, idx: int = 1, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
f"Pool{idx}": self.url,
f"UserName{idx}": f"{self.user}{user_suffix}",
f"Password{idx}": self.password,
}
return {
f"Pool{idx}": self.url,
f"UserName{idx}": self.user,
f"Password{idx}": self.password,
}
def as_bosminer(self, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
"url": self.url,
"user": f"{self.user}{user_suffix}",
"password": self.password,
}
return {"url": self.url, "user": self.user, "password": self.password}
def as_auradine(self, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
"url": self.url,
"user": f"{self.user}{user_suffix}",
"pass": self.password,
}
return {"url": self.url, "user": self.user, "pass": self.password}
def as_epic(self, user_suffix: str = None):
if user_suffix is not None:
return {
"pool": self.url,
"login": f"{self.user}{user_suffix}",
"password": self.password,
}
return {"pool": self.url, "login": self.user, "password": 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_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"]
)
# TODO: check if this is accurate, user/username, pass/password
@classmethod
def from_goldshell(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_inno(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_bosminer(cls, toml_pool_conf: dict) -> "Pool":
return cls(
url=toml_pool_conf["url"],
user=toml_pool_conf["user"],
password=toml_pool_conf["password"],
)
@classmethod
def from_vnish(cls, web_pool: dict) -> "Pool":
return cls(
url=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"],
)
@dataclass
class PoolGroup(MinerConfigValue):
pools: list[Pool] = field(default_factory=list)
quota: int = 1
name: str = None
def __post_init__(self):
if self.name is None:
self.name = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(6)
) # generate random pool group name in case it isn't set
def as_am_modern(self, user_suffix: str = None) -> list:
pools = []
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.append(self.pools[idx].as_am_modern(user_suffix=user_suffix))
else:
pools.append(Pool("", "", "").as_am_modern())
idx += 1
return pools
def as_wm(self, user_suffix: str = None) -> dict:
pools = {}
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.update(
**self.pools[idx].as_wm(idx=idx + 1, user_suffix=user_suffix)
)
else:
pools.update(**Pool("", "", "").as_wm(idx=idx + 1))
idx += 1
return pools
def as_am_old(self, user_suffix: str = None) -> dict:
pools = {}
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.update(
**self.pools[idx].as_am_old(idx=idx + 1, user_suffix=user_suffix)
)
else:
pools.update(**Pool("", "", "").as_am_old(idx=idx + 1))
idx += 1
return pools
def as_goldshell(self, user_suffix: str = None) -> list:
return [pool.as_goldshell(user_suffix) for pool in self.pools]
def as_avalon(self, user_suffix: str = None) -> str:
if len(self.pools) > 0:
return self.pools[0].as_avalon(user_suffix=user_suffix)
return Pool("", "", "").as_avalon()
def as_inno(self, user_suffix: str = None) -> dict:
pools = {}
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.update(
**self.pools[idx].as_inno(idx=idx + 1, user_suffix=user_suffix)
)
else:
pools.update(**Pool("", "", "").as_inno(idx=idx + 1))
idx += 1
return pools
def as_bosminer(self, user_suffix: str = None) -> dict:
if len(self.pools) > 0:
conf = {
"name": self.name,
"pool": [
pool.as_bosminer(user_suffix=user_suffix) for pool in self.pools
],
}
if self.quota is not None:
conf["quota"] = self.quota
return conf
return {"name": "Group", "pool": []}
def as_auradine(self, user_suffix: str = None) -> list:
return [p.as_auradine(user_suffix=user_suffix) for p in self.pools]
def as_epic(self, user_suffix: str = None) -> dict:
if len(self.pools) > 0:
conf = {
"name": self.name,
"pool": [pool.as_epic(user_suffix=user_suffix) for pool in self.pools],
}
if self.quota is not None:
conf["quota"] = self.quota
return conf
return {"name": self.name, "pool": []}
@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_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_goldshell(cls, web_pools: list) -> "PoolGroup":
return cls([Pool.from_goldshell(p) for p in web_pools])
@classmethod
def from_inno(cls, web_pools: list) -> "PoolGroup":
return cls([Pool.from_inno(p) for p in web_pools])
@classmethod
def from_bosminer(cls, toml_group_conf: dict) -> "PoolGroup":
if toml_group_conf.get("pool") is not None:
return cls(
name=toml_group_conf["name"],
quota=toml_group_conf.get("quota"),
pools=[Pool.from_bosminer(p) for p in toml_group_conf["pool"]],
)
return cls()
@classmethod
def from_vnish(cls, web_settings_pools: dict) -> "PoolGroup":
return cls([Pool.from_vnish(p) for p in web_settings_pools])
@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()
@dataclass
class PoolConfig(MinerConfigValue):
groups: List[PoolGroup] = field(default_factory=list)
@classmethod
def default(cls) -> "PoolConfig":
return cls()
@classmethod
def from_dict(cls, dict_conf: 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) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_am_modern(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_am_modern()}
def as_wm(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_wm()}
def as_am_old(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return self.groups[0].as_am_old(user_suffix=user_suffix)
return PoolGroup().as_am_old()
def as_goldshell(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_goldshell(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_goldshell()}
def as_avalon(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_avalon(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_avalon()}
def as_inno(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return self.groups[0].as_inno(user_suffix=user_suffix)
return PoolGroup().as_inno()
def as_bosminer(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {
"group": [g.as_bosminer(user_suffix=user_suffix) for g in self.groups]
}
return {"group": [PoolGroup().as_bosminer()]}
def as_boser(self, user_suffix: str = None) -> dict:
return {}
def as_auradine(self, user_suffix: str = 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) -> dict:
if len(self.groups) > 0:
return {
"pools": {
"coin": "Btc",
"stratum_configs": [
g.as_epic(user_suffix=user_suffix) for g in self.groups
],
"unique_id": False,
}
}
return {
"pools": {
"coin": "Btc",
"stratum_configs": [PoolGroup().as_epic()],
"unique_id": False,
}
}
@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([PoolGroup.from_api(pool_data)])
@classmethod
def from_epic(cls, web_conf: dict) -> "PoolConfig":
pool_data = web_conf["StratumConfigs"]
return cls([PoolGroup.from_epic(pool_data)])
@classmethod
def from_am_modern(cls, web_conf: dict) -> "PoolConfig":
pool_data = web_conf["pools"]
return cls([PoolGroup.from_am_modern(pool_data)])
@classmethod
def from_goldshell(cls, web_pools: list) -> "PoolConfig":
return cls([PoolGroup.from_goldshell(web_pools)])
@classmethod
def from_inno(cls, web_pools: list) -> "PoolConfig":
return cls([PoolGroup.from_inno(web_pools)])
@classmethod
def from_bosminer(cls, toml_conf: dict) -> "PoolConfig":
if toml_conf.get("group") is None:
return cls()
return cls([PoolGroup.from_bosminer(g) for g in toml_conf["group"]])
@classmethod
def from_vnish(cls, web_settings: dict) -> "PoolConfig":
try:
return cls([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()

View File

@@ -1,217 +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 dataclass, field
from pyasic.config.base import MinerConfigOption, MinerConfigValue
from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
DpsPowerTarget,
DpsTarget,
Power,
SetDpsRequest,
)
@dataclass
class PowerScalingShutdownEnabled(MinerConfigValue):
mode: str = field(init=False, default="enabled")
duration: int = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "PowerScalingShutdownEnabled":
return cls(duration=dict_conf.get("duration"))
def as_bosminer(self) -> dict:
cfg = {"shutdown_enabled": True}
if self.duration is not None:
cfg["shutdown_duration"] = self.duration
return cfg
def as_boser(self) -> dict:
return {"enable_shutdown": True, "shutdown_duration": self.duration}
@dataclass
class PowerScalingShutdownDisabled(MinerConfigValue):
mode: str = field(init=False, default="disabled")
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "PowerScalingShutdownDisabled":
return cls()
def as_bosminer(self) -> dict:
return {"shutdown_enabled": False}
def as_boser(self) -> dict:
return {"enable_shutdown ": False}
class PowerScalingShutdown(MinerConfigOption):
enabled = PowerScalingShutdownEnabled
disabled = PowerScalingShutdownDisabled
@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()
clsattr = getattr(cls, mode)
if clsattr is not None:
return clsattr().from_dict(dict_conf)
@classmethod
def from_bosminer(cls, power_scaling_conf: dict):
sd_enabled = power_scaling_conf.get("shutdown_enabled")
if sd_enabled is not None:
if sd_enabled:
return cls.enabled(power_scaling_conf.get("shutdown_duration"))
else:
return cls.disabled()
return None
@classmethod
def from_boser(cls, power_scaling_conf: dict):
sd_enabled = power_scaling_conf.get("shutdownEnabled")
if sd_enabled is not None:
if sd_enabled:
try:
return cls.enabled(power_scaling_conf["shutdownDuration"]["hours"])
except KeyError:
return cls.enabled()
else:
return cls.disabled()
return None
@dataclass
class PowerScalingEnabled(MinerConfigValue):
mode: str = field(init=False, default="enabled")
power_step: int = None
minimum_power: int = None
shutdown_enabled: PowerScalingShutdownEnabled | PowerScalingShutdownDisabled = None
@classmethod
def from_bosminer(cls, power_scaling_conf: dict) -> "PowerScalingEnabled":
power_step = power_scaling_conf.get("power_step")
min_power = power_scaling_conf.get("min_psu_power_limit")
sd_mode = PowerScalingShutdown.from_bosminer(power_scaling_conf)
return cls(
power_step=power_step, minimum_power=min_power, shutdown_enabled=sd_mode
)
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "PowerScalingEnabled":
cls_conf = {
"power_step": dict_conf.get("power_step"),
"minimum_power": dict_conf.get("minimum_power"),
}
shutdown_enabled = dict_conf.get("shutdown_enabled")
if shutdown_enabled is not None:
cls_conf["shutdown_enabled"] = PowerScalingShutdown.from_dict(
shutdown_enabled
)
return cls(**cls_conf)
def as_bosminer(self) -> dict:
cfg = {"enabled": True}
if self.power_step is not None:
cfg["power_step"] = self.power_step
if self.minimum_power is not None:
cfg["min_psu_power_limit"] = self.minimum_power
if self.shutdown_enabled is not None:
cfg = {**cfg, **self.shutdown_enabled.as_bosminer()}
return {"power_scaling": cfg}
def as_boser(self) -> dict:
return {
"set_dps": SetDpsRequest(
enable=True,
**self.shutdown_enabled.as_boser(),
target=DpsTarget(
power_target=DpsPowerTarget(
power_step=Power(self.power_step),
min_power_target=Power(self.minimum_power),
)
),
),
}
@dataclass
class PowerScalingDisabled(MinerConfigValue):
mode: str = field(init=False, default="disabled")
class PowerScalingConfig(MinerConfigOption):
enabled = PowerScalingEnabled
disabled = PowerScalingDisabled
@classmethod
def default(cls):
return cls.disabled()
@classmethod
def from_dict(cls, dict_conf: dict | None):
if dict_conf is None:
return cls.default()
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
clsattr = getattr(cls, mode)
if clsattr is not None:
return clsattr().from_dict(dict_conf)
@classmethod
def from_bosminer(cls, toml_conf: dict):
power_scaling = toml_conf.get("power_scaling")
if power_scaling is not None:
enabled = power_scaling.get("enabled")
if enabled is not None:
if enabled:
return cls.enabled().from_bosminer(power_scaling)
else:
return cls.disabled()
return cls.default()
@classmethod
def from_boser(cls, grpc_miner_conf: dict):
try:
dps_conf = grpc_miner_conf["dps"]
if not dps_conf.get("enabled", False):
return cls.disabled()
except LookupError:
return cls.default()
conf = {"shutdown_enabled": PowerScalingShutdown.from_boser(dps_conf)}
if dps_conf.get("minPowerTarget") is not None:
conf["minimum_power"] = dps_conf["minPowerTarget"]["watt"]
if dps_conf.get("powerStep") is not None:
conf["power_step"] = dps_conf["powerStep"]["watt"]
return cls.enabled(**conf)

View File

@@ -1,123 +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 dataclass
from pyasic.config.base import MinerConfigValue
@dataclass
class TemperatureConfig(MinerConfigValue):
target: int = None
hot: int = None
danger: int = None
@classmethod
def default(cls):
return cls()
def as_bosminer(self) -> dict:
temp_cfg = {}
if self.target is not None:
temp_cfg["target_temp"] = self.target
if self.hot is not None:
temp_cfg["hot_temp"] = self.hot
if self.danger is not None:
temp_cfg["dangerous_temp"] = self.danger
return {"temp_control": temp_cfg}
def as_epic(self) -> dict:
temps_config = {"temps": {}, "fans": {"Auto": {}}}
if self.target is not None:
temps_config["fans"]["Target Temperature"] = self.target
else:
temps_config["fans"]["Target Temperature"] = 60
if self.danger is not None:
temps_config["temps"]["shutdown"] = self.danger
return temps_config
@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"),
)
@classmethod
def from_epic(cls, web_conf: dict) -> "TemperatureConfig":
try:
dangerous_temp = web_conf["Misc"]["Shutdown Temp"]
except KeyError:
dangerous_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, danger=dangerous_temp)
@classmethod
def from_vnish(cls, web_settings: dict) -> "TemperatureConfig":
try:
if web_settings["miner"]["cooling"]["mode"]["name"] == "auto":
return cls(target=web_settings["miner"]["cooling"]["mode"]["param"])
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()

View File

@@ -16,17 +16,77 @@
import copy import copy
import json import json
import logging
import time import time
from dataclasses import asdict, dataclass, field, fields from dataclasses import asdict, dataclass, field, fields
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, List, Union from typing import Any, List, Union
from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune
from .boards import HashBoard
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
from .fans import Fan
@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 = None
temp: int = None
chip_temp: int = None
chips: int = None
expected_chips: int = None
missing: bool = True
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}")
@dataclass
class Fan:
"""A Dataclass to standardize fan data.
Attributes:
speed: The speed of the fan.
"""
speed: int = None
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}")
@dataclass @dataclass
@@ -45,8 +105,8 @@ class MinerData:
hostname: The network hostname of 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. hashrate: The hashrate of the miner in TH/s as a float. Calculated automatically.
_hashrate: Backup for hashrate found via API instead of hashboards. _hashrate: Backup for hashrate found via API instead of hashboards.
expected_hashrate: The factory nominal 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.
hashboards: A list of [`HashBoard`][pyasic.data.HashBoard]s on the miner with their statistics. hashboards: A list of hashboards on the miner with their statistics.
temperature_avg: The average temperature across the boards. Calculated automatically. temperature_avg: The average temperature across the boards. Calculated automatically.
env_temp: The environment temps as a float. env_temp: The environment temps as a float.
wattage: Current power draw of the miner as an int. wattage: Current power draw of the miner as an int.
@@ -54,12 +114,16 @@ class MinerData:
fans: A list of fans on the miner with their speeds. fans: A list of fans on the miner with their speeds.
fan_psu: The speed of the PSU on the fan if the miner collects it. fan_psu: The speed of the PSU on the fan if the miner collects it.
total_chips: The total number of chips on all boards. Calculated automatically. 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. ideal_chips: The ideal 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_ideal_chips: The percent of total chips out of the ideal count. Calculated automatically.
percent_expected_hashrate: The percent of total hashrate out of the expected hashrate. Calculated automatically. percent_ideal_hashrate: The percent of total hashrate out of the ideal hashrate. Calculated automatically.
percent_expected_wattage: The percent of total wattage out of the expected wattage. 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. 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. errors: A list of errors on the miner.
fault_light: Whether the fault light is on as a boolean. fault_light: Whether the fault light is on as a boolean.
efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically. efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically.
@@ -76,24 +140,27 @@ class MinerData:
fw_ver: str = None fw_ver: str = None
hostname: str = None hostname: str = None
hashrate: float = field(init=False) hashrate: float = field(init=False)
_hashrate: float = field(repr=False, default=None) _hashrate: float = None
expected_hashrate: float = None nominal_hashrate: float = None
hashboards: List[HashBoard] = field(default_factory=list) hashboards: List[HashBoard] = field(default_factory=list)
expected_hashboards: int = None ideal_hashboards: int = None
temperature_avg: int = field(init=False) temperature_avg: int = field(init=False)
env_temp: float = None env_temp: float = None
wattage: int = None wattage: int = None
wattage_limit: int = field(init=False) wattage_limit: int = None
_wattage_limit: int = field(repr=False, default=None)
fans: List[Fan] = field(default_factory=list) fans: List[Fan] = field(default_factory=list)
fan_psu: int = None fan_psu: int = None
total_chips: int = field(init=False) total_chips: int = field(init=False)
expected_chips: int = None ideal_chips: int = None
percent_expected_chips: float = field(init=False) percent_ideal_chips: float = field(init=False)
percent_expected_hashrate: float = field(init=False) percent_ideal_hashrate: float = field(init=False)
percent_expected_wattage: float = field(init=False) percent_ideal_wattage: float = field(init=False)
nominal: bool = field(init=False) nominal: bool = field(init=False)
config: MinerConfig = None pool_split: str = "0"
pool_1_url: str = "Unknown"
pool_1_user: str = "Unknown"
pool_2_url: str = ""
pool_2_user: str = ""
errors: List[ errors: List[
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError] Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
] = field(default_factory=list) ] = field(default_factory=list)
@@ -103,11 +170,7 @@ class MinerData:
@classmethod @classmethod
def fields(cls): def fields(cls):
return [f.name for f in fields(cls) if not f.name.startswith("_")] return [f.name for f in fields(cls)]
@staticmethod
def dict_factory(x):
return {k: v for (k, v) in x if not k.startswith("_")}
def __post_init__(self): def __post_init__(self):
self.datetime = datetime.now(timezone.utc).astimezone() self.datetime = datetime.now(timezone.utc).astimezone()
@@ -185,17 +248,6 @@ class MinerData:
def hashrate(self, val): def hashrate(self, val):
self._hashrate = val self._hashrate = val
@property
def wattage_limit(self): # noqa - Skip PyCharm inspection
if self.config is not None:
if isinstance(self.config.mining_mode, MiningModePowerTune):
return self.config.mining_mode.power
return self._wattage_limit
@wattage_limit.setter
def wattage_limit(self, val: int):
self._wattage_limit = val
@property @property
def total_chips(self): # noqa - Skip PyCharm inspection def total_chips(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) > 0: if len(self.hashboards) > 0:
@@ -213,48 +265,48 @@ class MinerData:
@property @property
def nominal(self): # noqa - Skip PyCharm inspection def nominal(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.expected_chips is None: if self.total_chips is None or self.ideal_chips is None:
return None return None
return self.expected_chips == self.total_chips return self.ideal_chips == self.total_chips
@nominal.setter @nominal.setter
def nominal(self, val): def nominal(self, val):
pass pass
@property @property
def percent_expected_chips(self): # noqa - Skip PyCharm inspection def percent_ideal_chips(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.expected_chips is None: if self.total_chips is None or self.ideal_chips is None:
return None return None
if self.total_chips == 0 or self.expected_chips == 0: if self.total_chips == 0 or self.ideal_chips == 0:
return 0 return 0
return round((self.total_chips / self.expected_chips) * 100) return round((self.total_chips / self.ideal_chips) * 100)
@percent_expected_chips.setter @percent_ideal_chips.setter
def percent_expected_chips(self, val): def percent_ideal_chips(self, val):
pass pass
@property @property
def percent_expected_hashrate(self): # noqa - Skip PyCharm inspection def percent_ideal_hashrate(self): # noqa - Skip PyCharm inspection
if self.hashrate is None or self.expected_hashrate is None: if self.hashrate is None or self.nominal_hashrate is None:
return None return None
if self.hashrate == 0 or self.expected_hashrate == 0: if self.hashrate == 0 or self.nominal_hashrate == 0:
return 0 return 0
return round((self.hashrate / self.expected_hashrate) * 100) return round((self.hashrate / self.nominal_hashrate) * 100)
@percent_expected_hashrate.setter @percent_ideal_hashrate.setter
def percent_expected_hashrate(self, val): def percent_ideal_hashrate(self, val):
pass pass
@property @property
def percent_expected_wattage(self): # noqa - Skip PyCharm inspection def percent_ideal_wattage(self): # noqa - Skip PyCharm inspection
if self.wattage_limit is None or self.wattage is None: if self.wattage_limit is None or self.wattage is None:
return None return None
if self.wattage_limit == 0 or self.wattage == 0: if self.wattage_limit == 0 or self.wattage == 0:
return 0 return 0
return round((self.wattage / self.wattage_limit) * 100) return round((self.wattage / self.wattage_limit) * 100)
@percent_expected_wattage.setter @percent_ideal_wattage.setter
def percent_expected_wattage(self, val): def percent_ideal_wattage(self, val):
pass pass
@property @property
@@ -286,7 +338,8 @@ class MinerData:
pass pass
def asdict(self) -> dict: def asdict(self) -> dict:
return asdict(self, dict_factory=self.dict_factory) logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
return asdict(self)
def as_dict(self) -> dict: def as_dict(self) -> dict:
"""Get this dataclass as a dictionary. """Get this dataclass as a dictionary.
@@ -302,6 +355,7 @@ class MinerData:
Returns: Returns:
A JSON version of this class. A JSON version of this class.
""" """
logging.debug(f"MinerData - (To JSON) - Dumping JSON data")
data = self.asdict() data = self.asdict()
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple()))) data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
return json.dumps(data) return json.dumps(data)
@@ -312,6 +366,7 @@ class MinerData:
Returns: Returns:
A CSV version of this class with no headers. A CSV version of this class with no headers.
""" """
logging.debug(f"MinerData - (To CSV) - Dumping CSV data")
data = self.asdict() data = self.asdict()
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple()))) data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
errs = [] errs = []
@@ -330,6 +385,7 @@ class MinerData:
Returns: Returns:
A influxdb line protocol version of this class. A influxdb line protocol version of this class.
""" """
logging.debug(f"MinerData - (To InfluxDB) - Dumping InfluxDB data")
tag_data = [measurement_name] tag_data = [measurement_name]
field_data = [] field_data = []

View File

@@ -1,58 +0,0 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from dataclasses import dataclass
from typing import Any
@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 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.
"""
slot: int = 0
hashrate: float = None
temp: int = None
chip_temp: int = None
chips: int = None
expected_chips: int = None
serial_number: str = None
missing: bool = True
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}")

View File

@@ -20,7 +20,7 @@ from typing import List, Union
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.miners import AnyMiner from pyasic.miners import AnyMiner
from pyasic.miners.backends import AntminerModern, BOSMiner, BTMiner from pyasic.miners.backends import AntminerModern, BOSMiner, BTMiner
from pyasic.miners.models import S9, S17, T17, S17e, S17Plus, S17Pro, T17e, T17Plus from pyasic.miners.types import S9, S17, T17, S17e, S17Plus, S17Pro, T17e, T17Plus
FAN_USAGE = 50 # 50 W per fan FAN_USAGE = 50 # 50 W per fan
@@ -66,14 +66,14 @@ class _MinerPhaseBalancer:
str(miner.ip): { str(miner.ip): {
"miner": miner, "miner": miner,
"set": 0, "set": 0,
"min": miner.expected_fans * FAN_USAGE, "min": miner.fan_count * FAN_USAGE,
} }
for miner in miners for miner in miners
} }
for miner in miners: for miner in miners:
if ( if (
isinstance(miner, BTMiner) isinstance(miner, BTMiner)
and not (miner.raw_model.startswith("M2") if miner.raw_model else True) and not (miner.model.startswith("M2") if miner.model else True)
) or isinstance(miner, BOSMiner): ) or isinstance(miner, BOSMiner):
if isinstance(miner, S9): if isinstance(miner, S9):
self.miners[str(miner.ip)]["tune"] = True self.miners[str(miner.ip)]["tune"] = True
@@ -98,8 +98,8 @@ class _MinerPhaseBalancer:
self.miners[str(miner.ip)]["tune"] = False self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = True self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 3600 self.miners[str(miner.ip)]["max"] = 3600
if miner.raw_model: if miner.model:
if miner.raw_model.startswith("M2"): if miner.model.startswith("M2"):
self.miners[str(miner.ip)]["tune"] = False self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = True self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 2400 self.miners[str(miner.ip)]["max"] = 2400
@@ -137,10 +137,10 @@ class _MinerPhaseBalancer:
for miner in self.miners for miner in self.miners
] ]
) )
pct_expected_list = [d.percent_ideal for d in data] pct_ideal_list = [d.percent_ideal for d in data]
pct_ideal = 0 pct_ideal = 0
if len(pct_expected_list) > 0: if len(pct_ideal_list) > 0:
pct_ideal = sum(pct_expected_list) / len(pct_expected_list) pct_ideal = sum(pct_ideal_list) / len(pct_ideal_list)
wattage = round(wattage * 1 / (pct_ideal / 100)) wattage = round(wattage * 1 / (pct_ideal / 100))
@@ -149,10 +149,10 @@ class _MinerPhaseBalancer:
not self.miners[data_point.ip]["shutdown"] not self.miners[data_point.ip]["shutdown"]
): ):
# cant do anything with it so need to find a semi-accurate power limit # cant do anything with it so need to find a semi-accurate power limit
if data_point.wattage_limit is not None: if not data_point.wattage_limit == None:
self.miners[data_point.ip]["max"] = int(data_point.wattage_limit) self.miners[data_point.ip]["max"] = int(data_point.wattage_limit)
self.miners[data_point.ip]["min"] = int(data_point.wattage_limit) self.miners[data_point.ip]["min"] = int(data_point.wattage_limit)
elif data_point.wattage is not None: elif not data_point.wattage == None:
self.miners[data_point.ip]["max"] = int(data_point.wattage) self.miners[data_point.ip]["max"] = int(data_point.wattage)
self.miners[data_point.ip]["min"] = int(data_point.wattage) self.miners[data_point.ip]["min"] = int(data_point.wattage)
@@ -183,19 +183,13 @@ class _MinerPhaseBalancer:
if (not miner["tune"]) and (miner["shutdown"]) if (not miner["tune"]) and (miner["shutdown"])
] ]
) )
# min_other_wattage = sum( # min_other_wattage = sum([miner["min"] for miner in self.miners.values() if (not miner["tune"]) and (not miner["shutdown"])])
# [
# miner["min"]
# for miner in self.miners.values()
# if (not miner["tune"]) and (not miner["shutdown"])
# ]
# )
# make sure wattage isnt set too high # make sure wattage isnt set too high
if wattage > (max_tune_wattage + max_shutdown_wattage + max_other_wattage): if wattage > (max_tune_wattage + max_shutdown_wattage + max_other_wattage):
raise APIError( raise APIError(
f"Wattage setpoint is too high, setpoint: {wattage}W, max: {max_tune_wattage + max_shutdown_wattage + max_other_wattage}W" f"Wattage setpoint is too high, setpoint: {wattage}W, max: {max_tune_wattage + max_shutdown_wattage + max_other_wattage}W"
) ) # PhaseBalancingError(f"Wattage setpoint is too high, setpoint: {wattage}W, max: {max_tune_wattage + max_shutdown_wattage + max_other_wattage}W")
# should now know wattage limits and which can be tuned/shutdown # should now know wattage limits and which can be tuned/shutdown
# check if 1/2 max of the miners which can be tuned is low enough # check if 1/2 max of the miners which can be tuned is low enough

View File

@@ -14,7 +14,13 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .base import AnyMiner import ipaddress
from .data import DataOptions from typing import Union
from .factory import get_miner, miner_factory
from .listener import MinerListener from pyasic.miners.base import AnyMiner, BaseMiner
from pyasic.miners.miner_factory import miner_factory
# abstracted version of get miner that is easier to access
async def get_miner(ip: Union[ipaddress.ip_address, str]) -> AnyMiner:
return await miner_factory.get_miner(ip)

View File

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

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerOld from pyasic.miners.backends import AntminerOld
from pyasic.miners.models import S17, S17e, S17Plus, S17Pro from pyasic.miners.types import S17, S17e, S17Plus, S17Pro
class BMMinerS17(AntminerOld, S17): class BMMinerS17(AntminerOld, S17):

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerOld from pyasic.miners.backends import AntminerOld
from pyasic.miners.models import T17, T17e, T17Plus from pyasic.miners.types import T17, T17e, T17Plus
class BMMinerT17(AntminerOld, T17): class BMMinerT17(AntminerOld, T17):

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerModern from pyasic.miners.backends import AntminerModern
from pyasic.miners.models import ( from pyasic.miners.types import (
S19, S19,
S19L, S19L,
S19XP, S19XP,
@@ -27,7 +27,6 @@ from pyasic.miners.models import (
S19jPro, S19jPro,
S19Plus, S19Plus,
S19Pro, S19Pro,
S19ProHydro,
S19ProPlus, S19ProPlus,
) )
@@ -78,7 +77,3 @@ class BMMinerS19jPro(AntminerModern, S19jPro):
class BMMinerS19L(AntminerModern, S19L): class BMMinerS19L(AntminerModern, S19L):
pass pass
class BMMinerS19ProHydro(AntminerModern, S19ProHydro):
pass

View File

@@ -15,7 +15,9 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerModern from pyasic.miners.backends import AntminerModern
from pyasic.miners.models import T19 from pyasic.miners.types import T19
# noqa - Ignore access to _module
class BMMinerT19(AntminerModern, T19): class BMMinerT19(AntminerModern, T19):

View File

@@ -25,7 +25,6 @@ from .S19 import (
BMMinerS19L, BMMinerS19L,
BMMinerS19Plus, BMMinerS19Plus,
BMMinerS19Pro, BMMinerS19Pro,
BMMinerS19ProHydro,
BMMinerS19ProPlus, BMMinerS19ProPlus,
BMMinerS19XP, BMMinerS19XP,
) )

View File

@@ -15,8 +15,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerModern from pyasic.miners.backends import AntminerModern
from pyasic.miners.models import HS3 from pyasic.miners.types import HS3
class BMMinerHS3(AntminerModern, HS3): class BMMinerHS3(AntminerModern, HS3):
supports_shutdown = False def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.supports_shutdown = False

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerOld from pyasic.miners.backends import AntminerOld
from pyasic.miners.models import L3Plus from pyasic.miners.types import L3Plus
class BMMinerL3Plus(AntminerOld, L3Plus): class BMMinerL3Plus(AntminerOld, L3Plus):

View File

@@ -14,8 +14,10 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerModern from pyasic.miners.backends import AntminerModern
from pyasic.miners.models import L7 from pyasic.miners.types import L7
class BMMinerL7(AntminerModern, L7): class BMMinerL7(AntminerModern, L7):
supports_shutdown = False def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.supports_shutdown = False

View File

@@ -15,8 +15,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerModern from pyasic.miners.backends import AntminerModern
from pyasic.miners.models import E9Pro from pyasic.miners.types import E9Pro
class BMMinerE9Pro(AntminerModern, E9Pro): class BMMinerE9Pro(AntminerModern, E9Pro):
supports_shutdown = False def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.supports_shutdown = False

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import BMMiner from pyasic.miners.backends import BMMiner
from pyasic.miners.models import S9, S9i, S9j from pyasic.miners.types import S9, S9i, S9j
class BMMinerS9(BMMiner, S9): class BMMinerS9(BMMiner, S9):

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import BMMiner from pyasic.miners.backends import BMMiner
from pyasic.miners.models import T9 from pyasic.miners.types import T9
class BMMinerT9(BMMiner, T9): class BMMinerT9(BMMiner, T9):

View File

@@ -14,21 +14,21 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import BOSer from pyasic.miners.backends import BOSMiner
from pyasic.miners.models import S17, S17e, S17Plus, S17Pro from pyasic.miners.types import S17, S17e, S17Plus, S17Pro
class BOSMinerS17(BOSer, S17): class BOSMinerS17(BOSMiner, S17):
pass pass
class BOSMinerS17Plus(BOSer, S17Plus): class BOSMinerS17Plus(BOSMiner, S17Plus):
pass pass
class BOSMinerS17Pro(BOSer, S17Pro): class BOSMinerS17Pro(BOSMiner, S17Pro):
pass pass
class BOSMinerS17e(BOSer, S17e): class BOSMinerS17e(BOSMiner, S17e):
pass pass

View File

@@ -14,17 +14,17 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import BOSer from pyasic.miners.backends import BOSMiner
from pyasic.miners.models import T17, T17e, T17Plus from pyasic.miners.types import T17, T17e, T17Plus
class BOSMinerT17(BOSer, T17): class BOSMinerT17(BOSMiner, T17):
pass pass
class BOSMinerT17Plus(BOSer, T17Plus): class BOSMinerT17Plus(BOSMiner, T17Plus):
pass pass
class BOSMinerT17e(BOSer, T17e): class BOSMinerT17e(BOSMiner, T17e):
pass pass

View File

@@ -14,61 +14,25 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import BOSer from pyasic.miners.backends import BOSMiner
from pyasic.miners.models import ( from pyasic.miners.types import S19, S19j, S19jNoPIC, S19jPro, S19Pro
S19,
S19XP,
S19a,
S19aPro,
S19j,
S19jNoPIC,
S19jPro,
S19jProPlus,
S19kProNoPIC,
S19Plus,
S19Pro,
)
class BOSMinerS19(BOSer, S19): class BOSMinerS19(BOSMiner, S19):
pass pass
class BOSMinerS19Plus(BOSer, S19Plus): class BOSMinerS19Pro(BOSMiner, S19Pro):
pass pass
class BOSMinerS19Pro(BOSer, S19Pro): class BOSMinerS19j(BOSMiner, S19j):
pass pass
class BOSMinerS19a(BOSer, S19a): class BOSMinerS19jNoPIC(BOSMiner, S19jNoPIC):
pass pass
class BOSMinerS19j(BOSer, S19j): class BOSMinerS19jPro(BOSMiner, S19jPro):
pass
class BOSMinerS19jNoPIC(BOSer, S19jNoPIC):
pass
class BOSMinerS19jPro(BOSer, S19jPro):
pass
class BOSMinerS19kProNoPIC(BOSer, S19kProNoPIC):
pass
class BOSMinerS19aPro(BOSer, S19aPro):
pass
class BOSMinerS19jProPlus(BOSer, S19jProPlus):
pass
class BOSMinerS19XP(BOSer, S19XP):
pass pass

View File

@@ -14,9 +14,9 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import BOSer from pyasic.miners.backends import BOSMiner
from pyasic.miners.models import T19 from pyasic.miners.types import T19
class BOSMinerT19(BOSer, T19): class BOSMinerT19(BOSMiner, T19):
pass pass

View File

@@ -16,15 +16,9 @@
from .S19 import ( from .S19 import (
BOSMinerS19, BOSMinerS19,
BOSMinerS19a,
BOSMinerS19aPro,
BOSMinerS19j, BOSMinerS19j,
BOSMinerS19jNoPIC, BOSMinerS19jNoPIC,
BOSMinerS19jPro, BOSMinerS19jPro,
BOSMinerS19jProPlus,
BOSMinerS19kProNoPIC,
BOSMinerS19Plus,
BOSMinerS19Pro, BOSMinerS19Pro,
BOSMinerS19XP,
) )
from .T19 import BOSMinerT19 from .T19 import BOSMinerT19

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import BOSMiner from pyasic.miners.backends import BOSMiner
from pyasic.miners.models import S9 from pyasic.miners.types import S9
class BOSMinerS9(BOSMiner, S9): class BOSMinerS9(BOSMiner, S9):

View File

@@ -15,8 +15,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerOld from pyasic.miners.backends import AntminerOld
from pyasic.miners.models import Z15 from pyasic.miners.types import Z15
class CGMinerZ15(AntminerOld, Z15): class CGMinerZ15(AntminerOld, Z15):
supports_shutdown = False def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.supports_shutdown = False

View File

@@ -14,8 +14,10 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerOld from pyasic.miners.backends import AntminerOld
from pyasic.miners.models import D3 from pyasic.miners.types import D3
class CGMinerD3(AntminerOld, D3): class CGMinerD3(AntminerOld, D3):
supports_shutdown = False def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.supports_shutdown = False

View File

@@ -15,8 +15,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerOld from pyasic.miners.backends import AntminerOld
from pyasic.miners.models import DR5 from pyasic.miners.types import DR5
class CGMinerDR5(AntminerOld, DR5): class CGMinerDR5(AntminerOld, DR5):
supports_shutdown = False def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)
self.supports_shutdown = False

View File

@@ -1,25 +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 .S19 import (
ePICS19,
ePICS19j,
ePICS19jPro,
ePICS19jProPlus,
ePICS19kPro,
ePICS19Pro,
ePICS19XP,
)

View File

@@ -21,53 +21,14 @@ import asyncssh
from pyasic.data import HashBoard from pyasic.data import HashBoard
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.miners.backends import Hiveon from pyasic.miners.backends import Hiveon
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.types import T9
from pyasic.miners.models import T9
HIVEON_T9_DATA_LOC = DataLocations(
**{
str(DataOptions.API_VERSION): DataFunction(
"_get_api_ver",
[RPCAPICommand("rpc_version", "version")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[RPCAPICommand("rpc_version", "version")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[RPCAPICommand("rpc_summary", "summary")],
),
str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.ENVIRONMENT_TEMP): DataFunction(
"_get_env_temp",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[RPCAPICommand("rpc_stats", "stats")],
),
}
)
class HiveonT9(Hiveon, T9): class HiveonT9(Hiveon, T9):
data_locations = HIVEON_T9_DATA_LOC def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver=api_ver)
self.ip = ip
self.pwd = "admin"
################################################## ##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### DATA GATHERING FUNCTIONS (get_{some_data}) ###
@@ -84,57 +45,47 @@ class HiveonT9(Hiveon, T9):
except (TypeError, ValueError, asyncssh.Error, OSError, AttributeError): except (TypeError, ValueError, asyncssh.Error, OSError, AttributeError):
pass pass
async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
hashboards = [
HashBoard(slot=board, expected_chips=self.expected_chips)
for board in range(self.expected_hashboards)
]
if rpc_stats is None:
try:
rpc_stats = self.rpc.stats()
except APIError:
return []
board_map = { board_map = {
0: [2, 9, 10], 0: [2, 9, 10],
1: [3, 11, 12], 1: [3, 11, 12],
2: [4, 13, 14], 2: [4, 13, 14],
} }
hashboards = []
for board in board_map: for board in board_map:
hashboard = HashBoard(slot=board, expected_chips=self.nominal_chips)
hashrate = 0 hashrate = 0
chips = 0 chips = 0
for chipset in board_map[board]: for chipset in board_map[board]:
if hashboards[board].chip_temp is None: if hashboard.chip_temp == None:
try: try:
hashboards[board].temp = rpc_stats["STATS"][1][f"temp{chipset}"] hashboard.board_temp = api_stats["STATS"][1][f"temp{chipset}"]
hashboards[board].chip_temp = rpc_stats["STATS"][1][ hashboard.chip_temp = api_stats["STATS"][1][f"temp2_{chipset}"]
f"temp2_{chipset}"
]
except (KeyError, IndexError): except (KeyError, IndexError):
pass pass
else: else:
hashboards[board].missing = False hashboard.missing = False
try: try:
hashrate += rpc_stats["STATS"][1][f"chain_rate{chipset}"] hashrate += api_stats["STATS"][1][f"chain_rate{chipset}"]
chips += rpc_stats["STATS"][1][f"chain_acn{chipset}"] chips += api_stats["STATS"][1][f"chain_acn{chipset}"]
except (KeyError, IndexError): except (KeyError, IndexError):
pass pass
hashboards[board].hashrate = round(hashrate / 1000, 2) hashboard.hashrate = round(hashrate / 1000, 2)
hashboards[board].chips = chips hashboard.chips = chips
hashboards.append(hashboard)
return hashboards return hashboards
async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]: async def get_wattage(self, api_stats: dict = None) -> Optional[int]:
if not rpc_stats: if not api_stats:
try: try:
rpc_stats = await self.rpc.stats() api_stats = await self.api.stats()
except APIError: except APIError:
pass pass
if rpc_stats: if api_stats:
boards = rpc_stats.get("STATS") boards = api_stats.get("STATS")
try: try:
wattage_raw = boards[1]["chain_power"] wattage_raw = boards[1]["chain_power"]
except (KeyError, IndexError): except (KeyError, IndexError):
@@ -143,23 +94,23 @@ class HiveonT9(Hiveon, T9):
# parse wattage position out of raw data # parse wattage position out of raw data
return round(float(wattage_raw.split(" ")[0])) return round(float(wattage_raw.split(" ")[0]))
async def _get_env_temp(self, rpc_stats: dict = None) -> Optional[float]: async def get_env_temp(self, api_stats: dict = None) -> Optional[float]:
env_temp_list = [] env_temp_list = []
board_map = { board_map = {
0: [2, 9, 10], 0: [2, 9, 10],
1: [3, 11, 12], 1: [3, 11, 12],
2: [4, 13, 14], 2: [4, 13, 14],
} }
if not rpc_stats: if not api_stats:
try: try:
rpc_stats = await self.rpc.stats() api_stats = await self.api.stats()
except APIError: except APIError:
pass pass
if rpc_stats: if api_stats:
for board in board_map.values(): for board in board_map.values():
for chipset in board: for chipset in board:
try: try:
env_temp = rpc_stats["STATS"][1][f"temp3_{chipset}"] env_temp = api_stats["STATS"][1][f"temp3_{chipset}"]
if not env_temp == 0: if not env_temp == 0:
env_temp_list.append(int(env_temp)) env_temp_list.append(int(env_temp))
except (KeyError, IndexError): except (KeyError, IndexError):

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import LUXMiner from pyasic.miners.backends import LUXMiner
from pyasic.miners.models import S9 from pyasic.miners.types import S9
class LUXMinerS9(LUXMiner, S9): class LUXMinerS9(LUXMiner, S9):

View File

@@ -14,7 +14,7 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import VNish from pyasic.miners.backends import VNish
from pyasic.miners.models import S17Plus, S17Pro from pyasic.miners.types import S17Plus, S17Pro
class VNishS17Plus(VNish, S17Plus): class VNishS17Plus(VNish, S17Plus):

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import VNish from pyasic.miners.backends import VNish
from pyasic.miners.models import ( from pyasic.miners.types import (
S19, S19,
S19XP, S19XP,
S19a, S19a,

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import VNish from pyasic.miners.backends import VNish
from pyasic.miners.models import T19 from pyasic.miners.types import T19
class VNishT19(VNish, T19): class VNishT19(VNish, T19):

View File

@@ -15,8 +15,9 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import VNish from pyasic.miners.backends import VNish
from pyasic.miners.models import L3Plus from pyasic.miners.types import L3Plus
class VnishL3Plus(VNish, L3Plus): class VnishL3Plus(VNish, L3Plus):
pass def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver)

View File

@@ -1 +0,0 @@
from .flux import *

View File

@@ -1,6 +0,0 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.models import AuradineAT1500
class AuradineFluxAT1500(AuradineAT1500, Auradine):
pass

View File

@@ -1,10 +0,0 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.models import AuradineAT2860, AuradineAT2880
class AuradineFluxAT2860(AuradineAT2860, Auradine):
pass
class AuradineFluxAT2880(AuradineAT2880, Auradine):
pass

View File

@@ -1,2 +0,0 @@
from .AT1 import AuradineFluxAT1500
from .AT2 import AuradineFluxAT2860, AuradineFluxAT2880

View File

@@ -1,6 +0,0 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.models import AuradineAI2500
class AuradineFluxAI2500(AuradineAI2500, Auradine):
pass

View File

@@ -1,6 +0,0 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.models import AuradineAI3680
class AuradineFluxAI3680(AuradineAI3680, Auradine):
pass

View File

@@ -1,2 +0,0 @@
from .AI2 import AuradineFluxAI2500
from .AI3 import AuradineFluxAI3680

View File

@@ -1,6 +0,0 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.models import AuradineAD2500
class AuradineFluxAD2500(AuradineAD2500, Auradine):
pass

View File

@@ -1,6 +0,0 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.models import AuradineAD3500
class AuradineFluxAD3500(AuradineAD3500, Auradine):
pass

View File

@@ -1,2 +0,0 @@
from .AD2 import AuradineFluxAD2500
from .AD3 import AuradineFluxAD3500

View File

@@ -1,3 +0,0 @@
from .AD import *
from .AI import *
from .AT import *

View File

@@ -14,9 +14,9 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AvalonMiner from pyasic.miners.backends import CGMinerAvalon
from pyasic.miners.models import Avalon1026 from pyasic.miners.types import Avalon1026
class CGMinerAvalon1026(AvalonMiner, Avalon1026): class CGMinerAvalon1026(CGMinerAvalon, Avalon1026):
pass pass

View File

@@ -14,9 +14,9 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AvalonMiner from pyasic.miners.backends import CGMinerAvalon
from pyasic.miners.models import Avalon1047 from pyasic.miners.types import Avalon1047
class CGMinerAvalon1047(AvalonMiner, Avalon1047): class CGMinerAvalon1047(CGMinerAvalon, Avalon1047):
pass pass

View File

@@ -14,9 +14,9 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AvalonMiner from pyasic.miners.backends import CGMinerAvalon
from pyasic.miners.models import Avalon1066 from pyasic.miners.types import Avalon1066
class CGMinerAvalon1066(AvalonMiner, Avalon1066): class CGMinerAvalon1066(CGMinerAvalon, Avalon1066):
pass pass

View File

@@ -14,9 +14,9 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AvalonMiner from pyasic.miners.backends import CGMinerAvalon
from pyasic.miners.models import Avalon1166Pro from pyasic.miners.types import Avalon1166Pro
class CGMinerAvalon1166Pro(AvalonMiner, Avalon1166Pro): class CGMinerAvalon1166Pro(CGMinerAvalon, Avalon1166Pro):
pass pass

View File

@@ -14,9 +14,9 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AvalonMiner from pyasic.miners.backends import CGMinerAvalon
from pyasic.miners.models import Avalon1246 from pyasic.miners.types import Avalon1246
class CGMinerAvalon1246(AvalonMiner, Avalon1246): class CGMinerAvalon1246(CGMinerAvalon, Avalon1246):
pass pass

View File

@@ -14,9 +14,9 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AvalonMiner from pyasic.miners.backends import CGMinerAvalon
from pyasic.miners.models import Avalon721 from pyasic.miners.types import Avalon721
class CGMinerAvalon721(AvalonMiner, Avalon721): class CGMinerAvalon721(CGMinerAvalon, Avalon721):
pass pass

View File

@@ -14,9 +14,9 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AvalonMiner from pyasic.miners.backends import CGMinerAvalon
from pyasic.miners.models import Avalon741 from pyasic.miners.types import Avalon741
class CGMinerAvalon741(AvalonMiner, Avalon741): class CGMinerAvalon741(CGMinerAvalon, Avalon741):
pass pass

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