Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d80ce5a0e | ||
|
|
ddcafe0f2b | ||
|
|
ea195b34db | ||
|
|
7377cb0d26 | ||
|
|
24b66de971 | ||
|
|
62d664a14c | ||
|
|
03b9a90f68 | ||
|
|
fefe0324b9 | ||
|
|
62b14a78b7 | ||
|
|
0ff505bbb4 | ||
|
|
b6c8c930a2 | ||
|
|
903bb93c4e | ||
|
|
59667cf104 | ||
|
|
3fd1b41bec | ||
|
|
6569107f64 | ||
|
|
9d746a6dcb | ||
|
|
fce4c07c32 | ||
|
|
094857758a | ||
|
|
2a49b89849 | ||
|
|
4ecd135734 | ||
|
|
836defc216 | ||
|
|
f8f777b5b5 | ||
|
|
b15e0a7363 | ||
|
|
5c1d06f743 | ||
|
|
51de56feb3 | ||
|
|
256a4ac909 | ||
|
|
09800c8ad2 | ||
|
|
83a7d8c60f | ||
|
|
ee2698be50 | ||
|
|
dc43087b0d | ||
|
|
4fa3511725 | ||
|
|
4b9ae70424 | ||
|
|
74ebffb4fc | ||
|
|
54206da449 | ||
|
|
dd54ff7ee4 | ||
|
|
abef0c3d59 | ||
|
|
957c9a3678 | ||
|
|
50ccfec1b3 | ||
|
|
8e7d6751e2 | ||
|
|
b77c4173c6 | ||
|
|
4941cffb70 | ||
|
|
81d5d23189 | ||
|
|
9da5a836ce | ||
|
|
c9a536fc60 | ||
|
|
fa172b56b0 | ||
|
|
ee45f2342e | ||
|
|
1f59ef025d | ||
|
|
d6a153144f | ||
|
|
99001e2e13 | ||
|
|
92b847656e | ||
|
|
a41525e828 | ||
|
|
5e9588cc56 | ||
|
|
b8239703c1 | ||
|
|
5d49135b59 | ||
|
|
3a5a76080b | ||
|
|
f23e10d629 | ||
|
|
b7d4891140 | ||
|
|
5f5cbd9060 | ||
|
|
8379359caf | ||
|
|
62238192ce | ||
|
|
1997003643 | ||
|
|
3a81844898 | ||
|
|
0ac80fb205 | ||
|
|
9494018c12 | ||
|
|
0bc86c98c5 | ||
|
|
f0d69c9ca7 | ||
|
|
b81590bd2e | ||
|
|
a53e01df6f | ||
|
|
f63e063954 | ||
|
|
9cbaf7076a | ||
|
|
daa5ac5870 | ||
|
|
0b8c08016b | ||
|
|
8c768d351b | ||
|
|
c9e7fa2629 | ||
|
|
9d3f2b5968 | ||
|
|
283e3d5e11 | ||
|
|
add4b575c2 | ||
|
|
af2f1e9ad5 | ||
|
|
8258320a7b |
17
.coveragerc
Normal file
17
.coveragerc
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
# Skip @abstractmethod
|
||||||
|
@abstractmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
|
||||||
|
# Don't complain if tests don't hit defensive assertion code:
|
||||||
|
raise AssertionError
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# Don't complain about missing debug-only code:
|
||||||
|
def __repr__
|
||||||
|
if self\.debug
|
||||||
|
|
||||||
|
# Don't complain if non-runnable code isn't run:
|
||||||
|
if 0:
|
||||||
|
if __name__ == .__main__.:
|
||||||
2
.github/workflows/python-publish.yml
vendored
2
.github/workflows/python-publish.yml
vendored
@@ -19,4 +19,4 @@ jobs:
|
|||||||
- name: Build using poetry and publish to PyPi
|
- name: Build using poetry and publish to PyPi
|
||||||
uses: JRubics/poetry-publish@v1.11
|
uses: JRubics/poetry-publish@v1.11
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
|||||||
21
.pre-commit-config.yaml
Normal file
21
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.3.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-added-large-files
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.6.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: unittest
|
||||||
|
name: unittest
|
||||||
|
entry: python -m unittest discover
|
||||||
|
language: system
|
||||||
|
'types': [python]
|
||||||
|
args: ["-p '*test.py'"] # Probably this option is absolutely not needed.
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [commit]
|
||||||
47
README.md
47
README.md
@@ -5,8 +5,11 @@
|
|||||||
[](https://pypi.org/project/pyasic/)
|
[](https://pypi.org/project/pyasic/)
|
||||||
[](https://pypi.org/project/pyasic/)
|
[](https://pypi.org/project/pyasic/)
|
||||||
[](https://pyasic.readthedocs.io/en/latest/)
|
[](https://pyasic.readthedocs.io/en/latest/)
|
||||||

|
[](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
|
||||||

|
[](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
|
||||||
|
## Supported Miners
|
||||||
|
Supported miners are listed in the docs, [here](https://pyasic.readthedocs.io/en/latest/miners/supported_types/)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/)
|
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/)
|
||||||
|
|
||||||
@@ -18,13 +21,15 @@ You can install pyasic directly from pip with the command `pip install pyasic`
|
|||||||
For those of you who aren't comfortable with code and developer tools, there are windows builds of GUI applications that use this library here -> (https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing)
|
For those of you who aren't comfortable with code and developer tools, there are windows builds of GUI applications that use this library here -> (https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing)
|
||||||
|
|
||||||
### Developers
|
### Developers
|
||||||
To use this repo, first download it, create a virtual environment, enter the virtual environment, and install relevant packages by navigating to this directory and running ```pip install -r requirements.txt``` on Windows or ```pip3 install -r requirements.txt``` on Mac or UNIX if the first command fails.
|
To use this repo, first download it, create a virtual environment, enter the virtual environment, and install relevant packages by navigating to this directory and running ```pip install -r requirements-dev.txt``` on Windows or ```pip3 install -r requirements-dev.txt``` on Mac or UNIX if the first command fails.
|
||||||
|
|
||||||
You can also use poetry by initializing and running ```poetry install```
|
You can also use poetry by initializing and running ```poetry install```, and you will have to install `pre-commit` (`pip install pre-commit`).
|
||||||
|
|
||||||
|
Finally, initialize pre-commit hooks with `pre-commit install`
|
||||||
|
|
||||||
### Interfacing with miners programmatically
|
### Interfacing with miners programmatically
|
||||||
|
|
||||||
##### Note: If you are trying to interface with Whatsminers, there is a bug in the way they are interacted with on Windows, so to fix that you need to change the event loop policy using this code:
|
##### Note: If you are trying to interface with Whatsminers, there is a bug in the way they are interacted with on Windows, so to fix that you need to change the event loop policy using this code:
|
||||||
```python
|
```python
|
||||||
# need to import these 2 libraries, you need asyncio anyway so make sure you have sys imported
|
# need to import these 2 libraries, you need asyncio anyway so make sure you have sys imported
|
||||||
import sys
|
import sys
|
||||||
@@ -66,18 +71,18 @@ async def scan_and_get_data():
|
|||||||
# Scan the network for miners
|
# Scan the network for miners
|
||||||
# This function returns a list of miners of the correct type as a class
|
# This function returns a list of miners of the correct type as a class
|
||||||
miners: list = await net.scan_network_for_miners()
|
miners: list = await net.scan_network_for_miners()
|
||||||
|
|
||||||
# We can now get data from any of these 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
|
# To do them all we have to create a list of tasks and gather them
|
||||||
tasks = [miner.get_data() for miner in miners]
|
tasks = [miner.get_data() for miner in miners]
|
||||||
# Gather all tasks asynchronously and run them
|
# Gather all tasks asynchronously and run them
|
||||||
data = await asyncio.gather(*tasks)
|
data = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
# Data is now a list of MinerData, and we can reference any part of that
|
# Data is now a list of MinerData, and we can reference any part of that
|
||||||
# Print out all data for now
|
# Print out all data for now
|
||||||
for item in data:
|
for item in data:
|
||||||
print(item)
|
print(item)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(scan_and_get_data())
|
asyncio.run(scan_and_get_data())
|
||||||
```
|
```
|
||||||
@@ -89,7 +94,7 @@ if __name__ == "__main__":
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pyasic.miners.miner_factory import MinerFactory
|
from pyasic.miners import get_miner
|
||||||
|
|
||||||
# Fix whatsminer bug
|
# Fix whatsminer bug
|
||||||
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
||||||
@@ -101,8 +106,8 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
|
|||||||
async def get_miner_data(miner_ip: str):
|
async def get_miner_data(miner_ip: str):
|
||||||
# Use MinerFactory to get miner
|
# Use MinerFactory to get miner
|
||||||
# MinerFactory is a singleton, so we can just get the instance in place
|
# MinerFactory is a singleton, so we can just get the instance in place
|
||||||
miner = await MinerFactory().get_miner(miner_ip)
|
miner = await get_miner(miner_ip)
|
||||||
|
|
||||||
# Get data from the miner
|
# Get data from the miner
|
||||||
data = await miner.get_data()
|
data = await miner.get_data()
|
||||||
print(data)
|
print(data)
|
||||||
@@ -120,7 +125,7 @@ If needed, this library exposes a wrapper for the miner API that can be used for
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pyasic.miners.miner_factory import MinerFactory
|
from pyasic.miners import get_miner
|
||||||
|
|
||||||
# Fix whatsminer bug
|
# Fix whatsminer bug
|
||||||
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
||||||
@@ -130,12 +135,12 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
|
|||||||
|
|
||||||
async def get_api_commands(miner_ip: str):
|
async def get_api_commands(miner_ip: str):
|
||||||
# Get the miner
|
# Get the miner
|
||||||
miner = await MinerFactory().get_miner(miner_ip)
|
miner = await get_miner(miner_ip)
|
||||||
|
|
||||||
# List all available commands
|
# List all available commands
|
||||||
print(miner.api.get_commands())
|
print(miner.api.get_commands())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(get_api_commands("192.168.1.69"))
|
asyncio.run(get_api_commands("192.168.1.69"))
|
||||||
```
|
```
|
||||||
@@ -148,7 +153,7 @@ The miner API commands will raise an `APIError` if they fail with a bad status c
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pyasic.miners.miner_factory import MinerFactory
|
from pyasic.miners import get_miner
|
||||||
|
|
||||||
# Fix whatsminer bug
|
# Fix whatsminer bug
|
||||||
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
||||||
@@ -158,14 +163,14 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
|
|||||||
|
|
||||||
async def get_api_commands(miner_ip: str):
|
async def get_api_commands(miner_ip: str):
|
||||||
# Get the miner
|
# Get the miner
|
||||||
miner = await MinerFactory().get_miner(miner_ip)
|
miner = await get_miner(miner_ip)
|
||||||
|
|
||||||
# Run the devdetails command
|
# Run the devdetails command
|
||||||
# This is equivalent to await miner.api.send_command("devdetails")
|
# This is equivalent to await miner.api.send_command("devdetails")
|
||||||
devdetails: dict = await miner.api.devdetails()
|
devdetails: dict = await miner.api.devdetails()
|
||||||
print(devdetails)
|
print(devdetails)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(get_api_commands("192.168.1.69"))
|
asyncio.run(get_api_commands("192.168.1.69"))
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
## Miner APIs
|
## Miner APIs
|
||||||
Each miner has a unique API that is used to communicate with it.
|
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 of these API types has commands that differ between them, and some commands have data that others do not.
|
||||||
Each miner that is a subclass of `BaseMiner` should have an API linked to it as `Miner.api`.
|
Each miner that is a subclass of [`BaseMiner`][pyasic.miners.BaseMiner] should have an API linked to it as `Miner.api`.
|
||||||
|
|
||||||
All API implementations inherit from [`BaseMinerAPI`][pyasic.API.BaseMinerAPI], which implements the basic communications protocols.
|
All API implementations inherit from [`BaseMinerAPI`][pyasic.API.BaseMinerAPI], which implements the basic communications protocols.
|
||||||
|
|
||||||
BaseMinerAPI should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
|
[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
|
||||||
|
[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] cannot be instantiated directly, it will raise a `TypeError`.
|
||||||
Use these instead -
|
Use these instead -
|
||||||
|
|
||||||
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]
|
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]
|
||||||
|
|||||||
25
docs/data/error_codes.md
Normal file
25
docs/data/error_codes.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# pyasic
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Whatsminer Error Codes
|
||||||
|
::: pyasic.data.error_codes.WhatsminerError
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Braiins OS Error Codes
|
||||||
|
::: pyasic.data.error_codes.BraiinsOSError
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## X19 Error Codes
|
||||||
|
::: pyasic.data.error_codes.X19Error
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
[](https://pypi.org/project/pyasic/)
|
[](https://pypi.org/project/pyasic/)
|
||||||
[](https://pypi.org/project/pyasic/)
|
[](https://pypi.org/project/pyasic/)
|
||||||
[](https://pyasic.readthedocs.io/en/latest/)
|
[](https://pyasic.readthedocs.io/en/latest/)
|
||||||
|
[](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
|
||||||
|
[](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
|
||||||
|
|
||||||
## 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.
|
||||||
@@ -27,13 +29,13 @@ 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("192.168.1.50") # 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_network_for_miners()
|
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()
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,8 +56,8 @@ async def get_miners(): # define async scan function to allow awaiting
|
|||||||
miner_1 = await MinerFactory().get_miner("192.168.1.75")
|
miner_1 = await MinerFactory().get_miner("192.168.1.75")
|
||||||
miner_2 = await MinerFactory().get_miner("192.168.1.76")
|
miner_2 = await MinerFactory().get_miner("192.168.1.76")
|
||||||
print(miner_1, miner_2)
|
print(miner_1, miner_2)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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()
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -76,7 +78,7 @@ async def gather_miner_data():
|
|||||||
print(miner_data) # all data from the dataclass
|
print(miner_data) # all data from the dataclass
|
||||||
print(miner_data.hashrate) # hashrate of the miner in TH/s
|
print(miner_data.hashrate) # hashrate of the miner in TH/s
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(gather_miner_data())
|
asyncio.run(gather_miner_data())
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,13 +91,13 @@ 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("192.168.1.50")
|
network = MinerNetwork("192.168.1.50")
|
||||||
miners = await network.scan_network_for_miners()
|
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])
|
||||||
|
|
||||||
for miner_data in all_miner_data:
|
for miner_data in all_miner_data:
|
||||||
print(miner_data) # print out all the data one by one
|
print(miner_data) # print out all the data one by one
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(gather_miner_data())
|
asyncio.run(gather_miner_data())
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -57,3 +57,61 @@
|
|||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## S17 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S17+ (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.S17_Plus.BOSMinerS17Plus
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S17 Pro (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.S17_Pro.BOSMinerS17Pro
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S17e (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.S17e.BOSMinerS17e
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T17 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T17+ (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.T17_Plus.BOSMinerT17Plus
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## T17e (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X17.T17e.BOSMinerT17e
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|||||||
@@ -50,3 +50,45 @@
|
|||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## S19 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S19 Pro (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.S19_Pro.BOSMinerS19Pro
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
|
## S19j (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.S19j.BOSMinerS19j
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## S19j Pro (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.S19j_Pro.BOSMinerS19jPro
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## T19 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
# pyasic
|
# pyasic
|
||||||
## X9 Models
|
## X9 Models
|
||||||
|
|
||||||
|
|
||||||
|
## X9 (BOS)
|
||||||
|
|
||||||
|
::: pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
## S9
|
## S9
|
||||||
|
|
||||||
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9
|
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9
|
||||||
|
|||||||
10
docs/miners/base_miner.md
Normal file
10
docs/miners/base_miner.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# pyasic
|
||||||
|
## Base Miner
|
||||||
|
[`BaseMiner`][pyasic.miners.BaseMiner] is the basis for all miner classes, they all subclass (usually indirectly) from this class.
|
||||||
|
|
||||||
|
You may not instantiate this class on its own, only subclass from it. Trying to instantiate an instance of this class will raise `TypeError`.
|
||||||
|
|
||||||
|
::: pyasic.miners.BaseMiner
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
10
docs/miners/innosilicon/T3X.md
Normal file
10
docs/miners/innosilicon/T3X.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# pyasic
|
||||||
|
## T3X Models
|
||||||
|
|
||||||
|
## T3H+
|
||||||
|
|
||||||
|
::: pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
@@ -6,3 +6,15 @@
|
|||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## AnyMiner
|
||||||
|
::: pyasic.miners.miner_factory.AnyMiner
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
[`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] is a placeholder type variable used for typing returns of functions.
|
||||||
|
A function returning [`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] will always return a subclass of [`BaseMiner`][pyasic.miners.BaseMiner],
|
||||||
|
and is used to specify a function returning some arbitrary type of miner class instance.
|
||||||
|
|||||||
@@ -3,11 +3,26 @@
|
|||||||
|
|
||||||
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
|
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
|
||||||
|
|
||||||
## Miner List
|
|
||||||
|
|
||||||
##### pyasic currently supports the following miners and subtypes:
|
##### pyasic currently supports the following miners and subtypes:
|
||||||
* Braiins OS+ Devices:
|
* Braiins OS+ Devices:
|
||||||
* All devices supported by BraiinsOS+ are supported here.
|
* X19 Series:
|
||||||
|
* [S19][pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19]
|
||||||
|
* [S19 Pro][pyasic.miners.antminer.bosminer.X19.S19_Pro.BOSMinerS19Pro]
|
||||||
|
* [S19j][pyasic.miners.antminer.bosminer.X19.S19j.BOSMinerS19j]
|
||||||
|
* [S19j Pro][pyasic.miners.antminer.bosminer.X19.S19j_Pro.BOSMinerS19jPro]
|
||||||
|
* [T19][pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19]
|
||||||
|
* X17 Series:
|
||||||
|
* [S17][pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17]
|
||||||
|
* [S17+][pyasic.miners.antminer.bosminer.X17.S17_Plus.BOSMinerS17Plus]
|
||||||
|
* [S17 Pro][pyasic.miners.antminer.bosminer.X17.S17_Pro.BOSMinerS17Pro]
|
||||||
|
* [S17e][pyasic.miners.antminer.bosminer.X17.S17e.BOSMinerS17e]
|
||||||
|
* [T17][pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17]
|
||||||
|
* [T17+][pyasic.miners.antminer.bosminer.X17.T17_Plus.BOSMinerT17Plus]
|
||||||
|
* [T17e][pyasic.miners.antminer.bosminer.X17.T17e.BOSMinerT17e]
|
||||||
|
* X9 Series:
|
||||||
|
* [S9][pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9]
|
||||||
|
* [S9i][pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9]
|
||||||
|
* [S9j][pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9]
|
||||||
* Stock Firmware Whatsminers:
|
* Stock Firmware Whatsminers:
|
||||||
* M3X Series:
|
* M3X Series:
|
||||||
* [M30S][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30S]:
|
* [M30S][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30S]:
|
||||||
@@ -22,11 +37,16 @@ Supported miner types are here on this list. If your miner (or miner version) i
|
|||||||
* [M30S++][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlus]:
|
* [M30S++][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlus]:
|
||||||
* [VG30][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30]
|
* [VG30][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30]
|
||||||
* [VG40][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40]
|
* [VG40][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40]
|
||||||
|
* [VH60][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVH60]
|
||||||
* [M31S][pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S]
|
* [M31S][pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S]
|
||||||
* [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]:
|
* [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]:
|
||||||
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20]
|
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20]
|
||||||
|
* [M32][pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32]
|
||||||
|
* [V20][pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20]
|
||||||
* [M32S][pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S]
|
* [M32S][pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S]
|
||||||
* M2X Series:
|
* M2X Series:
|
||||||
|
* [M20][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20]:
|
||||||
|
* [V10][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20V10]
|
||||||
* [M20S][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S]:
|
* [M20S][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S]:
|
||||||
* [V10][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV10]
|
* [V10][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV10]
|
||||||
* [V20][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV20]
|
* [V20][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV20]
|
||||||
@@ -71,3 +91,6 @@ Supported miner types are here on this list. If your miner (or miner version) i
|
|||||||
* [A1026][pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026]
|
* [A1026][pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026]
|
||||||
* [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047]
|
* [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047]
|
||||||
* [A1066][pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066]
|
* [A1066][pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066]
|
||||||
|
* Stock Firmware Innosilicon Miners:
|
||||||
|
* T3X Series:
|
||||||
|
* [T3H+][pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus]
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
# pyasic
|
# pyasic
|
||||||
## M2X Models
|
## M2X Models
|
||||||
|
|
||||||
|
## M20
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M20V10
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20V10
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
## M20S
|
## M20S
|
||||||
|
|
||||||
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S
|
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
|
||||||
## M30S+VG40
|
## M30S++VG40
|
||||||
|
|
||||||
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
|
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
|
||||||
handler: python
|
handler: python
|
||||||
@@ -97,6 +97,14 @@
|
|||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
|
||||||
|
## M30S++VH60
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVH60
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
|
||||||
## M31S
|
## M31S
|
||||||
|
|
||||||
@@ -122,6 +130,22 @@
|
|||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
|
||||||
|
## M32
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## M32V20
|
||||||
|
|
||||||
|
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
## M32S
|
## M32S
|
||||||
|
|
||||||
::: pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S
|
::: pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# pyasic
|
# pyasic
|
||||||
## Miner Network Range
|
## Miner Network Range
|
||||||
|
|
||||||
`MinerNetworkRange` is a class used by [`MinerNetwork`][pyasic.network.MinerNetwork] to handle any constructor stings.
|
[`MinerNetworkRange`][pyasic.network.net_range.MinerNetworkRange] is a class used by [`MinerNetwork`][pyasic.network.MinerNetwork] to handle any constructor stings.
|
||||||
The goal is to emulate what is produced by `ipaddress.ip_network` by allowing [`MinerNetwork`][pyasic.network.MinerNetwork] to get a list of hosts.
|
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.
|
This allows this class to be the [`MinerNetwork.network`][pyasic.network.MinerNetwork] and hence be used for scanning.
|
||||||
|
|
||||||
|
|||||||
@@ -22,23 +22,28 @@ nav:
|
|||||||
- Avalon 10X: "miners/avalonminer/A10X.md"
|
- Avalon 10X: "miners/avalonminer/A10X.md"
|
||||||
- Whatsminer M2X: "miners/whatsminer/M2X.md"
|
- Whatsminer M2X: "miners/whatsminer/M2X.md"
|
||||||
- Whatsminer M3X: "miners/whatsminer/M3X.md"
|
- Whatsminer M3X: "miners/whatsminer/M3X.md"
|
||||||
|
- Innosilicon T3X: "miners/innosilicon/T3X.md"
|
||||||
|
|
||||||
- Network:
|
- Network:
|
||||||
- Miner Network: "network/miner_network.md"
|
- Miner Network: "network/miner_network.md"
|
||||||
- Miner Network Range: "network/miner_network_range.md"
|
- Miner Network Range: "network/miner_network_range.md"
|
||||||
- Data:
|
- Data:
|
||||||
- Miner Data: "data/miner_data.md"
|
- Miner Data: "data/miner_data.md"
|
||||||
|
- Error Codes: "data/error_codes.md"
|
||||||
- Config:
|
- Config:
|
||||||
- Miner Config: "config/miner_config.md"
|
- Miner Config: "config/miner_config.md"
|
||||||
- Advanced:
|
- Advanced:
|
||||||
- Miner APIs:
|
- Miner APIs:
|
||||||
- Base: "API/api.md"
|
- Intro: "API/api.md"
|
||||||
- BMMiner: "API/bmminer.md"
|
- BMMiner: "API/bmminer.md"
|
||||||
- BOSMiner: "API/bosminer.md"
|
- BOSMiner: "API/bosminer.md"
|
||||||
- BTMiner: "API/btminer.md"
|
- BTMiner: "API/btminer.md"
|
||||||
- CGMiner: "API/cgminer.md"
|
- CGMiner: "API/cgminer.md"
|
||||||
- Unknown: "API/unknown.md"
|
- Unknown: "API/unknown.md"
|
||||||
|
|
||||||
|
- Base Miner: "miners/base_miner.md"
|
||||||
|
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- mkdocstrings
|
- mkdocstrings
|
||||||
- search
|
- search
|
||||||
|
|||||||
6
poetry.lock
generated
6
poetry.lock
generated
@@ -17,7 +17,7 @@ trio = ["trio (>=0.16)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asyncssh"
|
name = "asyncssh"
|
||||||
version = "2.11.0"
|
version = "2.12.0"
|
||||||
description = "AsyncSSH: Asynchronous SSHv2 client and server library"
|
description = "AsyncSSH: Asynchronous SSHv2 client and server library"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -218,8 +218,8 @@ anyio = [
|
|||||||
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
|
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
|
||||||
]
|
]
|
||||||
asyncssh = [
|
asyncssh = [
|
||||||
{file = "asyncssh-2.11.0-py3-none-any.whl", hash = "sha256:7302348cbd54c58d3259da17f13e77912de1b005e366b15c8b183d948c8a91a8"},
|
{file = "asyncssh-2.12.0-py3-none-any.whl", hash = "sha256:6841c4242c606fd51188c974ec2f4887efeec67ecdfa5b84140711dacd985ab3"},
|
||||||
{file = "asyncssh-2.11.0.tar.gz", hash = "sha256:59c36ce77ba9dda8dd57ad875776e7105ddb1fa851bc039bb3aeadeac4f67b56"},
|
{file = "asyncssh-2.12.0.tar.gz", hash = "sha256:274101322c4b941823aeed8e1ab6e7be5191686c6db2d2bd35afeba30505e780"},
|
||||||
]
|
]
|
||||||
certifi = [
|
certifi = [
|
||||||
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
|
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
|
||||||
|
|||||||
@@ -19,33 +19,7 @@ import warnings
|
|||||||
import logging
|
import logging
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
from pyasic.errors import APIError, APIWarning
|
||||||
class APIError(Exception):
|
|
||||||
def __init__(self, *args):
|
|
||||||
if args:
|
|
||||||
self.message = args[0]
|
|
||||||
else:
|
|
||||||
self.message = None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.message:
|
|
||||||
return f"{self.message}"
|
|
||||||
else:
|
|
||||||
return "Incorrect API parameters."
|
|
||||||
|
|
||||||
|
|
||||||
class APIWarning(Warning):
|
|
||||||
def __init__(self, *args):
|
|
||||||
if args:
|
|
||||||
self.message = args[0]
|
|
||||||
else:
|
|
||||||
self.message = None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.message:
|
|
||||||
return f"{self.message}"
|
|
||||||
else:
|
|
||||||
return "Incorrect API parameters."
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMinerAPI:
|
class BaseMinerAPI:
|
||||||
@@ -55,6 +29,11 @@ class BaseMinerAPI:
|
|||||||
# ip address of the miner
|
# ip address of the miner
|
||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ipaddress.ip_address(ip)
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls is BaseMinerAPI:
|
||||||
|
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
def get_commands(self) -> list:
|
def get_commands(self) -> list:
|
||||||
"""Get a list of command accessible to a specific type of API on the miner.
|
"""Get a list of command accessible to a specific type of API on the miner.
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ import hashlib
|
|||||||
import binascii
|
import binascii
|
||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from passlib.handlers.md5_crypt import md5_crypt
|
from passlib.handlers.md5_crypt import md5_crypt
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
||||||
from pyasic.API import BaseMinerAPI, APIError
|
from pyasic.errors import APIError
|
||||||
|
from pyasic.API import BaseMinerAPI
|
||||||
from pyasic.settings import PyasicSettings
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
|
||||||
@@ -182,13 +184,13 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
pwd: str = PyasicSettings().global_whatsminer_password,
|
pwd: str = PyasicSettings().global_whatsminer_password,
|
||||||
):
|
):
|
||||||
super().__init__(ip, port)
|
super().__init__(ip, port)
|
||||||
self.admin_pwd = pwd
|
self.pwd = pwd
|
||||||
self.current_token = None
|
self.current_token = None
|
||||||
|
|
||||||
async def send_command(
|
async def send_command(
|
||||||
self,
|
self,
|
||||||
command: str or bytes,
|
command: Union[str, bytes],
|
||||||
parameters: str or int or bool = None,
|
parameters: Union[str, int, bool] = None,
|
||||||
ignore_errors: bool = False,
|
ignore_errors: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -259,7 +261,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
data = await self.send_command("get_token")
|
data = await self.send_command("get_token")
|
||||||
|
|
||||||
# encrypt the admin password with the salt
|
# encrypt the admin password with the salt
|
||||||
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + "$")
|
pwd = _crypt(self.pwd, "$1$" + data["Msg"]["salt"] + "$")
|
||||||
pwd = pwd.split("$")
|
pwd = pwd.split("$")
|
||||||
|
|
||||||
# take the 4th item from the pwd split
|
# take the 4th item from the pwd split
|
||||||
@@ -436,9 +438,10 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
|
|
||||||
async def set_led(
|
async def set_led(
|
||||||
self,
|
self,
|
||||||
|
auto: bool = True,
|
||||||
color: str = "red",
|
color: str = "red",
|
||||||
period: int = 2000,
|
period: int = 60,
|
||||||
duration: int = 1000,
|
duration: int = 20,
|
||||||
start: int = 0,
|
start: int = 0,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Set the LED on the miner using the API.
|
"""Set the LED on the miner using the API.
|
||||||
@@ -449,6 +452,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
changing the password of the miner using the Whatsminer tool.
|
changing the password of the miner using the Whatsminer tool.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
auto: Whether or not to reset the LED to auto mode.
|
||||||
color: The LED color to set, either 'red' or 'green'.
|
color: The LED color to set, either 'red' or 'green'.
|
||||||
period: The flash cycle in ms.
|
period: The flash cycle in ms.
|
||||||
duration: LED on time in the cycle in ms.
|
duration: LED on time in the cycle in ms.
|
||||||
@@ -458,16 +462,19 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
A reply informing of the status of setting the LED.
|
A reply informing of the status of setting the LED.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
command = {
|
if not auto:
|
||||||
"cmd": "set_led",
|
command = {
|
||||||
"color": color,
|
"cmd": "set_led",
|
||||||
"period": period,
|
"color": color,
|
||||||
"duration": duration,
|
"period": period,
|
||||||
"start": start,
|
"duration": duration,
|
||||||
}
|
"start": start,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
command = {"cmd": "set_led", "param": "auto"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command, ignore_errors=True)
|
||||||
|
|
||||||
async def set_low_power(self) -> dict:
|
async def set_low_power(self) -> dict:
|
||||||
"""Set low power mode on the miner using the API.
|
"""Set low power mode on the miner using the API.
|
||||||
@@ -541,6 +548,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
|
|
||||||
A reply informing of the status of setting the password.
|
A reply informing of the status of setting the password.
|
||||||
"""
|
"""
|
||||||
|
self.pwd = old_pwd
|
||||||
# check if password length is greater than 8 bytes
|
# check if password length is greater than 8 bytes
|
||||||
if len(new_pwd.encode("utf-8")) > 8:
|
if len(new_pwd.encode("utf-8")) > 8:
|
||||||
raise APIError(
|
raise APIError(
|
||||||
@@ -550,7 +558,12 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
|
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
try:
|
||||||
|
data = await self.send_command(enc_command)
|
||||||
|
except APIError as e:
|
||||||
|
raise e
|
||||||
|
self.pwd = new_pwd
|
||||||
|
return data
|
||||||
|
|
||||||
async def set_target_freq(self, percent: int) -> dict:
|
async def set_target_freq(self, percent: int) -> dict:
|
||||||
"""Update the target frequency.
|
"""Update the target frequency.
|
||||||
|
|||||||
@@ -11,3 +11,51 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
from pyasic.API.bmminer import BMMinerAPI
|
||||||
|
from pyasic.API.bosminer import BOSMinerAPI
|
||||||
|
from pyasic.API.btminer import BTMinerAPI
|
||||||
|
from pyasic.API.cgminer import CGMinerAPI
|
||||||
|
from pyasic.API.unknown import UnknownAPI
|
||||||
|
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
|
||||||
|
from pyasic.data import (
|
||||||
|
MinerData,
|
||||||
|
BraiinsOSError,
|
||||||
|
InnosiliconError,
|
||||||
|
WhatsminerError,
|
||||||
|
X19Error,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pyasic.errors import APIError, APIWarning
|
||||||
|
|
||||||
|
from pyasic.miners import get_miner
|
||||||
|
from pyasic.miners.base import AnyMiner
|
||||||
|
from pyasic.miners.miner_factory import MinerFactory
|
||||||
|
from pyasic.miners.miner_listener import MinerListener
|
||||||
|
|
||||||
|
from pyasic.network import MinerNetwork
|
||||||
|
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BMMinerAPI",
|
||||||
|
"BOSMinerAPI",
|
||||||
|
"BTMinerAPI",
|
||||||
|
"CGMinerAPI",
|
||||||
|
"UnknownAPI",
|
||||||
|
"MinerConfig",
|
||||||
|
"MinerData",
|
||||||
|
"BraiinsOSError",
|
||||||
|
"InnosiliconError",
|
||||||
|
"WhatsminerError",
|
||||||
|
"X19Error",
|
||||||
|
"APIError",
|
||||||
|
"APIWarning",
|
||||||
|
"get_miner",
|
||||||
|
"AnyMiner",
|
||||||
|
"MinerFactory",
|
||||||
|
"MinerListener",
|
||||||
|
"MinerNetwork",
|
||||||
|
"PyasicSettings",
|
||||||
|
]
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ class _Pool:
|
|||||||
self.password = data[key]
|
self.password = data[key]
|
||||||
return self
|
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:
|
def as_x19(self, user_suffix: str = None) -> dict:
|
||||||
"""Convert the data in this class to a dict usable by an X19 device.
|
"""Convert the data in this class to a dict usable by an X19 device.
|
||||||
|
|
||||||
@@ -65,6 +78,23 @@ class _Pool:
|
|||||||
pool = {"url": self.url, "user": username, "pass": self.password}
|
pool = {"url": self.url, "user": username, "pass": self.password}
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
|
def as_inno(self, user_suffix: str = None) -> dict:
|
||||||
|
"""Convert the data in this class to a dict usable by an Innosilicon device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
username = self.username
|
||||||
|
if user_suffix:
|
||||||
|
username = f"{username}{user_suffix}"
|
||||||
|
|
||||||
|
pool = {
|
||||||
|
f"Pool": self.url,
|
||||||
|
f"UserName": username,
|
||||||
|
f"Password": self.password,
|
||||||
|
}
|
||||||
|
return pool
|
||||||
|
|
||||||
def as_avalon(self, user_suffix: str = None) -> str:
|
def as_avalon(self, user_suffix: str = None) -> str:
|
||||||
"""Convert the data in this class to a string usable by an Avalonminer device.
|
"""Convert the data in this class to a string usable by an Avalonminer device.
|
||||||
|
|
||||||
@@ -141,6 +171,32 @@ class _PoolGroup:
|
|||||||
pools.append(pool.as_x19(user_suffix=user_suffix))
|
pools.append(pool.as_x19(user_suffix=user_suffix))
|
||||||
return pools
|
return pools
|
||||||
|
|
||||||
|
def as_inno(self, user_suffix: str = None) -> dict:
|
||||||
|
"""Convert the data in this class to a list usable by an Innosilicon device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
pools = {}
|
||||||
|
for idx, pool in enumerate(self.pools[:3]):
|
||||||
|
pool_data = pool.as_inno(user_suffix=user_suffix)
|
||||||
|
for key in pool_data:
|
||||||
|
pools[f"{key}{idx+1}"] = pool_data[key]
|
||||||
|
return pools
|
||||||
|
|
||||||
|
def as_wm(self, user_suffix: str = None) -> List[dict]:
|
||||||
|
"""Convert the data in this class to a list usable by an Whatsminer device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
pools = []
|
||||||
|
for pool in self.pools[:3]:
|
||||||
|
pools.append(pool.as_wm(user_suffix=user_suffix))
|
||||||
|
while len(pools) < 3:
|
||||||
|
pools.append({"url": None, "user": None, "pass": None})
|
||||||
|
return pools
|
||||||
|
|
||||||
def as_avalon(self, user_suffix: str = None) -> str:
|
def as_avalon(self, user_suffix: str = None) -> str:
|
||||||
"""Convert the data in this class to a dict usable by an Avalonminer device.
|
"""Convert the data in this class to a dict usable by an Avalonminer device.
|
||||||
|
|
||||||
@@ -191,7 +247,7 @@ class MinerConfig:
|
|||||||
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
|
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
|
||||||
temp_target: float = 70.0
|
temp_target: float = 70.0
|
||||||
temp_hot: float = 80.0
|
temp_hot: float = 80.0
|
||||||
temp_dangerous: float = 10.0
|
temp_dangerous: float = 100.0
|
||||||
|
|
||||||
minimum_fans: int = None
|
minimum_fans: int = None
|
||||||
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
|
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
|
||||||
@@ -287,6 +343,15 @@ class MinerConfig:
|
|||||||
self.pool_groups = pool_groups
|
self.pool_groups = pool_groups
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def from_api(self, pools: list):
|
||||||
|
_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):
|
def from_dict(self, data: dict):
|
||||||
"""Convert an output dict of this class back into usable data and save it to this class.
|
"""Convert an output dict of this class back into usable data and save it to this class.
|
||||||
|
|
||||||
@@ -318,6 +383,22 @@ class MinerConfig:
|
|||||||
"""
|
"""
|
||||||
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
|
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
|
||||||
|
|
||||||
|
def as_wm(self, user_suffix: str = None) -> List[dict]:
|
||||||
|
"""Convert the data in this class to a config usable by an Whatsminer device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
return self.pool_groups[0].as_wm(user_suffix=user_suffix)
|
||||||
|
|
||||||
|
def as_inno(self, user_suffix: str = None) -> dict:
|
||||||
|
"""Convert the data in this class to a config usable by an Innosilicon device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
|
||||||
|
|
||||||
def as_x19(self, user_suffix: str = None) -> str:
|
def as_x19(self, user_suffix: str = None) -> str:
|
||||||
"""Convert the data in this class to a config usable by an X19 device.
|
"""Convert the data in this class to a config usable by an X19 device.
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,13 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union, List
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import dataclass, field, asdict
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .error_codes import X19Error, WhatsminerError, BraiinsOSError, InnosiliconError
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -50,13 +54,15 @@ class MinerData:
|
|||||||
total_chips: The total number of chips on all boards. Calculated automatically.
|
total_chips: The total number of chips on all boards. Calculated automatically.
|
||||||
ideal_chips: The ideal number of chips in the miner as an int.
|
ideal_chips: The ideal number of chips in the miner as an int.
|
||||||
percent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
|
percent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
|
||||||
nominal: The nominal amount of chips in the miner. Calculated automatically.
|
nominal: Whether the number of chips in the miner is nominal. Calculated automatically.
|
||||||
pool_split: The pool split as a str.
|
pool_split: The pool split as a str.
|
||||||
pool_1_url: The first pool url on the miner as a str.
|
pool_1_url: The first pool url on the miner as a str.
|
||||||
pool_1_user: The first pool user on the miner as a str.
|
pool_1_user: The first pool user on the miner as a str.
|
||||||
pool_2_url: The second pool url 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.
|
pool_2_user: The second pool user on the miner as a str.
|
||||||
errors: A list of errors on the miner.
|
errors: A list of errors on the miner.
|
||||||
|
fault_light: Whether or not the fault light is on as a boolean.
|
||||||
|
efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ip: str
|
ip: str
|
||||||
@@ -65,11 +71,11 @@ class MinerData:
|
|||||||
model: str = "Unknown"
|
model: str = "Unknown"
|
||||||
hostname: str = "Unknown"
|
hostname: str = "Unknown"
|
||||||
hashrate: float = 0
|
hashrate: float = 0
|
||||||
left_board_hashrate: float = 0
|
left_board_hashrate: float = 0.0
|
||||||
center_board_hashrate: float = 0
|
center_board_hashrate: float = 0.0
|
||||||
right_board_hashrate: float = 0
|
right_board_hashrate: float = 0.0
|
||||||
temperature_avg: int = field(init=False)
|
temperature_avg: int = field(init=False)
|
||||||
env_temp: float = 0
|
env_temp: float = 0.0
|
||||||
left_board_temp: int = 0
|
left_board_temp: int = 0
|
||||||
left_board_chip_temp: int = 0
|
left_board_chip_temp: int = 0
|
||||||
center_board_temp: int = 0
|
center_board_temp: int = 0
|
||||||
@@ -94,11 +100,26 @@ class MinerData:
|
|||||||
pool_1_user: str = "Unknown"
|
pool_1_user: str = "Unknown"
|
||||||
pool_2_url: str = ""
|
pool_2_url: str = ""
|
||||||
pool_2_user: str = ""
|
pool_2_user: str = ""
|
||||||
errors: list = field(default_factory=list)
|
errors: List[
|
||||||
|
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
|
||||||
|
] = field(default_factory=list)
|
||||||
fault_light: Union[bool, None] = None
|
fault_light: Union[bool, None] = None
|
||||||
|
efficiency: int = field(init=False)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self.datetime = datetime.now()
|
self.datetime = datetime.now(timezone.utc).astimezone()
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
try:
|
||||||
|
return getattr(self, item)
|
||||||
|
except AttributeError:
|
||||||
|
raise KeyError(f"{item}")
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
return setattr(self, key, value)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter([item for item in self.asdict()])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_chips(self): # noqa - Skip PyCharm inspection
|
def total_chips(self): # noqa - Skip PyCharm inspection
|
||||||
@@ -144,5 +165,55 @@ class MinerData:
|
|||||||
def temperature_avg(self, val):
|
def temperature_avg(self, val):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def efficiency(self): # noqa - Skip PyCharm inspection
|
||||||
|
if self.hashrate == 0:
|
||||||
|
return 0
|
||||||
|
return round(self.wattage / self.hashrate)
|
||||||
|
|
||||||
|
@efficiency.setter
|
||||||
|
def efficiency(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
|
def as_json(self):
|
||||||
|
data = self.asdict()
|
||||||
|
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
||||||
|
return json.dumps(data)
|
||||||
|
|
||||||
|
def as_influxdb(self, measurement_name: str = "miner_data"):
|
||||||
|
tag_data = [measurement_name]
|
||||||
|
field_data = []
|
||||||
|
|
||||||
|
tags = ["ip", "mac", "model", "hostname"]
|
||||||
|
for attribute in self:
|
||||||
|
if attribute in tags:
|
||||||
|
escaped_data = self[attribute].replace(" ", "\\ ")
|
||||||
|
tag_data.append(f"{attribute}={escaped_data}")
|
||||||
|
continue
|
||||||
|
if isinstance(self[attribute], str):
|
||||||
|
field_data.append(f'{attribute}="{self[attribute]}"')
|
||||||
|
continue
|
||||||
|
if isinstance(self[attribute], bool):
|
||||||
|
field_data.append(f"{attribute}={str(self[attribute]).lower()}")
|
||||||
|
continue
|
||||||
|
if isinstance(self[attribute], int):
|
||||||
|
field_data.append(f"{attribute}={self[attribute]}")
|
||||||
|
continue
|
||||||
|
if isinstance(self[attribute], float):
|
||||||
|
field_data.append(f"{attribute}={self[attribute]}")
|
||||||
|
continue
|
||||||
|
if attribute == "fault_light" and not self[attribute]:
|
||||||
|
field_data.append(f"{attribute}=false")
|
||||||
|
continue
|
||||||
|
if attribute == "errors":
|
||||||
|
for idx, item in enumerate(self[attribute]):
|
||||||
|
field_data.append(f'error_{idx+1}="{item.error_message}"')
|
||||||
|
|
||||||
|
tags_str = ",".join(tag_data)
|
||||||
|
field_str = ",".join(field_data)
|
||||||
|
timestamp = str(int(time.mktime(self.datetime.timetuple()) * 1e9))
|
||||||
|
|
||||||
|
return " ".join([tags_str, field_str, timestamp])
|
||||||
|
|||||||
29
pyasic/data/error_codes/X19.py
Normal file
29
pyasic/data/error_codes/X19.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class X19Error:
|
||||||
|
"""A Dataclass to handle error codes of X19 miners.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error_message: The error message as a string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
error_message: str
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
return asdict(self)
|
||||||
@@ -14,3 +14,5 @@
|
|||||||
|
|
||||||
from .whatsminer import WhatsminerError
|
from .whatsminer import WhatsminerError
|
||||||
from .bos import BraiinsOSError
|
from .bos import BraiinsOSError
|
||||||
|
from .X19 import X19Error
|
||||||
|
from .innosilicon import InnosiliconError
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ from dataclasses import dataclass, asdict
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BraiinsOSError:
|
class BraiinsOSError:
|
||||||
"""A Dataclass to handle error codes of BraiinsOS+ miners."""
|
"""A Dataclass to handle error codes of BraiinsOS+ miners.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error_message: The error message as a string.
|
||||||
|
"""
|
||||||
|
|
||||||
error_message: str
|
error_message: str
|
||||||
|
|
||||||
|
|||||||
65
pyasic/data/error_codes/innosilicon.py
Normal file
65
pyasic/data/error_codes/innosilicon.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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, field, asdict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InnosiliconError:
|
||||||
|
"""A Dataclass to handle error codes of Innosilicon miners.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error_code: The error code as an int.
|
||||||
|
error_message: The error message as a string. Automatically found from the error code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
error_code: int
|
||||||
|
error_message: str = field(init=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error_message(self): # noqa - Skip PyCharm inspection
|
||||||
|
if self.error_code in ERROR_CODES:
|
||||||
|
return ERROR_CODES[self.error_code]
|
||||||
|
return "Unknown error type."
|
||||||
|
|
||||||
|
@error_message.setter
|
||||||
|
def error_message(self, val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
ERROR_CODES = {
|
||||||
|
21: "The PLUG signal of the hash board is not detected.",
|
||||||
|
22: "Power I2C communication is abnormal.",
|
||||||
|
23: "The SPI of all hash boards is blocked.",
|
||||||
|
24: "Some of the hash boards fail to connect to the SPI'.",
|
||||||
|
25: "Hashboard failed to set frequency.",
|
||||||
|
26: "Hashboard failed to set voltage.",
|
||||||
|
27: "Chip BIST test failed.",
|
||||||
|
28: "Hashboard SPI communication is abnormal.",
|
||||||
|
29: "Power I2C communication is abnormal.",
|
||||||
|
30: "Pool connection failed.",
|
||||||
|
31: "Individual chips are damaged.",
|
||||||
|
32: "Over temperature protection.",
|
||||||
|
33: "Hashboard fault.",
|
||||||
|
34: "The data cables are not connected in the correct order.",
|
||||||
|
35: "No power output.",
|
||||||
|
36: "Hashboard fault.",
|
||||||
|
37: "Control board and/or hashboard do not match.",
|
||||||
|
40: "Power output is abnormal.",
|
||||||
|
41: "Power output is abnormal.",
|
||||||
|
42: "Hashboard fault.",
|
||||||
|
}
|
||||||
@@ -17,7 +17,12 @@ from dataclasses import dataclass, field, asdict
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WhatsminerError:
|
class WhatsminerError:
|
||||||
"""A Dataclass to handle error codes of Whatsminers."""
|
"""A Dataclass to handle error codes of Whatsminers.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error_code: The error code as an int.
|
||||||
|
error_message: The error message as a string. Automatically found from the error code.
|
||||||
|
"""
|
||||||
|
|
||||||
error_code: int
|
error_code: int
|
||||||
error_message: str = field(init=False)
|
error_message: str = field(init=False)
|
||||||
@@ -147,6 +152,7 @@ ERROR_CODES = {
|
|||||||
2020: "Pool 0 connection failed.",
|
2020: "Pool 0 connection failed.",
|
||||||
2021: "Pool 1 connection failed.",
|
2021: "Pool 1 connection failed.",
|
||||||
2022: "Pool 2 connection failed.",
|
2022: "Pool 2 connection failed.",
|
||||||
|
2023: "Pool 3 connection failed.",
|
||||||
2030: "High rejection rate on pool.",
|
2030: "High rejection rate on pool.",
|
||||||
2040: "The pool does not support asicboost mode.",
|
2040: "The pool does not support asicboost mode.",
|
||||||
2310: "Hashrate is too low.",
|
2310: "Hashrate is too low.",
|
||||||
|
|||||||
41
pyasic/errors/__init__.py
Normal file
41
pyasic/errors/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(Exception):
|
||||||
|
def __init__(self, *args):
|
||||||
|
if args:
|
||||||
|
self.message = args[0]
|
||||||
|
else:
|
||||||
|
self.message = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.message:
|
||||||
|
return f"{self.message}"
|
||||||
|
else:
|
||||||
|
return "Incorrect API parameters."
|
||||||
|
|
||||||
|
|
||||||
|
class APIWarning(Warning):
|
||||||
|
def __init__(self, *args):
|
||||||
|
if args:
|
||||||
|
self.message = args[0]
|
||||||
|
else:
|
||||||
|
self.message = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.message:
|
||||||
|
return f"{self.message}"
|
||||||
|
else:
|
||||||
|
return "Incorrect API parameters."
|
||||||
@@ -12,105 +12,12 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import asyncssh
|
|
||||||
import logging
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from pyasic.data import MinerData
|
from pyasic.miners.base import BaseMiner, AnyMiner
|
||||||
|
from pyasic.miners.miner_factory import MinerFactory
|
||||||
|
|
||||||
|
# abstracted version of get miner that is easier to access
|
||||||
class BaseMiner:
|
async def get_miner(ip: Union[ipaddress.ip_address, str]) -> AnyMiner:
|
||||||
def __init__(self, *args) -> None:
|
return await MinerFactory().get_miner(ip)
|
||||||
self.ip = None
|
|
||||||
self.uname = "root"
|
|
||||||
self.pwd = "admin"
|
|
||||||
self.api = None
|
|
||||||
self.api_type = None
|
|
||||||
self.model = None
|
|
||||||
self.light = None
|
|
||||||
self.hostname = None
|
|
||||||
self.nominal_chips = 1
|
|
||||||
self.version = None
|
|
||||||
self.fan_count = 2
|
|
||||||
self.config = None
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
|
|
||||||
|
|
||||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
|
||||||
"""Create a new asyncssh connection"""
|
|
||||||
try:
|
|
||||||
conn = await asyncssh.connect(
|
|
||||||
str(self.ip),
|
|
||||||
known_hosts=None,
|
|
||||||
username=self.uname,
|
|
||||||
password=self.pwd,
|
|
||||||
server_host_key_algs=["ssh-rsa"],
|
|
||||||
)
|
|
||||||
return conn
|
|
||||||
except asyncssh.misc.PermissionDenied:
|
|
||||||
try:
|
|
||||||
conn = await asyncssh.connect(
|
|
||||||
str(self.ip),
|
|
||||||
known_hosts=None,
|
|
||||||
username="root",
|
|
||||||
password="admin",
|
|
||||||
server_host_key_algs=["ssh-rsa"],
|
|
||||||
)
|
|
||||||
return conn
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
except OSError as e:
|
|
||||||
logging.warning(f"Connection refused: {self}")
|
|
||||||
raise e
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
async def fault_light_on(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def fault_light_off(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def send_file(self, src, dest):
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
await asyncssh.scp(src, (conn, dest))
|
|
||||||
|
|
||||||
async def check_light(self):
|
|
||||||
return self.light
|
|
||||||
|
|
||||||
async def get_board_info(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_hostname(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_model(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def restart_backend(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def send_config(self, *args, **kwargs):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_mac(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_data(self) -> MinerData:
|
|
||||||
return MinerData(ip=str(self.ip))
|
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ from typing import Union
|
|||||||
|
|
||||||
|
|
||||||
from pyasic.API.bmminer import BMMinerAPI
|
from pyasic.API.bmminer import BMMinerAPI
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
from pyasic.data import MinerData
|
from pyasic.data import MinerData
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
|
||||||
from pyasic.settings import PyasicSettings
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
@@ -154,6 +155,29 @@ class BMMiner(BaseMiner):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def check_light(self) -> bool:
|
||||||
|
if not self.light:
|
||||||
|
self.light = False
|
||||||
|
return self.light
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_errors(self) -> list:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_mac(self) -> str:
|
||||||
|
return "00:00:00:00:00:00"
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
async def get_data(self) -> MinerData:
|
async def get_data(self) -> MinerData:
|
||||||
"""Get data from the miner.
|
"""Get data from the miner.
|
||||||
|
|
||||||
@@ -168,6 +192,7 @@ class BMMiner(BaseMiner):
|
|||||||
model = await self.get_model()
|
model = await self.get_model()
|
||||||
hostname = await self.get_hostname()
|
hostname = await self.get_hostname()
|
||||||
mac = await self.get_mac()
|
mac = await self.get_mac()
|
||||||
|
errors = await self.get_errors()
|
||||||
|
|
||||||
if model:
|
if model:
|
||||||
data.model = model
|
data.model = model
|
||||||
@@ -178,6 +203,10 @@ class BMMiner(BaseMiner):
|
|||||||
if mac:
|
if mac:
|
||||||
data.mac = mac
|
data.mac = mac
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for error in errors:
|
||||||
|
data.errors.append(error)
|
||||||
|
|
||||||
data.fault_light = await self.check_light()
|
data.fault_light = await self.check_light()
|
||||||
|
|
||||||
miner_data = None
|
miner_data = None
|
||||||
@@ -257,7 +286,8 @@ class BMMiner(BaseMiner):
|
|||||||
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
|
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
|
||||||
if not env_temp == 0:
|
if not env_temp == 0:
|
||||||
env_temp_list.append(int(env_temp))
|
env_temp_list.append(int(env_temp))
|
||||||
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
if not env_temp_list == []:
|
||||||
|
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
||||||
|
|
||||||
if pools:
|
if pools:
|
||||||
pool_1 = None
|
pool_1 = None
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ from typing import Union
|
|||||||
import toml
|
import toml
|
||||||
|
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
from pyasic.API.bosminer import BOSMinerAPI
|
from pyasic.API.bosminer import BOSMinerAPI
|
||||||
from pyasic.API import APIError
|
from pyasic.errors import APIError
|
||||||
|
|
||||||
from pyasic.data.error_codes import BraiinsOSError
|
from pyasic.data.error_codes import BraiinsOSError
|
||||||
from pyasic.data import MinerData
|
from pyasic.data import MinerData
|
||||||
@@ -72,11 +72,11 @@ class BOSMiner(BaseMiner):
|
|||||||
async def fault_light_on(self) -> bool:
|
async def fault_light_on(self) -> bool:
|
||||||
"""Sends command to turn on fault light on the miner."""
|
"""Sends command to turn on fault light on the miner."""
|
||||||
logging.debug(f"{self}: Sending fault_light on command.")
|
logging.debug(f"{self}: Sending fault_light on command.")
|
||||||
self.light = True
|
|
||||||
_ret = await self.send_ssh_command("miner fault_light on")
|
_ret = await self.send_ssh_command("miner fault_light on")
|
||||||
logging.debug(f"{self}: fault_light on command completed.")
|
logging.debug(f"{self}: fault_light on command completed.")
|
||||||
if isinstance(_ret, str):
|
if isinstance(_ret, str):
|
||||||
return True
|
self.light = True
|
||||||
|
return self.light
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def fault_light_off(self) -> bool:
|
async def fault_light_off(self) -> bool:
|
||||||
@@ -86,6 +86,7 @@ class BOSMiner(BaseMiner):
|
|||||||
_ret = await self.send_ssh_command("miner fault_light off")
|
_ret = await self.send_ssh_command("miner fault_light off")
|
||||||
logging.debug(f"{self}: fault_light off command completed.")
|
logging.debug(f"{self}: fault_light off command completed.")
|
||||||
if isinstance(_ret, str):
|
if isinstance(_ret, str):
|
||||||
|
self.light = False
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -214,22 +215,12 @@ class BOSMiner(BaseMiner):
|
|||||||
logging.warning(f"Failed to get model for miner: {self}")
|
logging.warning(f"Failed to get model for miner: {self}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
"""Configures miner with yaml config."""
|
"""Configures miner with yaml config."""
|
||||||
logging.debug(f"{self}: Sending config.")
|
logging.debug(f"{self}: Sending config.")
|
||||||
if ip_user:
|
toml_conf = config.as_bos(
|
||||||
suffix = str(self.ip).split(".")[-1]
|
model=self.model.replace(" (BOS)", ""), user_suffix=user_suffix
|
||||||
toml_conf = (
|
)
|
||||||
MinerConfig()
|
|
||||||
.from_yaml(yaml_config)
|
|
||||||
.as_bos(model=self.model.replace(" (BOS)", ""), user_suffix=suffix)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
toml_conf = (
|
|
||||||
MinerConfig()
|
|
||||||
.from_yaml(yaml_config)
|
|
||||||
.as_bos(model=self.model.replace(" (BOS)", ""))
|
|
||||||
)
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
await conn.run("/etc/init.d/bosminer stop")
|
await conn.run("/etc/init.d/bosminer stop")
|
||||||
logging.debug(f"{self}: Opening SFTP connection.")
|
logging.debug(f"{self}: Opening SFTP connection.")
|
||||||
@@ -240,6 +231,55 @@ class BOSMiner(BaseMiner):
|
|||||||
logging.debug(f"{self}: Restarting BOSMiner")
|
logging.debug(f"{self}: Restarting BOSMiner")
|
||||||
await conn.run("/etc/init.d/bosminer start")
|
await conn.run("/etc/init.d/bosminer start")
|
||||||
|
|
||||||
|
async def check_light(self) -> bool:
|
||||||
|
if self.light:
|
||||||
|
return self.light
|
||||||
|
data = (
|
||||||
|
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
|
||||||
|
).strip()
|
||||||
|
self.light = False
|
||||||
|
if data == "50":
|
||||||
|
self.light = True
|
||||||
|
return self.light
|
||||||
|
|
||||||
|
async def get_errors(self) -> list:
|
||||||
|
tunerstatus = None
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
tunerstatus = await self.api.tunerstatus()
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(e)
|
||||||
|
|
||||||
|
if tunerstatus:
|
||||||
|
tuner = tunerstatus[0].get("TUNERSTATUS")
|
||||||
|
if tuner:
|
||||||
|
if len(tuner) > 0:
|
||||||
|
chain_status = tuner[0].get("TunerChainStatus")
|
||||||
|
if chain_status and len(chain_status) > 0:
|
||||||
|
board_map = {
|
||||||
|
0: "Left board",
|
||||||
|
1: "Center board",
|
||||||
|
2: "Right board",
|
||||||
|
}
|
||||||
|
offset = (
|
||||||
|
6
|
||||||
|
if chain_status[0]["HashchainIndex"] in [6, 7, 8]
|
||||||
|
else chain_status[0]["HashchainIndex"]
|
||||||
|
)
|
||||||
|
for board in chain_status:
|
||||||
|
_id = board["HashchainIndex"] - offset
|
||||||
|
if board["Status"] not in [
|
||||||
|
"Stable",
|
||||||
|
"Testing performance profile",
|
||||||
|
]:
|
||||||
|
_error = board["Status"]
|
||||||
|
_error = _error[0].lower() + _error[1:]
|
||||||
|
errors.append(
|
||||||
|
BraiinsOSError(f"{board_map[_id]} {_error}")
|
||||||
|
)
|
||||||
|
return errors
|
||||||
|
|
||||||
async def get_data(self) -> MinerData:
|
async def get_data(self) -> MinerData:
|
||||||
"""Get data from the miner.
|
"""Get data from the miner.
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from pyasic.API.bosminer import BOSMinerAPI
|
from pyasic.API.bosminer import BOSMinerAPI
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
|
||||||
|
|
||||||
class BOSMinerOld(BaseMiner):
|
class BOSMinerOld(BaseMiner):
|
||||||
@@ -29,7 +32,7 @@ class BOSMinerOld(BaseMiner):
|
|||||||
self.uname = "root"
|
self.uname = "root"
|
||||||
self.pwd = "admin"
|
self.pwd = "admin"
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd: str) -> str or None:
|
async def send_ssh_command(self, cmd: str) -> Union[str, None]:
|
||||||
"""Send a command to the miner over ssh.
|
"""Send a command to the miner over ssh.
|
||||||
|
|
||||||
:return: Result of the command or None.
|
:return: Result of the command or None.
|
||||||
@@ -61,3 +64,39 @@ class BOSMinerOld(BaseMiner):
|
|||||||
async def update_to_plus(self):
|
async def update_to_plus(self):
|
||||||
result = await self.send_ssh_command("opkg update && opkg install bos_plus")
|
result = await self.send_ssh_command("opkg update && opkg install bos_plus")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def check_light(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_config(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_errors(self) -> list:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_hostname(self) -> str:
|
||||||
|
return "?"
|
||||||
|
|
||||||
|
async def get_mac(self) -> str:
|
||||||
|
return "00:00:00:00:00:00"
|
||||||
|
|
||||||
|
async def get_model(self) -> str:
|
||||||
|
return "S9"
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_data(self) -> MinerData:
|
||||||
|
return MinerData(ip=str(self.ip))
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ from typing import Union
|
|||||||
|
|
||||||
|
|
||||||
from pyasic.API.btminer import BTMinerAPI
|
from pyasic.API.btminer import BTMinerAPI
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
from pyasic.API import APIError
|
from pyasic.errors import APIError
|
||||||
|
|
||||||
from pyasic.data import MinerData
|
from pyasic.data import MinerData
|
||||||
from pyasic.data.error_codes import WhatsminerError
|
from pyasic.data.error_codes import WhatsminerError
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
|
||||||
from pyasic.settings import PyasicSettings
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
@@ -99,6 +100,100 @@ class BTMiner(BaseMiner):
|
|||||||
|
|
||||||
return str(mac).upper()
|
return str(mac).upper()
|
||||||
|
|
||||||
|
async def _reset_api_pwd_to_admin(self, pwd: str):
|
||||||
|
try:
|
||||||
|
data = await self.api.update_pwd(pwd, "admin")
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data:
|
||||||
|
if "Code" in data.keys():
|
||||||
|
if data["Code"] == 131:
|
||||||
|
return True
|
||||||
|
print(data)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def check_light(self) -> bool:
|
||||||
|
data = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await self.api.get_miner_info()
|
||||||
|
except APIError:
|
||||||
|
if not self.light:
|
||||||
|
self.light = False
|
||||||
|
if data:
|
||||||
|
if "Msg" in data.keys():
|
||||||
|
if "ledstat" in data["Msg"].keys():
|
||||||
|
if not data["Msg"]["ledstat"] == "auto":
|
||||||
|
self.light = True
|
||||||
|
if data["Msg"]["ledstat"] == "auto":
|
||||||
|
self.light = False
|
||||||
|
return self.light
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.set_led(auto=True)
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data:
|
||||||
|
if "Code" in data.keys():
|
||||||
|
if data["Code"] == 131:
|
||||||
|
self.light = False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.api.set_led(auto=False)
|
||||||
|
await self.api.set_led(
|
||||||
|
auto=False, color="green", start=0, period=1, duration=0
|
||||||
|
)
|
||||||
|
except APIError:
|
||||||
|
return False
|
||||||
|
if data:
|
||||||
|
if "Code" in data.keys():
|
||||||
|
if data["Code"] == 131:
|
||||||
|
self.light = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_errors(self) -> list:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
conf = config.as_wm(user_suffix=user_suffix)
|
||||||
|
|
||||||
|
await self.api.update_pools(
|
||||||
|
conf[0]["url"],
|
||||||
|
conf[0]["user"],
|
||||||
|
conf[0]["pass"],
|
||||||
|
conf[1]["url"],
|
||||||
|
conf[1]["user"],
|
||||||
|
conf[1]["pass"],
|
||||||
|
conf[2]["url"],
|
||||||
|
conf[2]["user"],
|
||||||
|
conf[2]["pass"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_config(self) -> MinerConfig:
|
||||||
|
pools = None
|
||||||
|
cfg = MinerConfig()
|
||||||
|
|
||||||
|
try:
|
||||||
|
pools = await self.api.pools()
|
||||||
|
except APIError as e:
|
||||||
|
logging.warning(e)
|
||||||
|
|
||||||
|
if pools:
|
||||||
|
if "POOLS" in pools.keys():
|
||||||
|
cfg = cfg.from_api(pools["POOLS"])
|
||||||
|
return cfg
|
||||||
|
|
||||||
async def get_data(self) -> MinerData:
|
async def get_data(self) -> MinerData:
|
||||||
"""Get data from the miner.
|
"""Get data from the miner.
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ from typing import Union
|
|||||||
|
|
||||||
|
|
||||||
from pyasic.API.cgminer import CGMinerAPI
|
from pyasic.API.cgminer import CGMinerAPI
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
from pyasic.API import APIError
|
from pyasic.errors import APIError
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
|
||||||
from pyasic.data import MinerData
|
from pyasic.data import MinerData
|
||||||
|
|
||||||
@@ -151,6 +152,26 @@ class CGMiner(BaseMiner):
|
|||||||
self.config = result.stdout
|
self.config = result.stdout
|
||||||
return self.config
|
return self.config
|
||||||
|
|
||||||
|
async def check_light(self) -> bool:
|
||||||
|
if not self.light:
|
||||||
|
self.light = False
|
||||||
|
return self.light
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_errors(self) -> list:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_mac(self) -> str:
|
||||||
|
return "00:00:00:00:00:00"
|
||||||
|
|
||||||
async def get_data(self) -> MinerData:
|
async def get_data(self) -> MinerData:
|
||||||
"""Get data from the miner.
|
"""Get data from the miner.
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,4 @@
|
|||||||
from .antminer import *
|
from .antminer import *
|
||||||
from .avalonminer import *
|
from .avalonminer import *
|
||||||
from .whatsminer import *
|
from .whatsminer import *
|
||||||
|
from .innosilicon import *
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S17(BaseMiner):
|
class S17(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S17Plus(BaseMiner):
|
class S17Plus(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S17Pro(BaseMiner):
|
class S17Pro(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S17e(BaseMiner):
|
class S17e(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class T17(BaseMiner):
|
class T17(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class T17Plus(BaseMiner):
|
class T17Plus(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class T17e(BaseMiner):
|
class T17e(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S19(BaseMiner):
|
class S19(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S19Pro(BaseMiner):
|
class S19Pro(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S19a(BaseMiner):
|
class S19a(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S19j(BaseMiner):
|
class S19j(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S19jPro(BaseMiner):
|
class S19jPro(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class T19(BaseMiner):
|
class T19(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S9(BaseMiner):
|
class S9(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class S9i(BaseMiner):
|
class S9i(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class T9(BaseMiner):
|
class T9(BaseMiner):
|
||||||
@@ -20,5 +20,5 @@ class T9(BaseMiner):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "T9"
|
self.model = "T9"
|
||||||
self.nominal_chips = 57
|
self.nominal_chips = 54
|
||||||
self.fan_count = 2
|
self.fan_count = 2
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon1026(BaseMiner):
|
class Avalon1026(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon1047(BaseMiner):
|
class Avalon1047(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon1066(BaseMiner):
|
class Avalon1066(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon721(BaseMiner):
|
class Avalon721(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon741(BaseMiner):
|
class Avalon741(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon761(BaseMiner):
|
class Avalon761(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon821(BaseMiner):
|
class Avalon821(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon841(BaseMiner):
|
class Avalon841(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon851(BaseMiner):
|
class Avalon851(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class Avalon921(BaseMiner):
|
class Avalon921(BaseMiner):
|
||||||
|
|||||||
24
pyasic/miners/_types/innosilicon/T3X/T3H_Plus.py
Normal file
24
pyasic/miners/_types/innosilicon/T3X/T3H_Plus.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class InnosiliconT3HPlus(BaseMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "T3H+"
|
||||||
|
self.nominal_chips = 114
|
||||||
|
self.fan_count = 4
|
||||||
@@ -12,7 +12,4 @@
|
|||||||
# 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 unittest
|
from .T3H_Plus import InnosiliconT3HPlus
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
15
pyasic/miners/_types/innosilicon/__init__.py
Normal file
15
pyasic/miners/_types/innosilicon/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from .T3X import *
|
||||||
33
pyasic/miners/_types/whatsminer/M2X/M20.py
Normal file
33
pyasic/miners/_types/whatsminer/M2X/M20.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class M20(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "M20"
|
||||||
|
self.nominal_chips = 70
|
||||||
|
self.fan_count = 2
|
||||||
|
|
||||||
|
|
||||||
|
class M20V10(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "M20 V10"
|
||||||
|
self.nominal_chips = 70
|
||||||
|
self.fan_count = 2
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M20S(BaseMiner):
|
class M20S(BaseMiner):
|
||||||
@@ -28,7 +28,7 @@ class M20SV10(BaseMiner):
|
|||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "M20S"
|
self.model = "M20S V10"
|
||||||
self.nominal_chips = 105
|
self.nominal_chips = 105
|
||||||
self.fan_count = 2
|
self.fan_count = 2
|
||||||
|
|
||||||
@@ -37,6 +37,6 @@ class M20SV20(BaseMiner):
|
|||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "M20S"
|
self.model = "M20S V20"
|
||||||
self.nominal_chips = 111
|
self.nominal_chips = 111
|
||||||
self.fan_count = 2
|
self.fan_count = 2
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M20SPlus(BaseMiner):
|
class M20SPlus(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M21(BaseMiner):
|
class M21(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M21S(BaseMiner):
|
class M21S(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M21SPlus(BaseMiner):
|
class M21SPlus(BaseMiner):
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from .M20 import M20, M20V10
|
||||||
from .M20S import M20S, M20SV10, M20SV20
|
from .M20S import M20S, M20SV10, M20SV20
|
||||||
from .M20S_Plus import M20SPlus
|
from .M20S_Plus import M20SPlus
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M30S(BaseMiner):
|
class M30S(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M30SPlus(BaseMiner):
|
class M30SPlus(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M30SPlusPlus(BaseMiner):
|
class M30SPlusPlus(BaseMiner):
|
||||||
@@ -28,7 +28,7 @@ class M30SPlusPlusVG30(BaseMiner):
|
|||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "M30S++ V30"
|
self.model = "M30S++ VG30"
|
||||||
self.nominal_chips = 111
|
self.nominal_chips = 111
|
||||||
self.fan_count = 2
|
self.fan_count = 2
|
||||||
|
|
||||||
@@ -37,6 +37,15 @@ class M30SPlusPlusVG40(BaseMiner):
|
|||||||
def __init__(self, ip: str):
|
def __init__(self, ip: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "M30S++ V40"
|
self.model = "M30S++ VG40"
|
||||||
self.nominal_chips = 117
|
self.nominal_chips = 117
|
||||||
self.fan_count = 2
|
self.fan_count = 2
|
||||||
|
|
||||||
|
|
||||||
|
class M30SPlusPlusVH60(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "M30S++ VH60"
|
||||||
|
self.nominal_chips = 78
|
||||||
|
self.fan_count = 2
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M31S(BaseMiner):
|
class M31S(BaseMiner):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M31SPlus(BaseMiner):
|
class M31SPlus(BaseMiner):
|
||||||
|
|||||||
33
pyasic/miners/_types/whatsminer/M3X/M32.py
Normal file
33
pyasic/miners/_types/whatsminer/M3X/M32.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
|
class M32(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "M32"
|
||||||
|
self.nominal_chips = 74
|
||||||
|
self.fan_count = 2
|
||||||
|
|
||||||
|
|
||||||
|
class M32V20(BaseMiner):
|
||||||
|
def __init__(self, ip: str):
|
||||||
|
super().__init__()
|
||||||
|
self.ip = ip
|
||||||
|
self.model = "M32 V20"
|
||||||
|
self.nominal_chips = 74
|
||||||
|
self.fan_count = 2
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
|
||||||
|
|
||||||
class M32S(BaseMiner):
|
class M32S(BaseMiner):
|
||||||
|
|||||||
@@ -14,9 +14,15 @@
|
|||||||
|
|
||||||
from .M30S import M30S, M30SVE10, M30SVE20, M30SVG20, M30SV50
|
from .M30S import M30S, M30SVE10, M30SVE20, M30SVG20, M30SV50
|
||||||
from .M30S_Plus import M30SPlus, M30SPlusVG60, M30SPlusVE40, M30SPlusVF20
|
from .M30S_Plus import M30SPlus, M30SPlusVG60, M30SPlusVE40, M30SPlusVF20
|
||||||
from .M30S_Plus_Plus import M30SPlusPlus, M30SPlusPlusVG30, M30SPlusPlusVG40
|
from .M30S_Plus_Plus import (
|
||||||
|
M30SPlusPlus,
|
||||||
|
M30SPlusPlusVG30,
|
||||||
|
M30SPlusPlusVG40,
|
||||||
|
M30SPlusPlusVH60,
|
||||||
|
)
|
||||||
|
|
||||||
from .M31S import M31S
|
from .M31S import M31S
|
||||||
from .M31S_Plus import M31SPlus, M31SPlusVE20
|
from .M31S_Plus import M31SPlus, M31SPlusVE20
|
||||||
|
|
||||||
|
from .M32 import M32, M32V20
|
||||||
from .M32S import M32S
|
from .M32S import M32S
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
|
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
|
||||||
|
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
@@ -22,11 +24,13 @@ class BMMinerX17(BMMiner):
|
|||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
super().__init__(ip)
|
super().__init__(ip)
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
self.uname = "root"
|
||||||
|
self.pwd = PyasicSettings().global_x17_password
|
||||||
|
|
||||||
async def get_hostname(self) -> Union[str, None]:
|
async def get_hostname(self) -> Union[str, None]:
|
||||||
hostname = None
|
hostname = None
|
||||||
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
|
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.get(url, auth=auth)
|
data = await client.get(url, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
@@ -36,10 +40,10 @@ class BMMinerX17(BMMiner):
|
|||||||
hostname = data["hostname"]
|
hostname = data["hostname"]
|
||||||
return hostname
|
return hostname
|
||||||
|
|
||||||
async def get_mac(self):
|
async def get_mac(self) -> Union[str, None]:
|
||||||
mac = None
|
mac = None
|
||||||
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
|
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.get(url, auth=auth)
|
data = await client.get(url, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
@@ -51,7 +55,7 @@ class BMMinerX17(BMMiner):
|
|||||||
|
|
||||||
async def fault_light_on(self) -> bool:
|
async def fault_light_on(self) -> bool:
|
||||||
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
await client.post(url, data={"action": "startBlink"}, auth=auth)
|
await client.post(url, data={"action": "startBlink"}, auth=auth)
|
||||||
@@ -62,35 +66,43 @@ class BMMinerX17(BMMiner):
|
|||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
data = data.json()
|
data = data.json()
|
||||||
if data["isBlinking"]:
|
if data["isBlinking"]:
|
||||||
|
self.light = True
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def fault_light_off(self) -> bool:
|
async def fault_light_off(self) -> bool:
|
||||||
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
await client.post(url, data={"action": "stopBlink"}, auth=auth)
|
await client.post(url, data={"action": "stopBlink"}, auth=auth)
|
||||||
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
|
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
data = data.json()
|
data = data.json()
|
||||||
if not data["isBlinking"]:
|
if not data["isBlinking"]:
|
||||||
|
self.light = False
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def check_light(self):
|
async def check_light(self) -> Union[bool, None]:
|
||||||
|
if self.light:
|
||||||
|
return self.light
|
||||||
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
|
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
data = data.json()
|
data = data.json()
|
||||||
if data["isBlinking"]:
|
if data["isBlinking"]:
|
||||||
|
self.light = True
|
||||||
return True
|
return True
|
||||||
return False
|
else:
|
||||||
|
self.light = False
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
async def reboot(self) -> bool:
|
async def reboot(self) -> bool:
|
||||||
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
|
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.get(url, auth=auth)
|
data = await client.get(url, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
|
|||||||
@@ -15,33 +15,39 @@
|
|||||||
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
|
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
|
||||||
|
|
||||||
from pyasic.config import MinerConfig
|
from pyasic.config import MinerConfig
|
||||||
|
from pyasic.data.error_codes import X19Error
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from typing import Union, List
|
||||||
|
|
||||||
|
|
||||||
class BMMinerX19(BMMiner):
|
class BMMinerX19(BMMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
super().__init__(ip)
|
super().__init__(ip)
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
self.uname = "root"
|
||||||
|
self.pwd = PyasicSettings().global_x19_password
|
||||||
|
|
||||||
async def check_light(self) -> bool:
|
async def check_light(self) -> Union[bool, None]:
|
||||||
if self.light:
|
if self.light:
|
||||||
return self.light
|
return self.light
|
||||||
url = f"http://{self.ip}/cgi-bin/get_blink_status.cgi"
|
url = f"http://{self.ip}/cgi-bin/get_blink_status.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.get(url, auth=auth)
|
data = await client.get(url, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
data = data.json()
|
data = data.json()
|
||||||
light = data["blink"]
|
light = data["blink"]
|
||||||
|
self.light = light
|
||||||
return light
|
return light
|
||||||
return False
|
return None
|
||||||
|
|
||||||
async def get_config(self) -> MinerConfig:
|
async def get_config(self) -> MinerConfig:
|
||||||
url = f"http://{self.ip}/cgi-bin/get_miner_conf.cgi"
|
url = f"http://{self.ip}/cgi-bin/get_miner_conf.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.get(url, auth=auth)
|
data = await client.get(url, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
@@ -49,14 +55,10 @@ class BMMinerX19(BMMiner):
|
|||||||
self.config = MinerConfig().from_raw(data)
|
self.config = MinerConfig().from_raw(data)
|
||||||
return self.config
|
return self.config
|
||||||
|
|
||||||
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
|
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
if ip_user:
|
conf = config.as_x19(user_suffix=user_suffix)
|
||||||
suffix = str(self.ip).split(".")[-1]
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
|
|
||||||
else:
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_x19()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
@@ -69,10 +71,10 @@ class BMMinerX19(BMMiner):
|
|||||||
break
|
break
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
async def get_hostname(self) -> str or None:
|
async def get_hostname(self) -> Union[str, None]:
|
||||||
hostname = None
|
hostname = None
|
||||||
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
|
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.get(url, auth=auth)
|
data = await client.get(url, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
@@ -82,10 +84,10 @@ class BMMinerX19(BMMiner):
|
|||||||
hostname = data["hostname"]
|
hostname = data["hostname"]
|
||||||
return hostname
|
return hostname
|
||||||
|
|
||||||
async def get_mac(self):
|
async def get_mac(self) -> Union[str, None]:
|
||||||
mac = None
|
mac = None
|
||||||
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
|
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.get(url, auth=auth)
|
data = await client.get(url, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
@@ -97,7 +99,7 @@ class BMMinerX19(BMMiner):
|
|||||||
|
|
||||||
async def fault_light_on(self) -> bool:
|
async def fault_light_on(self) -> bool:
|
||||||
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
data = json.dumps({"blink": "true"})
|
data = json.dumps({"blink": "true"})
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.post(url, data=data, auth=auth)
|
data = await client.post(url, data=data, auth=auth)
|
||||||
@@ -110,7 +112,7 @@ class BMMinerX19(BMMiner):
|
|||||||
|
|
||||||
async def fault_light_off(self) -> bool:
|
async def fault_light_off(self) -> bool:
|
||||||
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
url = f"http://{self.ip}/cgi-bin/blink.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
data = json.dumps({"blink": "false"})
|
data = json.dumps({"blink": "false"})
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.post(url, data=data, auth=auth)
|
data = await client.post(url, data=data, auth=auth)
|
||||||
@@ -123,9 +125,24 @@ class BMMinerX19(BMMiner):
|
|||||||
|
|
||||||
async def reboot(self) -> bool:
|
async def reboot(self) -> bool:
|
||||||
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
|
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
|
||||||
auth = httpx.DigestAuth("root", "root")
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
data = await client.get(url, auth=auth)
|
data = await client.get(url, auth=auth)
|
||||||
if data.status_code == 200:
|
if data.status_code == 200:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def get_errors(self) -> List[X19Error]:
|
||||||
|
errors = []
|
||||||
|
url = f"http://{self.ip}/cgi-bin/summary.cgi"
|
||||||
|
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
data = await client.get(url, auth=auth)
|
||||||
|
if data:
|
||||||
|
data = data.json()
|
||||||
|
if "SUMMARY" in data.keys():
|
||||||
|
if "status" in data["SUMMARY"][0].keys():
|
||||||
|
for item in data["SUMMARY"][0]["status"]:
|
||||||
|
if not item["status"] == "s":
|
||||||
|
errors.append(X19Error(item["msg"]))
|
||||||
|
return errors
|
||||||
|
|||||||
@@ -13,3 +13,4 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from .S9 import CGMinerS9
|
from .S9 import CGMinerS9
|
||||||
|
from .T9 import CGMinerT9
|
||||||
|
|||||||
@@ -15,8 +15,180 @@
|
|||||||
from pyasic.miners._backends import Hiveon # noqa - Ignore access to _module
|
from pyasic.miners._backends import Hiveon # noqa - Ignore access to _module
|
||||||
from pyasic.miners._types import T9 # noqa - Ignore access to _module
|
from pyasic.miners._types import T9 # noqa - Ignore access to _module
|
||||||
|
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
|
||||||
|
|
||||||
class HiveonT9(Hiveon, T9):
|
class HiveonT9(Hiveon, T9):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
super().__init__(ip)
|
super().__init__(ip)
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
self.pwd = "admin"
|
||||||
|
|
||||||
|
async def get_mac(self):
|
||||||
|
mac = (
|
||||||
|
(await self.send_ssh_command("cat /sys/class/net/eth0/address"))
|
||||||
|
.strip()
|
||||||
|
.upper()
|
||||||
|
)
|
||||||
|
return mac
|
||||||
|
|
||||||
|
async def get_data(self) -> MinerData:
|
||||||
|
"""Get data from the miner.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
||||||
|
"""
|
||||||
|
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||||
|
|
||||||
|
board_offset = -1
|
||||||
|
fan_offset = -1
|
||||||
|
|
||||||
|
model = await self.get_model()
|
||||||
|
hostname = await self.get_hostname()
|
||||||
|
mac = await self.get_mac()
|
||||||
|
errors = await self.get_errors()
|
||||||
|
|
||||||
|
if model:
|
||||||
|
data.model = model
|
||||||
|
|
||||||
|
if hostname:
|
||||||
|
data.hostname = hostname
|
||||||
|
|
||||||
|
if mac:
|
||||||
|
data.mac = mac
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for error in errors:
|
||||||
|
data.errors.append(error)
|
||||||
|
|
||||||
|
data.fault_light = await self.check_light()
|
||||||
|
|
||||||
|
miner_data = None
|
||||||
|
for i in range(PyasicSettings().miner_get_data_retries):
|
||||||
|
miner_data = await self.api.multicommand(
|
||||||
|
"summary", "pools", "stats", ignore_x19_error=True
|
||||||
|
)
|
||||||
|
if miner_data:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not miner_data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
summary = miner_data.get("summary")[0]
|
||||||
|
pools = miner_data.get("pools")[0]
|
||||||
|
stats = miner_data.get("stats")[0]
|
||||||
|
|
||||||
|
if summary:
|
||||||
|
hr = summary.get("SUMMARY")
|
||||||
|
if hr:
|
||||||
|
if len(hr) > 0:
|
||||||
|
hr = hr[0].get("GHS 1m")
|
||||||
|
if hr:
|
||||||
|
data.hashrate = round(hr / 1000, 2)
|
||||||
|
|
||||||
|
if stats:
|
||||||
|
boards = stats.get("STATS")
|
||||||
|
if boards:
|
||||||
|
if len(boards) > 0:
|
||||||
|
if "chain_power" in boards[1].keys():
|
||||||
|
data.wattage = round(
|
||||||
|
float(boards[1]["chain_power"].split(" ")[0])
|
||||||
|
)
|
||||||
|
|
||||||
|
board_map = {
|
||||||
|
"left": [2, 9, 10],
|
||||||
|
"center": [3, 11, 12],
|
||||||
|
"right": [4, 13, 14],
|
||||||
|
}
|
||||||
|
|
||||||
|
env_temp_list = []
|
||||||
|
|
||||||
|
for board in board_map.keys():
|
||||||
|
chips = 0
|
||||||
|
hashrate = 0
|
||||||
|
chip_temp = 0
|
||||||
|
board_temp = 0
|
||||||
|
for chipset in board_map[board]:
|
||||||
|
if chip_temp == 0:
|
||||||
|
if f"temp{chipset}" in boards[1].keys():
|
||||||
|
board_temp = boards[1][f"temp{chipset}"]
|
||||||
|
chip_temp = boards[1][f"temp2_{chipset}"]
|
||||||
|
if f"temp3_{chipset}" in boards[1].keys():
|
||||||
|
env_temp = boards[1][f"temp3_{chipset}"]
|
||||||
|
if not env_temp == 0:
|
||||||
|
env_temp_list.append(int(env_temp))
|
||||||
|
|
||||||
|
hashrate += boards[1][f"chain_rate{chipset}"]
|
||||||
|
chips += boards[1][f"chain_acn{chipset}"]
|
||||||
|
setattr(data, f"{board}_chips", chips)
|
||||||
|
setattr(data, f"{board}_board_hashrate", hashrate)
|
||||||
|
setattr(data, f"{board}_board_temp", board_temp)
|
||||||
|
setattr(data, f"{board}_board_chip_temp", chip_temp)
|
||||||
|
if not env_temp_list == []:
|
||||||
|
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
||||||
|
|
||||||
|
if stats:
|
||||||
|
temp = stats.get("STATS")
|
||||||
|
if temp:
|
||||||
|
if len(temp) > 1:
|
||||||
|
for fan_num in range(1, 8, 4):
|
||||||
|
for _f_num in range(4):
|
||||||
|
f = temp[1].get(f"fan{fan_num + _f_num}")
|
||||||
|
if f and not f == 0 and fan_offset == -1:
|
||||||
|
fan_offset = fan_num
|
||||||
|
if fan_offset == -1:
|
||||||
|
fan_offset = 1
|
||||||
|
for fan in range(self.fan_count):
|
||||||
|
setattr(
|
||||||
|
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
|
||||||
|
)
|
||||||
|
|
||||||
|
if pools:
|
||||||
|
pool_1 = None
|
||||||
|
pool_2 = None
|
||||||
|
pool_1_user = None
|
||||||
|
pool_2_user = None
|
||||||
|
pool_1_quota = 1
|
||||||
|
pool_2_quota = 1
|
||||||
|
quota = 0
|
||||||
|
for pool in pools.get("POOLS"):
|
||||||
|
if not pool.get("User") == "*":
|
||||||
|
if not pool_1_user:
|
||||||
|
pool_1_user = pool.get("User")
|
||||||
|
pool_1 = pool["URL"]
|
||||||
|
pool_1_quota = pool["Quota"]
|
||||||
|
elif not pool_2_user:
|
||||||
|
pool_2_user = pool.get("User")
|
||||||
|
pool_2 = pool["URL"]
|
||||||
|
pool_2_quota = pool["Quota"]
|
||||||
|
if not pool.get("User") == pool_1_user:
|
||||||
|
if not pool_2_user == pool.get("User"):
|
||||||
|
pool_2_user = pool.get("User")
|
||||||
|
pool_2 = pool["URL"]
|
||||||
|
pool_2_quota = pool["Quota"]
|
||||||
|
if pool_2_user and not pool_2_user == pool_1_user:
|
||||||
|
quota = f"{pool_1_quota}/{pool_2_quota}"
|
||||||
|
|
||||||
|
if pool_1:
|
||||||
|
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
|
||||||
|
"stratum2+tcp://", ""
|
||||||
|
)
|
||||||
|
data.pool_1_url = pool_1
|
||||||
|
|
||||||
|
if pool_1_user:
|
||||||
|
data.pool_1_user = pool_1_user
|
||||||
|
|
||||||
|
if pool_2:
|
||||||
|
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
|
||||||
|
"stratum2+tcp://", ""
|
||||||
|
)
|
||||||
|
data.pool_2_url = pool_2
|
||||||
|
|
||||||
|
if pool_2_user:
|
||||||
|
data.pool_2_user = pool_2_user
|
||||||
|
|
||||||
|
if quota:
|
||||||
|
data.pool_split = str(quota)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|||||||
@@ -51,15 +51,11 @@ class CGMinerA10X(CGMiner):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
"""Configures miner with yaml config."""
|
"""Configures miner with yaml config."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
logging.debug(f"{self}: Sending config.")
|
logging.debug(f"{self}: Sending config.")
|
||||||
if ip_user:
|
conf = config.as_avalon(user_suffix=user_suffix)
|
||||||
suffix = str(self.ip).split(".")[-1]
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
|
|
||||||
else:
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
|
|
||||||
data = await self.api.ascset(
|
data = await self.api.ascset(
|
||||||
0, "setpool", f"root,root,{conf}"
|
0, "setpool", f"root,root,{conf}"
|
||||||
) # this should work but doesn't
|
) # this should work but doesn't
|
||||||
|
|||||||
@@ -51,15 +51,11 @@ class CGMinerA7X(CGMiner):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
"""Configures miner with yaml config."""
|
"""Configures miner with yaml config."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
logging.debug(f"{self}: Sending config.")
|
logging.debug(f"{self}: Sending config.")
|
||||||
if ip_user:
|
conf = config.as_avalon(user_suffix=user_suffix)
|
||||||
suffix = str(self.ip).split(".")[-1]
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
|
|
||||||
else:
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
|
|
||||||
data = await self.api.ascset(
|
data = await self.api.ascset(
|
||||||
0, "setpool", f"root,root,{conf}"
|
0, "setpool", f"root,root,{conf}"
|
||||||
) # this should work but doesn't
|
) # this should work but doesn't
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from A8X import CGMinerA8X # noqa - Ignore access to _module
|
from .A8X import CGMinerA8X # noqa - Ignore access to _module
|
||||||
from pyasic.miners._types import Avalon821 # noqa - Ignore access to _module
|
from pyasic.miners._types import Avalon821 # noqa - Ignore access to _module
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from A8X import CGMinerA8X # noqa - Ignore access to _module
|
from .A8X import CGMinerA8X # noqa - Ignore access to _module
|
||||||
from pyasic.miners._types import Avalon841 # noqa - Ignore access to _module
|
from pyasic.miners._types import Avalon841 # noqa - Ignore access to _module
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from A8X import CGMinerA8X # noqa - Ignore access to _module
|
from .A8X import CGMinerA8X # noqa - Ignore access to _module
|
||||||
from pyasic.miners._types import Avalon851 # noqa - Ignore access to _module
|
from pyasic.miners._types import Avalon851 # noqa - Ignore access to _module
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,15 +51,11 @@ class CGMinerA8X(CGMiner):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
"""Configures miner with yaml config."""
|
"""Configures miner with yaml config."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
logging.debug(f"{self}: Sending config.")
|
logging.debug(f"{self}: Sending config.")
|
||||||
if ip_user:
|
conf = config.as_avalon(user_suffix=user_suffix)
|
||||||
suffix = str(self.ip).split(".")[-1]
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
|
|
||||||
else:
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
|
|
||||||
data = await self.api.ascset(
|
data = await self.api.ascset(
|
||||||
0, "setpool", f"root,root,{conf}"
|
0, "setpool", f"root,root,{conf}"
|
||||||
) # this should work but doesn't
|
) # this should work but doesn't
|
||||||
|
|||||||
@@ -52,15 +52,11 @@ class CGMinerAvalon921(CGMiner, Avalon921):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
"""Configures miner with yaml config."""
|
"""Configures miner with yaml config."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
logging.debug(f"{self}: Sending config.")
|
logging.debug(f"{self}: Sending config.")
|
||||||
if ip_user:
|
conf = config.as_avalon(user_suffix=user_suffix)
|
||||||
suffix = str(self.ip).split(".")[-1]
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
|
|
||||||
else:
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
|
|
||||||
data = await self.api.ascset(
|
data = await self.api.ascset(
|
||||||
0, "setpool", f"root,root,{conf}"
|
0, "setpool", f"root,root,{conf}"
|
||||||
) # this should work but doesn't
|
) # this should work but doesn't
|
||||||
|
|||||||
135
pyasic/miners/base.py
Normal file
135
pyasic/miners/base.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# 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 asyncssh
|
||||||
|
import logging
|
||||||
|
import ipaddress
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMiner(ABC):
|
||||||
|
def __init__(self, *args) -> None:
|
||||||
|
self.ip = None
|
||||||
|
self.uname = "root"
|
||||||
|
self.pwd = "admin"
|
||||||
|
self.api = None
|
||||||
|
self.api_type = None
|
||||||
|
self.model = None
|
||||||
|
self.light = None
|
||||||
|
self.hostname = None
|
||||||
|
self.nominal_chips = 1
|
||||||
|
self.version = None
|
||||||
|
self.fan_count = 2
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls is BaseMiner:
|
||||||
|
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
|
||||||
|
|
||||||
|
async def _get_ssh_connection(self) -> asyncssh.connect:
|
||||||
|
"""Create a new asyncssh connection"""
|
||||||
|
try:
|
||||||
|
conn = await asyncssh.connect(
|
||||||
|
str(self.ip),
|
||||||
|
known_hosts=None,
|
||||||
|
username=self.uname,
|
||||||
|
password=self.pwd,
|
||||||
|
server_host_key_algs=["ssh-rsa"],
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
except asyncssh.misc.PermissionDenied:
|
||||||
|
try:
|
||||||
|
conn = await asyncssh.connect(
|
||||||
|
str(self.ip),
|
||||||
|
known_hosts=None,
|
||||||
|
username="root",
|
||||||
|
password="admin",
|
||||||
|
server_host_key_algs=["ssh-rsa"],
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
except OSError as e:
|
||||||
|
logging.warning(f"Connection refused: {self}")
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def check_light(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_config(self) -> MinerConfig:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_hostname(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_model(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_mac(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_errors(self) -> list:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_data(self) -> MinerData:
|
||||||
|
return MinerData(ip=str(self.ip))
|
||||||
|
|
||||||
|
|
||||||
|
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
|
||||||
15
pyasic/miners/innosilicon/__init__.py
Normal file
15
pyasic/miners/innosilicon/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from .cgminer import *
|
||||||
327
pyasic/miners/innosilicon/cgminer/T3X/T3H_Plus.py
Normal file
327
pyasic/miners/innosilicon/cgminer/T3X/T3H_Plus.py
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
|
||||||
|
from pyasic.miners._types import InnosiliconT3HPlus # noqa - Ignore access to _module
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
from pyasic.data.error_codes import InnosiliconError
|
||||||
|
from pyasic.settings import PyasicSettings
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
from pyasic.errors import APIError
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import warnings
|
||||||
|
from typing import Union
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.ip = ip
|
||||||
|
self.uname = "admin"
|
||||||
|
self.pwd = "admin"
|
||||||
|
self.jwt = None
|
||||||
|
|
||||||
|
async def auth(self):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
auth = await client.post(
|
||||||
|
f"http://{self.ip}/api/auth",
|
||||||
|
data={"username": self.uname, "password": self.pwd},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
warnings.warn(f"Could not authenticate web token with miner: {self}")
|
||||||
|
else:
|
||||||
|
json_auth = auth.json()
|
||||||
|
self.jwt = json_auth.get("jwt")
|
||||||
|
return self.jwt
|
||||||
|
|
||||||
|
async def send_web_command(self, command: str, data: Union[dict, None] = None):
|
||||||
|
if not self.jwt:
|
||||||
|
await self.auth()
|
||||||
|
if not data:
|
||||||
|
data = {}
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
for i in range(PyasicSettings().miner_get_data_retries):
|
||||||
|
response = await client.post(
|
||||||
|
f"http://{self.ip}/api/{command}",
|
||||||
|
headers={"Authorization": "Bearer " + self.jwt},
|
||||||
|
timeout=5,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
json_data = response.json()
|
||||||
|
if (
|
||||||
|
not json_data.get("success")
|
||||||
|
and "token" in json_data
|
||||||
|
and json_data.get("token") == "expired"
|
||||||
|
):
|
||||||
|
# refresh the token, retry
|
||||||
|
await self.auth()
|
||||||
|
continue
|
||||||
|
if not json_data.get("success"):
|
||||||
|
if json_data.get("msg"):
|
||||||
|
raise APIError(json_data["msg"])
|
||||||
|
elif json_data.get("message"):
|
||||||
|
raise APIError(json_data["message"])
|
||||||
|
raise APIError("Innosilicon web api command failed.")
|
||||||
|
return json_data
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_config(self) -> MinerConfig:
|
||||||
|
pools = None
|
||||||
|
cfg = MinerConfig()
|
||||||
|
|
||||||
|
try:
|
||||||
|
pools = await self.api.pools()
|
||||||
|
except APIError as e:
|
||||||
|
logging.warning(e)
|
||||||
|
|
||||||
|
if pools:
|
||||||
|
if "POOLS" in pools.keys():
|
||||||
|
cfg = cfg.from_api(pools["POOLS"])
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
async def get_mac(self) -> Union[str, None]:
|
||||||
|
try:
|
||||||
|
data = await self.send_web_command("overview")
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if data.get("version"):
|
||||||
|
return data["version"].get("ethaddr").upper()
|
||||||
|
|
||||||
|
async def get_hostname(self) -> Union[str, None]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_model(self) -> Union[str, None]:
|
||||||
|
try:
|
||||||
|
data = await self.send_web_command("type")
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return data["type"]
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.send_web_command("reboot")
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return data["success"]
|
||||||
|
|
||||||
|
async def restart_cgminer(self) -> bool:
|
||||||
|
try:
|
||||||
|
data = await self.send_web_command("restartCgMiner")
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return data["success"]
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
return await self.restart_cgminer()
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
await self.send_web_command(
|
||||||
|
"updatePools", data=config.as_inno(user_suffix=user_suffix)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_errors(self) -> list:
|
||||||
|
errors = []
|
||||||
|
try:
|
||||||
|
data = await self.send_web_command("getErrorDetail")
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if "code" in data:
|
||||||
|
err = data["code"]
|
||||||
|
if isinstance(err, str):
|
||||||
|
err = int(err)
|
||||||
|
if not err == 0:
|
||||||
|
errors.append(InnosiliconError(error_code=err))
|
||||||
|
return errors
|
||||||
|
|
||||||
|
async def get_data(self) -> MinerData:
|
||||||
|
"""Get data from the miner.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
||||||
|
"""
|
||||||
|
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||||
|
|
||||||
|
board_offset = -1
|
||||||
|
fan_offset = -1
|
||||||
|
|
||||||
|
model = await self.get_model()
|
||||||
|
hostname = await self.get_hostname()
|
||||||
|
|
||||||
|
if model:
|
||||||
|
data.model = model
|
||||||
|
|
||||||
|
if hostname:
|
||||||
|
data.hostname = hostname
|
||||||
|
|
||||||
|
data.errors = await self.get_errors()
|
||||||
|
data.fault_light = await self.check_light()
|
||||||
|
|
||||||
|
miner_data = None
|
||||||
|
all_data = None
|
||||||
|
for i in range(PyasicSettings().miner_get_data_retries):
|
||||||
|
miner_data = await self.api.multicommand(
|
||||||
|
"summary", "pools", "stats", ignore_x19_error=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if miner_data:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_data = (await self.send_web_command("getAll"))["all"]
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not (miner_data or all_data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
summary = miner_data.get("summary")
|
||||||
|
pools = miner_data.get("pools")
|
||||||
|
stats = miner_data.get("stats")
|
||||||
|
|
||||||
|
if summary:
|
||||||
|
summary = summary[0]
|
||||||
|
hr = summary.get("SUMMARY")
|
||||||
|
if hr:
|
||||||
|
if len(hr) > 0:
|
||||||
|
hr = hr[0].get("MHS 1m")
|
||||||
|
if hr:
|
||||||
|
data.hashrate = round(hr / 1000000, 2)
|
||||||
|
elif all_data:
|
||||||
|
if all_data.get("total_hash"):
|
||||||
|
print(all_data["total_hash"])
|
||||||
|
hr = all_data["total_hash"].get("Hash Rate H")
|
||||||
|
if hr:
|
||||||
|
data.hashrate = round(hr / 1000000000000, 2)
|
||||||
|
|
||||||
|
if stats:
|
||||||
|
stats = stats[0]
|
||||||
|
if stats.get("STATS"):
|
||||||
|
board_map = {0: "left", 1: "center", 2: "right"}
|
||||||
|
for idx, board in enumerate(stats["STATS"]):
|
||||||
|
chips = board.get("Num active chips")
|
||||||
|
if chips:
|
||||||
|
setattr(data, f"{board_map[idx]}_chips", chips)
|
||||||
|
temp = board.get("Temp")
|
||||||
|
if temp:
|
||||||
|
setattr(data, f"{board_map[idx]}_board_chip_temp", temp)
|
||||||
|
|
||||||
|
if all_data:
|
||||||
|
if all_data.get("chain"):
|
||||||
|
board_map = {0: "left", 1: "center", 2: "right"}
|
||||||
|
for idx, board in enumerate(all_data["chain"]):
|
||||||
|
temp = board.get("Temp max")
|
||||||
|
if temp:
|
||||||
|
setattr(data, f"{board_map[idx]}_board_chip_temp", temp)
|
||||||
|
temp_board = board.get("Temp min")
|
||||||
|
if temp_board:
|
||||||
|
setattr(data, f"{board_map[idx]}_board_temp", temp_board)
|
||||||
|
hr = board.get("Hash Rate H")
|
||||||
|
if hr:
|
||||||
|
setattr(
|
||||||
|
data,
|
||||||
|
f"{board_map[idx]}_board_hashrate",
|
||||||
|
round(hr / 1000000000000, 2),
|
||||||
|
)
|
||||||
|
if all_data.get("fansSpeed"):
|
||||||
|
speed = round((all_data["fansSpeed"] * 6000) / 100)
|
||||||
|
for fan in range(self.fan_count):
|
||||||
|
setattr(data, f"fan_{fan+1}", speed)
|
||||||
|
if all_data.get("mac"):
|
||||||
|
data.mac = all_data["mac"].upper()
|
||||||
|
else:
|
||||||
|
mac = await self.get_mac()
|
||||||
|
if mac:
|
||||||
|
data.mac = mac
|
||||||
|
if all_data.get("power"):
|
||||||
|
data.wattage = all_data["power"]
|
||||||
|
|
||||||
|
if pools or all_data.get("pools_config"):
|
||||||
|
pool_1 = None
|
||||||
|
pool_2 = None
|
||||||
|
pool_1_user = None
|
||||||
|
pool_2_user = None
|
||||||
|
pool_1_quota = 1
|
||||||
|
pool_2_quota = 1
|
||||||
|
quota = 0
|
||||||
|
if pools:
|
||||||
|
pools = pools[0]
|
||||||
|
for pool in pools.get("POOLS"):
|
||||||
|
if not pool_1_user:
|
||||||
|
pool_1_user = pool.get("User")
|
||||||
|
pool_1 = pool["URL"]
|
||||||
|
if pool.get("Quota"):
|
||||||
|
pool_2_quota = pool.get("Quota")
|
||||||
|
elif not pool_2_user:
|
||||||
|
pool_2_user = pool.get("User")
|
||||||
|
pool_2 = pool["URL"]
|
||||||
|
if pool.get("Quota"):
|
||||||
|
pool_2_quota = pool.get("Quota")
|
||||||
|
if not pool.get("User") == pool_1_user:
|
||||||
|
if not pool_2_user == pool.get("User"):
|
||||||
|
pool_2_user = pool.get("User")
|
||||||
|
pool_2 = pool["URL"]
|
||||||
|
if pool.get("Quota"):
|
||||||
|
pool_2_quota = pool.get("Quota")
|
||||||
|
elif all_data.get("pools_config"):
|
||||||
|
print(all_data["pools_config"])
|
||||||
|
for pool in all_data["pools_config"]:
|
||||||
|
if not pool_1_user:
|
||||||
|
pool_1_user = pool.get("user")
|
||||||
|
pool_1 = pool["url"]
|
||||||
|
elif not pool_2_user:
|
||||||
|
pool_2_user = pool.get("user")
|
||||||
|
pool_2 = pool["url"]
|
||||||
|
if not pool.get("user") == pool_1_user:
|
||||||
|
if not pool_2_user == pool.get("user"):
|
||||||
|
pool_2_user = pool.get("user")
|
||||||
|
pool_2 = pool["url"]
|
||||||
|
|
||||||
|
if pool_2_user and not pool_2_user == pool_1_user:
|
||||||
|
quota = f"{pool_1_quota}/{pool_2_quota}"
|
||||||
|
|
||||||
|
if pool_1:
|
||||||
|
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
|
||||||
|
"stratum2+tcp://", ""
|
||||||
|
)
|
||||||
|
data.pool_1_url = pool_1
|
||||||
|
|
||||||
|
if pool_1_user:
|
||||||
|
data.pool_1_user = pool_1_user
|
||||||
|
|
||||||
|
if pool_2:
|
||||||
|
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
|
||||||
|
"stratum2+tcp://", ""
|
||||||
|
)
|
||||||
|
data.pool_2_url = pool_2
|
||||||
|
|
||||||
|
if pool_2_user:
|
||||||
|
data.pool_2_user = pool_2_user
|
||||||
|
|
||||||
|
if quota:
|
||||||
|
data.pool_split = str(quota)
|
||||||
|
|
||||||
|
return data
|
||||||
15
pyasic/miners/innosilicon/cgminer/T3X/__init__.py
Normal file
15
pyasic/miners/innosilicon/cgminer/T3X/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from .T3H_Plus import CGMinerInnosiliconT3HPlus
|
||||||
15
pyasic/miners/innosilicon/cgminer/__init__.py
Normal file
15
pyasic/miners/innosilicon/cgminer/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2022 Upstream Data Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from .T3X import *
|
||||||
@@ -12,14 +12,15 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from typing import TypeVar, Tuple, List, Union
|
from typing import Tuple, List, Union
|
||||||
from collections.abc import AsyncIterable
|
from collections.abc import AsyncIterable
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import AnyMiner
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from pyasic.miners.antminer import *
|
from pyasic.miners.antminer import *
|
||||||
from pyasic.miners.avalonminer import *
|
from pyasic.miners.avalonminer import *
|
||||||
from pyasic.miners.whatsminer import *
|
from pyasic.miners.whatsminer import *
|
||||||
|
from pyasic.miners.innosilicon import *
|
||||||
|
|
||||||
from pyasic.miners._backends.cgminer import CGMiner # noqa - Ignore _module import
|
from pyasic.miners._backends.cgminer import CGMiner # noqa - Ignore _module import
|
||||||
from pyasic.miners._backends.bmminer import BMMiner # noqa - Ignore _module import
|
from pyasic.miners._backends.bmminer import BMMiner # noqa - Ignore _module import
|
||||||
@@ -31,7 +32,9 @@ from pyasic.miners._backends.bosminer_old import ( # noqa - Ignore _module impo
|
|||||||
|
|
||||||
from pyasic.miners.unknown import UnknownMiner
|
from pyasic.miners.unknown import UnknownMiner
|
||||||
|
|
||||||
from pyasic.API import APIError
|
from pyasic.errors import APIError
|
||||||
|
|
||||||
|
from pyasic.misc import Singleton
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import ipaddress
|
import ipaddress
|
||||||
@@ -42,96 +45,105 @@ from pyasic.settings import PyasicSettings
|
|||||||
|
|
||||||
import asyncssh
|
import asyncssh
|
||||||
|
|
||||||
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
|
|
||||||
|
|
||||||
MINER_CLASSES = {
|
MINER_CLASSES = {
|
||||||
"Antminer S9": {
|
"ANTMINER S9": {
|
||||||
"Default": BOSMinerS9,
|
"Default": BOSMinerS9,
|
||||||
"BOSMiner": BOSMinerOld,
|
"BOSMiner": BOSMinerOld,
|
||||||
"BOSMiner+": BOSMinerS9,
|
"BOSMiner+": BOSMinerS9,
|
||||||
"BMMiner": BMMinerS9,
|
"BMMiner": BMMinerS9,
|
||||||
"CGMiner": CGMinerS9,
|
"CGMiner": CGMinerS9,
|
||||||
},
|
},
|
||||||
"Antminer S9i": {
|
"ANTMINER S9I": {
|
||||||
"Default": BMMinerS9i,
|
"Default": BMMinerS9i,
|
||||||
"BMMiner": BMMinerS9i,
|
"BMMiner": BMMinerS9i,
|
||||||
},
|
},
|
||||||
"Antminer S17": {
|
"ANTMINER T9": {
|
||||||
|
"Default": BMMinerT9,
|
||||||
|
"BMMiner": BMMinerT9,
|
||||||
|
"Hiveon": HiveonT9,
|
||||||
|
"CGMiner": CGMinerT9,
|
||||||
|
},
|
||||||
|
"ANTMINER S17": {
|
||||||
"Default": BMMinerS17,
|
"Default": BMMinerS17,
|
||||||
"BOSMiner+": BOSMinerS17,
|
"BOSMiner+": BOSMinerS17,
|
||||||
"BMMiner": BMMinerS17,
|
"BMMiner": BMMinerS17,
|
||||||
"CGMiner": CGMinerS17,
|
"CGMiner": CGMinerS17,
|
||||||
},
|
},
|
||||||
"Antminer S17+": {
|
"ANTMINER S17+": {
|
||||||
"Default": BMMinerS17Plus,
|
"Default": BMMinerS17Plus,
|
||||||
"BOSMiner+": BOSMinerS17Plus,
|
"BOSMiner+": BOSMinerS17Plus,
|
||||||
"BMMiner": BMMinerS17Plus,
|
"BMMiner": BMMinerS17Plus,
|
||||||
"CGMiner": CGMinerS17Plus,
|
"CGMiner": CGMinerS17Plus,
|
||||||
},
|
},
|
||||||
"Antminer S17 Pro": {
|
"ANTMINER S17 PRO": {
|
||||||
"Default": BMMinerS17Pro,
|
"Default": BMMinerS17Pro,
|
||||||
"BOSMiner+": BOSMinerS17Pro,
|
"BOSMiner+": BOSMinerS17Pro,
|
||||||
"BMMiner": BMMinerS17Pro,
|
"BMMiner": BMMinerS17Pro,
|
||||||
"CGMiner": CGMinerS17Pro,
|
"CGMiner": CGMinerS17Pro,
|
||||||
},
|
},
|
||||||
"Antminer S17e": {
|
"ANTMINER S17E": {
|
||||||
"Default": BMMinerS17e,
|
"Default": BMMinerS17e,
|
||||||
"BOSMiner+": BOSMinerS17e,
|
"BOSMiner+": BOSMinerS17e,
|
||||||
"BMMiner": BMMinerS17e,
|
"BMMiner": BMMinerS17e,
|
||||||
"CGMiner": CGMinerS17e,
|
"CGMiner": CGMinerS17e,
|
||||||
},
|
},
|
||||||
"Antminer T17": {
|
"ANTMINER T17": {
|
||||||
"Default": BMMinerT17,
|
"Default": BMMinerT17,
|
||||||
"BOSMiner+": BOSMinerT17,
|
"BOSMiner+": BOSMinerT17,
|
||||||
"BMMiner": BMMinerT17,
|
"BMMiner": BMMinerT17,
|
||||||
"CGMiner": CGMinerT17,
|
"CGMiner": CGMinerT17,
|
||||||
},
|
},
|
||||||
"Antminer T17+": {
|
"ANTMINER T17+": {
|
||||||
"Default": BMMinerT17Plus,
|
"Default": BMMinerT17Plus,
|
||||||
"BOSMiner+": BOSMinerT17Plus,
|
"BOSMiner+": BOSMinerT17Plus,
|
||||||
"BMMiner": BMMinerT17Plus,
|
"BMMiner": BMMinerT17Plus,
|
||||||
"CGMiner": CGMinerT17Plus,
|
"CGMiner": CGMinerT17Plus,
|
||||||
},
|
},
|
||||||
"Antminer T17e": {
|
"ANTMINER T17E": {
|
||||||
"Default": BMMinerT17e,
|
"Default": BMMinerT17e,
|
||||||
"BOSMiner+": BOSMinerT17e,
|
"BOSMiner+": BOSMinerT17e,
|
||||||
"BMMiner": BMMinerT17e,
|
"BMMiner": BMMinerT17e,
|
||||||
"CGMiner": CGMinerT17e,
|
"CGMiner": CGMinerT17e,
|
||||||
},
|
},
|
||||||
"Antminer S19": {
|
"ANTMINER S19": {
|
||||||
"Default": BMMinerS19,
|
"Default": BMMinerS19,
|
||||||
"BOSMiner+": BOSMinerS19,
|
"BOSMiner+": BOSMinerS19,
|
||||||
"BMMiner": BMMinerS19,
|
"BMMiner": BMMinerS19,
|
||||||
"CGMiner": CGMinerS19,
|
"CGMiner": CGMinerS19,
|
||||||
},
|
},
|
||||||
"Antminer S19 Pro": {
|
"ANTMINER S19 PRO": {
|
||||||
"Default": BMMinerS19Pro,
|
"Default": BMMinerS19Pro,
|
||||||
"BOSMiner+": BOSMinerS19Pro,
|
"BOSMiner+": BOSMinerS19Pro,
|
||||||
"BMMiner": BMMinerS19Pro,
|
"BMMiner": BMMinerS19Pro,
|
||||||
"CGMiner": CGMinerS19Pro,
|
"CGMiner": CGMinerS19Pro,
|
||||||
},
|
},
|
||||||
"Antminer S19j": {
|
"ANTMINER S19J": {
|
||||||
"Default": BMMinerS19j,
|
"Default": BMMinerS19j,
|
||||||
"BOSMiner+": BOSMinerS19j,
|
"BOSMiner+": BOSMinerS19j,
|
||||||
"BMMiner": BMMinerS19j,
|
"BMMiner": BMMinerS19j,
|
||||||
"CGMiner": CGMinerS19j,
|
"CGMiner": CGMinerS19j,
|
||||||
},
|
},
|
||||||
"Antminer S19j Pro": {
|
"ANTMINER S19J PRO": {
|
||||||
"Default": BMMinerS19jPro,
|
"Default": BMMinerS19jPro,
|
||||||
"BOSMiner+": BOSMinerS19jPro,
|
"BOSMiner+": BOSMinerS19jPro,
|
||||||
"BMMiner": BMMinerS19jPro,
|
"BMMiner": BMMinerS19jPro,
|
||||||
"CGMiner": CGMinerS19jPro,
|
"CGMiner": CGMinerS19jPro,
|
||||||
},
|
},
|
||||||
"Antminer S19a": {
|
"ANTMINER S19A": {
|
||||||
"Default": BMMinerS19a,
|
"Default": BMMinerS19a,
|
||||||
"BMMiner": BMMinerS19a,
|
"BMMiner": BMMinerS19a,
|
||||||
},
|
},
|
||||||
"Antminer T19": {
|
"ANTMINER T19": {
|
||||||
"Default": BMMinerT19,
|
"Default": BMMinerT19,
|
||||||
"BOSMiner+": BOSMinerT19,
|
"BOSMiner+": BOSMinerT19,
|
||||||
"BMMiner": BMMinerT19,
|
"BMMiner": BMMinerT19,
|
||||||
"CGMiner": CGMinerT19,
|
"CGMiner": CGMinerT19,
|
||||||
},
|
},
|
||||||
|
"M20": {
|
||||||
|
"Default": BTMinerM20,
|
||||||
|
"BTMiner": BTMinerM20,
|
||||||
|
"10": BTMinerM20V10,
|
||||||
|
},
|
||||||
"M20S": {
|
"M20S": {
|
||||||
"Default": BTMinerM20S,
|
"Default": BTMinerM20S,
|
||||||
"BTMiner": BTMinerM20S,
|
"BTMiner": BTMinerM20S,
|
||||||
@@ -190,58 +202,59 @@ MINER_CLASSES = {
|
|||||||
"Default": BTMinerM32S,
|
"Default": BTMinerM32S,
|
||||||
"BTMiner": BTMinerM32S,
|
"BTMiner": BTMinerM32S,
|
||||||
},
|
},
|
||||||
"AvalonMiner 721": {
|
"M32": {
|
||||||
|
"Default": BTMinerM32,
|
||||||
|
"BTMiner": BTMinerM32,
|
||||||
|
"20": BTMinerM32V20,
|
||||||
|
},
|
||||||
|
"AVALONMINER 721": {
|
||||||
"Default": CGMinerAvalon721,
|
"Default": CGMinerAvalon721,
|
||||||
"CGMiner": CGMinerAvalon721,
|
"CGMiner": CGMinerAvalon721,
|
||||||
},
|
},
|
||||||
"AvalonMiner 741": {
|
"AVALONMINER 741": {
|
||||||
"Default": CGMinerAvalon741,
|
"Default": CGMinerAvalon741,
|
||||||
"CGMiner": CGMinerAvalon741,
|
"CGMiner": CGMinerAvalon741,
|
||||||
},
|
},
|
||||||
"AvalonMiner 761": {
|
"AVALONMINER 761": {
|
||||||
"Default": CGMinerAvalon761,
|
"Default": CGMinerAvalon761,
|
||||||
"CGMiner": CGMinerAvalon761,
|
"CGMiner": CGMinerAvalon761,
|
||||||
},
|
},
|
||||||
"AvalonMiner 821": {
|
"AVALONMINER 821": {
|
||||||
"Default": CGMinerAvalon821,
|
"Default": CGMinerAvalon821,
|
||||||
"CGMiner": CGMinerAvalon821,
|
"CGMiner": CGMinerAvalon821,
|
||||||
},
|
},
|
||||||
"AvalonMiner 841": {
|
"AVALONMINER 841": {
|
||||||
"Default": CGMinerAvalon841,
|
"Default": CGMinerAvalon841,
|
||||||
"CGMiner": CGMinerAvalon841,
|
"CGMiner": CGMinerAvalon841,
|
||||||
},
|
},
|
||||||
"AvalonMiner 851": {
|
"AVALONMINER 851": {
|
||||||
"Default": CGMinerAvalon851,
|
"Default": CGMinerAvalon851,
|
||||||
"CGMiner": CGMinerAvalon851,
|
"CGMiner": CGMinerAvalon851,
|
||||||
},
|
},
|
||||||
"AvalonMiner 921": {
|
"AVALONMINER 921": {
|
||||||
"Default": CGMinerAvalon921,
|
"Default": CGMinerAvalon921,
|
||||||
"CGMiner": CGMinerAvalon921,
|
"CGMiner": CGMinerAvalon921,
|
||||||
},
|
},
|
||||||
"AvalonMiner 1026": {
|
"AVALONMINER 1026": {
|
||||||
"Default": CGMinerAvalon1026,
|
"Default": CGMinerAvalon1026,
|
||||||
"CGMiner": CGMinerAvalon1026,
|
"CGMiner": CGMinerAvalon1026,
|
||||||
},
|
},
|
||||||
"AvalonMiner 1047": {
|
"AVALONMINER 1047": {
|
||||||
"Default": CGMinerAvalon1047,
|
"Default": CGMinerAvalon1047,
|
||||||
"CGMiner": CGMinerAvalon1047,
|
"CGMiner": CGMinerAvalon1047,
|
||||||
},
|
},
|
||||||
"AvalonMiner 1066": {
|
"AVALONMINER 1066": {
|
||||||
"Default": CGMinerAvalon1066,
|
"Default": CGMinerAvalon1066,
|
||||||
"CGMiner": CGMinerAvalon1066,
|
"CGMiner": CGMinerAvalon1066,
|
||||||
},
|
},
|
||||||
|
"T3H+": {
|
||||||
|
"Default": CGMinerInnosiliconT3HPlus,
|
||||||
|
"CGMiner": CGMinerInnosiliconT3HPlus,
|
||||||
|
},
|
||||||
|
"Unknown": {"Default": UnknownMiner},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Singleton(type):
|
|
||||||
_instances = {}
|
|
||||||
|
|
||||||
def __call__(cls, *args, **kwargs):
|
|
||||||
if cls not in cls._instances:
|
|
||||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
|
||||||
return cls._instances[cls]
|
|
||||||
|
|
||||||
|
|
||||||
class MinerFactory(metaclass=Singleton):
|
class MinerFactory(metaclass=Singleton):
|
||||||
"""A factory to handle identification and selection of the proper class of miner"""
|
"""A factory to handle identification and selection of the proper class of miner"""
|
||||||
|
|
||||||
@@ -290,10 +303,7 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
if ip in self.miners:
|
if ip in self.miners:
|
||||||
return self.miners[ip]
|
return self.miners[ip]
|
||||||
# if everything fails, the miner is already set to unknown
|
# if everything fails, the miner is already set to unknown
|
||||||
miner = UnknownMiner(str(ip))
|
model, api, ver = None, None, None
|
||||||
api = None
|
|
||||||
model = None
|
|
||||||
ver = None
|
|
||||||
|
|
||||||
# try to get the API multiple times based on retries
|
# try to get the API multiple times based on retries
|
||||||
for i in range(PyasicSettings().miner_factory_get_version_retries):
|
for i in range(PyasicSettings().miner_factory_get_version_retries):
|
||||||
@@ -314,6 +324,24 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
break
|
break
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logging.warning(f"{ip}: Get Miner Timed Out")
|
logging.warning(f"{ip}: Get Miner Timed Out")
|
||||||
|
|
||||||
|
miner = self._select_miner_from_classes(ip, model, api, ver)
|
||||||
|
|
||||||
|
# save the miner to the cache at its IP if its not unknown
|
||||||
|
if not isinstance(miner, UnknownMiner):
|
||||||
|
self.miners[ip] = miner
|
||||||
|
|
||||||
|
# return the miner
|
||||||
|
return miner
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _select_miner_from_classes(
|
||||||
|
ip: ipaddress.ip_address,
|
||||||
|
model: Union[str, None],
|
||||||
|
api: Union[str, None],
|
||||||
|
ver: Union[str, None],
|
||||||
|
) -> AnyMiner:
|
||||||
|
miner = UnknownMiner(str(ip))
|
||||||
# make sure we have model information
|
# make sure we have model information
|
||||||
if model:
|
if model:
|
||||||
if not api:
|
if not api:
|
||||||
@@ -349,11 +377,6 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
elif "BMMiner" in api:
|
elif "BMMiner" in api:
|
||||||
miner = BMMiner(str(ip))
|
miner = BMMiner(str(ip))
|
||||||
|
|
||||||
# save the miner to the cache at its IP if its not unknown
|
|
||||||
if not isinstance(miner, UnknownMiner):
|
|
||||||
self.miners[ip] = miner
|
|
||||||
|
|
||||||
# return the miner
|
|
||||||
return miner
|
return miner
|
||||||
|
|
||||||
def clear_cached_miners(self) -> None:
|
def clear_cached_miners(self) -> None:
|
||||||
@@ -364,13 +387,139 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
async def _get_miner_type(
|
async def _get_miner_type(
|
||||||
self, ip: Union[ipaddress.ip_address, str]
|
self, ip: Union[ipaddress.ip_address, str]
|
||||||
) -> Tuple[Union[str, None], Union[str, None], Union[str, None]]:
|
) -> Tuple[Union[str, None], Union[str, None], Union[str, None]]:
|
||||||
data = None
|
model, api, ver = None, None, None
|
||||||
|
|
||||||
model = None
|
try:
|
||||||
api = None
|
devdetails, version = await self.__get_devdetails_and_version(ip)
|
||||||
ver = None
|
except APIError as e:
|
||||||
|
# catch APIError and let the factory know we cant get data
|
||||||
|
logging.warning(f"{ip}: API Command Error: {e}")
|
||||||
|
return None, None, None
|
||||||
|
except OSError or ConnectionRefusedError:
|
||||||
|
# miner refused connection on API port, we wont be able to get data this way
|
||||||
|
# try ssh
|
||||||
|
try:
|
||||||
|
_model = await self.__get_model_from_ssh(ip)
|
||||||
|
if _model:
|
||||||
|
model = _model
|
||||||
|
api = "BOSMiner+"
|
||||||
|
except asyncssh.misc.PermissionDenied:
|
||||||
|
try:
|
||||||
|
data = await self.__get_system_info_from_web(ip)
|
||||||
|
if not data.get("success"):
|
||||||
|
_model = await self.__get_dragonmint_version_from_web(ip)
|
||||||
|
if _model:
|
||||||
|
model = _model
|
||||||
|
if "minertype" in data:
|
||||||
|
model = data["minertype"].upper()
|
||||||
|
if "bmminer" in "\t".join(data):
|
||||||
|
api = "BMMiner"
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Unable to get miner - {e}")
|
||||||
|
return model, api, ver
|
||||||
|
|
||||||
devdetails = None
|
# if we have devdetails, we can get model data from there
|
||||||
|
if devdetails:
|
||||||
|
for _devdetails_key in ["Model", "Driver"]:
|
||||||
|
try:
|
||||||
|
model = devdetails["DEVDETAILS"][0][_devdetails_key].upper()
|
||||||
|
if not model == "BITMICRO":
|
||||||
|
break
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
if not model:
|
||||||
|
# braiins OS bug check just in case
|
||||||
|
if "s9" in devdetails["STATUS"][0]["Description"]:
|
||||||
|
model = "ANTMINER S9"
|
||||||
|
if "s17" in version["STATUS"][0]["Description"]:
|
||||||
|
model = "ANTMINER S17"
|
||||||
|
|
||||||
|
# if we have version we can get API type from here
|
||||||
|
if version:
|
||||||
|
if "VERSION" in version:
|
||||||
|
api_types = ["BMMiner", "CGMiner", "BTMiner"]
|
||||||
|
# check basic API types, BOSMiner needs a special check
|
||||||
|
for api_type in api_types:
|
||||||
|
if any(api_type in string for string in version["VERSION"][0]):
|
||||||
|
api = api_type
|
||||||
|
|
||||||
|
# check if there are any BOSMiner strings in any of the dict keys
|
||||||
|
if any("BOSminer" in string for string in version["VERSION"][0]):
|
||||||
|
api = "BOSMiner"
|
||||||
|
if version["VERSION"][0].get("BOSminer"):
|
||||||
|
if "plus" in version["VERSION"][0]["BOSminer"]:
|
||||||
|
api = "BOSMiner+"
|
||||||
|
if "BOSminer+" in version["VERSION"][0]:
|
||||||
|
api = "BOSMiner+"
|
||||||
|
|
||||||
|
# check for avalonminers
|
||||||
|
for _version_key in ["PROD", "MODEL"]:
|
||||||
|
try:
|
||||||
|
_data = version["VERSION"][0][_version_key].split("-")
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
model = _data[0].upper()
|
||||||
|
if _version_key == "MODEL":
|
||||||
|
model = f"AVALONMINER {_data[0]}"
|
||||||
|
if len(_data) > 1:
|
||||||
|
ver = _data[1]
|
||||||
|
|
||||||
|
if version.get("Description") and (
|
||||||
|
"whatsminer" in version.get("Description")
|
||||||
|
):
|
||||||
|
api = "BTMiner"
|
||||||
|
|
||||||
|
# if we have no model from devdetails but have version, try to get it from there
|
||||||
|
if version and not model:
|
||||||
|
try:
|
||||||
|
model = version["VERSION"][0]["Type"].upper()
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not model:
|
||||||
|
stats = await self._send_api_command(str(ip), "stats")
|
||||||
|
if stats:
|
||||||
|
try:
|
||||||
|
_model = stats["STATS"][0]["Type"].upper()
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for split_point in [" BB", " XILINX", " (VNISH"]:
|
||||||
|
if split_point in _model:
|
||||||
|
_model = _model.split(split_point)[0]
|
||||||
|
if "PRO" in _model and " PRO" not in _model:
|
||||||
|
_model = _model.replace("PRO", " PRO")
|
||||||
|
model = _model
|
||||||
|
else:
|
||||||
|
_model = await self.__get_dragonmint_version_from_web(ip)
|
||||||
|
if _model:
|
||||||
|
model = _model
|
||||||
|
|
||||||
|
if model:
|
||||||
|
if "DRAGONMINT" in model:
|
||||||
|
_model = await self.__get_dragonmint_version_from_web(ip)
|
||||||
|
if _model:
|
||||||
|
model = _model
|
||||||
|
if " HIVEON" in model:
|
||||||
|
# do hiveon check before whatsminer as HIVEON contains a V
|
||||||
|
model = model.split(" HIVEON")[0]
|
||||||
|
api = "Hiveon"
|
||||||
|
# whatsminer have a V in their version string (M20SV41), everything after it is ver
|
||||||
|
if "V" in model:
|
||||||
|
_ver = model.split("V")
|
||||||
|
if len(_ver) > 1:
|
||||||
|
ver = model.split("V")[1]
|
||||||
|
model = model.split("V")[0]
|
||||||
|
# don't need "Bitmain", just "ANTMINER XX" as model
|
||||||
|
if "BITMAIN " in model:
|
||||||
|
model = model.replace("BITMAIN ", "")
|
||||||
|
|
||||||
|
return model, api, ver
|
||||||
|
|
||||||
|
async def __get_devdetails_and_version(
|
||||||
|
self, ip
|
||||||
|
) -> Tuple[Union[dict, None], Union[dict, None]]:
|
||||||
version = None
|
version = None
|
||||||
try:
|
try:
|
||||||
# get device details and version data
|
# get device details and version data
|
||||||
@@ -382,163 +531,83 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
# copy each part of the main command to devdetails and version
|
# copy each part of the main command to devdetails and version
|
||||||
devdetails = data["devdetails"][0]
|
devdetails = data["devdetails"][0]
|
||||||
version = data["version"][0]
|
version = data["version"][0]
|
||||||
|
return devdetails, version
|
||||||
except APIError:
|
except APIError:
|
||||||
try:
|
# try devdetails and version separately (X19s mainly require this)
|
||||||
# try devdetails and version separately (X19s mainly require this)
|
# get devdetails and validate
|
||||||
# get devdetails and validate
|
devdetails = await self._send_api_command(str(ip), "devdetails")
|
||||||
devdetails = await self._send_api_command(str(ip), "devdetails")
|
validation = await self._validate_command(devdetails)
|
||||||
validation = await self._validate_command(devdetails)
|
if not validation[0]:
|
||||||
|
# if devdetails fails try version instead
|
||||||
|
devdetails = None
|
||||||
|
|
||||||
|
# get version and validate
|
||||||
|
version = await self._send_api_command(str(ip), "version")
|
||||||
|
validation = await self._validate_command(version)
|
||||||
if not validation[0]:
|
if not validation[0]:
|
||||||
# if devdetails fails try version instead
|
# finally try get_version (Whatsminers) and validate
|
||||||
devdetails = None
|
version = await self._send_api_command(str(ip), "get_version")
|
||||||
|
|
||||||
# get version and validate
|
|
||||||
version = await self._send_api_command(str(ip), "version")
|
|
||||||
validation = await self._validate_command(version)
|
validation = await self._validate_command(version)
|
||||||
|
|
||||||
|
# if this fails we raise an error to be caught below
|
||||||
if not validation[0]:
|
if not validation[0]:
|
||||||
# finally try get_version (Whatsminers) and validate
|
raise APIError(validation[1])
|
||||||
version = await self._send_api_command(str(ip), "get_version")
|
return devdetails, version
|
||||||
validation = await self._validate_command(version)
|
|
||||||
|
|
||||||
# if this fails we raise an error to be caught below
|
@staticmethod
|
||||||
if not validation[0]:
|
async def __get_model_from_ssh(ip: ipaddress.ip_address) -> Union[str, None]:
|
||||||
raise APIError(validation[1])
|
model = None
|
||||||
except APIError as e:
|
async with asyncssh.connect(
|
||||||
# catch APIError and let the factory know we cant get data
|
str(ip),
|
||||||
logging.warning(f"{ip}: API Command Error: {e}")
|
known_hosts=None,
|
||||||
return None, None, None
|
username="root",
|
||||||
except OSError or ConnectionRefusedError:
|
password="admin",
|
||||||
# miner refused connection on API port, we wont be able to get data this way
|
server_host_key_algs=["ssh-rsa"],
|
||||||
# try ssh
|
) as conn:
|
||||||
|
board_name = None
|
||||||
|
cmd = await conn.run("cat /tmp/sysinfo/board_name")
|
||||||
|
if cmd:
|
||||||
|
board_name = cmd.stdout.strip()
|
||||||
|
if board_name == "am1-s9":
|
||||||
|
model = "ANTMINER S9"
|
||||||
|
if board_name == "am2-s17":
|
||||||
|
model = "ANTMINER S17"
|
||||||
|
return model
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def __get_system_info_from_web(ip) -> dict:
|
||||||
|
url = f"http://{ip}/cgi-bin/get_system_info.cgi"
|
||||||
|
auth = httpx.DigestAuth("root", "root")
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
data = await client.get(url, auth=auth)
|
||||||
|
if data.status_code == 200:
|
||||||
|
data = data.json()
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def __get_dragonmint_version_from_web(
|
||||||
|
ip: ipaddress.ip_address,
|
||||||
|
) -> Union[str, None]:
|
||||||
|
response = None
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
async with asyncssh.connect(
|
auth = (
|
||||||
str(ip),
|
await client.post(
|
||||||
known_hosts=None,
|
f"http://{ip}/api/auth",
|
||||||
username="root",
|
data={"username": "admin", "password": "admin"},
|
||||||
password="admin",
|
)
|
||||||
server_host_key_algs=["ssh-rsa"],
|
).json()["jwt"]
|
||||||
) as conn:
|
response = (
|
||||||
board_name = None
|
await client.post(
|
||||||
cmd = await conn.run("cat /tmp/sysinfo/board_name")
|
f"http://{ip}/api/type",
|
||||||
if cmd:
|
headers={"Authorization": "Bearer " + auth},
|
||||||
board_name = cmd.stdout.strip()
|
data={},
|
||||||
|
)
|
||||||
if board_name:
|
).json()
|
||||||
if board_name == "am1-s9":
|
except Exception as e:
|
||||||
model = "Antminer S9"
|
logging.info(e)
|
||||||
if board_name == "am2-s17":
|
if response:
|
||||||
model = "Antminer S17"
|
return response["type"]
|
||||||
api = "BOSMiner+"
|
|
||||||
return model, api, None
|
|
||||||
|
|
||||||
except asyncssh.misc.PermissionDenied:
|
|
||||||
try:
|
|
||||||
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
|
|
||||||
auth = httpx.DigestAuth("root", "root")
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
data = await client.get(url, auth=auth)
|
|
||||||
if data.status_code == 200:
|
|
||||||
data = data.json()
|
|
||||||
if "minertype" in data.keys():
|
|
||||||
model = data["minertype"]
|
|
||||||
if "bmminer" in "\t".join(data.keys()):
|
|
||||||
api = "BMMiner"
|
|
||||||
except Exception as e:
|
|
||||||
logging.debug(f"Unable to get miner - {e}")
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
# if we have devdetails, we can get model data from there
|
|
||||||
if devdetails:
|
|
||||||
if "DEVDETAILS" in devdetails.keys() and not devdetails["DEVDETAILS"] == []:
|
|
||||||
# check for model, for most miners
|
|
||||||
if not devdetails["DEVDETAILS"][0]["Model"] == "":
|
|
||||||
# model of most miners
|
|
||||||
model = devdetails["DEVDETAILS"][0]["Model"]
|
|
||||||
|
|
||||||
# if model fails, try driver
|
|
||||||
else:
|
|
||||||
# some avalonminers have model in driver
|
|
||||||
model = devdetails["DEVDETAILS"][0]["Driver"]
|
|
||||||
else:
|
|
||||||
if "s9" in devdetails["STATUS"][0]["Description"]:
|
|
||||||
model = "Antminer S9"
|
|
||||||
|
|
||||||
# if we have version we can get API type from here
|
|
||||||
if version:
|
|
||||||
if "VERSION" in version.keys():
|
|
||||||
# check if there are any BMMiner strings in any of the dict keys
|
|
||||||
if any("BMMiner" in string for string in version["VERSION"][0].keys()):
|
|
||||||
api = "BMMiner"
|
|
||||||
|
|
||||||
# check if there are any CGMiner strings in any of the dict keys
|
|
||||||
elif any(
|
|
||||||
"CGMiner" in string for string in version["VERSION"][0].keys()
|
|
||||||
):
|
|
||||||
api = "CGMiner"
|
|
||||||
|
|
||||||
elif any(
|
|
||||||
"BTMiner" in string for string in version["VERSION"][0].keys()
|
|
||||||
):
|
|
||||||
api = "BTMiner"
|
|
||||||
|
|
||||||
# check if there are any BOSMiner strings in any of the dict keys
|
|
||||||
elif any(
|
|
||||||
"BOSminer" in string for string in version["VERSION"][0].keys()
|
|
||||||
):
|
|
||||||
api = "BOSMiner"
|
|
||||||
if version["VERSION"][0].get("BOSminer"):
|
|
||||||
if "plus" in version["VERSION"][0]["BOSminer"]:
|
|
||||||
api = "BOSMiner+"
|
|
||||||
|
|
||||||
if "BOSminer+" in version["VERSION"][0].keys():
|
|
||||||
api = "BOSMiner+"
|
|
||||||
|
|
||||||
# check for avalonminers
|
|
||||||
if version["VERSION"][0].get("PROD"):
|
|
||||||
_data = version["VERSION"][0]["PROD"].split("-")
|
|
||||||
model = _data[0]
|
|
||||||
if len(data) > 1:
|
|
||||||
ver = _data[1]
|
|
||||||
elif version["VERSION"][0].get("MODEL"):
|
|
||||||
_data = version["VERSION"][0]["MODEL"].split("-")
|
|
||||||
model = f"AvalonMiner {_data[0]}"
|
|
||||||
if len(data) > 1:
|
|
||||||
ver = _data[1]
|
|
||||||
|
|
||||||
# if all that fails, check the Description to see if it is a whatsminer
|
|
||||||
if version.get("Description") and (
|
|
||||||
"whatsminer" in version.get("Description")
|
|
||||||
):
|
|
||||||
api = "BTMiner"
|
|
||||||
|
|
||||||
# if we have no model from devdetails but have version, try to get it from there
|
|
||||||
if version and not model:
|
|
||||||
# make sure version isn't blank
|
|
||||||
if (
|
|
||||||
"VERSION" in version.keys()
|
|
||||||
and version.get("VERSION")
|
|
||||||
and not version.get("VERSION") == []
|
|
||||||
):
|
|
||||||
# try to get "Type" which is model
|
|
||||||
if version["VERSION"][0].get("Type"):
|
|
||||||
model = version["VERSION"][0]["Type"]
|
|
||||||
|
|
||||||
# braiins OS bug check just in case
|
|
||||||
elif "am2-s17" in version["STATUS"][0]["Description"]:
|
|
||||||
model = "Antminer S17"
|
|
||||||
|
|
||||||
if model:
|
|
||||||
# whatsminer have a V in their version string (M20SV41), remove everything after it
|
|
||||||
if "V" in model:
|
|
||||||
_ver = model.split("V")
|
|
||||||
if len(_ver) > 1:
|
|
||||||
ver = model.split("V")[1]
|
|
||||||
model = model.split("V")[0]
|
|
||||||
# don't need "Bitmain", just "Antminer XX" as model
|
|
||||||
if "Bitmain " in model:
|
|
||||||
model = model.replace("Bitmain ", "")
|
|
||||||
return model, api, ver
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _validate_command(data: dict) -> Tuple[bool, Union[str, None]]:
|
async def _validate_command(data: dict) -> Tuple[bool, Union[str, None]]:
|
||||||
@@ -562,9 +631,7 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
else:
|
else:
|
||||||
# make sure the command succeeded
|
# make sure the command succeeded
|
||||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||||
# this is an error
|
return False, data["STATUS"][0]["Msg"]
|
||||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
|
||||||
return False, data["STATUS"][0]["Msg"]
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -14,14 +14,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from pyasic.misc import Singleton
|
||||||
class Singleton(type):
|
|
||||||
_instances = {}
|
|
||||||
|
|
||||||
def __call__(cls, *args, **kwargs):
|
|
||||||
if cls not in cls._instances:
|
|
||||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
|
||||||
return cls._instances[cls]
|
|
||||||
|
|
||||||
|
|
||||||
class _MinerListener:
|
class _MinerListener:
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from pyasic.API.unknown import UnknownAPI
|
from pyasic.API.unknown import UnknownAPI
|
||||||
from pyasic.miners import BaseMiner
|
from pyasic.miners.base import BaseMiner
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
from pyasic.data import MinerData
|
||||||
|
|
||||||
|
|
||||||
class UnknownMiner(BaseMiner):
|
class UnknownMiner(BaseMiner):
|
||||||
@@ -31,3 +33,35 @@ class UnknownMiner(BaseMiner):
|
|||||||
|
|
||||||
async def get_hostname(self):
|
async def get_hostname(self):
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
|
async def check_light(self) -> bool:
|
||||||
|
if not self.light:
|
||||||
|
self.light = False
|
||||||
|
return self.light
|
||||||
|
|
||||||
|
async def fault_light_off(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def fault_light_on(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_config(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_errors(self) -> list:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_mac(self) -> str:
|
||||||
|
return "00:00:00:00:00:00"
|
||||||
|
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_data(self) -> MinerData:
|
||||||
|
return MinerData(ip=str(self.ip))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user