Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc43087b0d | ||
|
|
4fa3511725 | ||
|
|
4b9ae70424 | ||
|
|
74ebffb4fc | ||
|
|
54206da449 | ||
|
|
dd54ff7ee4 | ||
|
|
abef0c3d59 | ||
|
|
957c9a3678 | ||
|
|
50ccfec1b3 | ||
|
|
8e7d6751e2 | ||
|
|
b77c4173c6 | ||
|
|
4941cffb70 | ||
|
|
81d5d23189 | ||
|
|
9da5a836ce | ||
|
|
c9a536fc60 | ||
|
|
fa172b56b0 | ||
|
|
ee45f2342e |
@@ -3,7 +3,6 @@ repos:
|
|||||||
rev: v4.3.0
|
rev: v4.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -7,6 +7,9 @@
|
|||||||
[](https://pyasic.readthedocs.io/en/latest/)
|
[](https://pyasic.readthedocs.io/en/latest/)
|
||||||
[](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
|
[](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
|
||||||
[](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
|
[](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/)
|
||||||
|
|
||||||
@@ -91,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
|
||||||
@@ -103,7 +106,7 @@ 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()
|
||||||
@@ -122,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
|
||||||
@@ -132,7 +135,7 @@ 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())
|
||||||
@@ -150,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
|
||||||
@@ -160,7 +163,7 @@ 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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,7 +5,24 @@ Supported miner types are here on this list. If your miner (or miner version) i
|
|||||||
|
|
||||||
##### 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]:
|
||||||
@@ -23,6 +40,8 @@ Supported miner types are here on this list. If your miner (or miner version) i
|
|||||||
* [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]:
|
* [M20][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20]:
|
||||||
|
|||||||
@@ -122,6 +122,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
|
||||||
|
|
||||||
|
## M32
|
||||||
|
|
||||||
|
::: 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
|
||||||
|
|||||||
@@ -439,8 +439,8 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
self,
|
self,
|
||||||
auto: bool = True,
|
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.
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
|
|
||||||
from typing import Union, List
|
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
|
||||||
|
|
||||||
from .error_codes import X19Error, WhatsminerError, BraiinsOSError
|
from .error_codes import X19Error, WhatsminerError, BraiinsOSError
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ class MinerData:
|
|||||||
pool_2_user: The second pool user on the miner as a str.
|
pool_2_user: The second pool user on the miner as a str.
|
||||||
errors: A list of errors on the miner.
|
errors: A list of errors on the miner.
|
||||||
fault_light: Whether or not the fault light is on as a boolean.
|
fault_light: Whether or not the fault light is on as a boolean.
|
||||||
|
efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ip: str
|
ip: str
|
||||||
@@ -101,9 +103,22 @@ class MinerData:
|
|||||||
default_factory=list
|
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.__dict__])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_chips(self): # noqa - Skip PyCharm inspection
|
def total_chips(self): # noqa - Skip PyCharm inspection
|
||||||
@@ -149,5 +164,46 @@ 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_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:
|
||||||
|
tag_data.append(f"{attribute}={self[attribute]}")
|
||||||
|
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]}i")
|
||||||
|
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
|
||||||
|
|
||||||
|
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])
|
||||||
|
|||||||
@@ -12,126 +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 abc import ABC, abstractmethod
|
from typing import Union
|
||||||
|
|
||||||
from pyasic.data import MinerData
|
from pyasic.miners.base import BaseMiner, AnyMiner
|
||||||
from pyasic.config import MinerConfig
|
from pyasic.miners.miner_factory import MinerFactory
|
||||||
|
|
||||||
|
# abstracted version of get miner that is easier to access
|
||||||
class BaseMiner(ABC):
|
async def get_miner(ip: Union[ipaddress.ip_address, str]) -> AnyMiner:
|
||||||
def __init__(self, *args) -> None:
|
return await MinerFactory().get_miner(ip)
|
||||||
self.ip = None
|
|
||||||
self.uname = "root"
|
|
||||||
self.pwd = "admin"
|
|
||||||
self.api = None
|
|
||||||
self.api_type = None
|
|
||||||
self.model = None
|
|
||||||
self.light = None
|
|
||||||
self.hostname = None
|
|
||||||
self.nominal_chips = 1
|
|
||||||
self.version = None
|
|
||||||
self.fan_count = 2
|
|
||||||
self.config = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
if cls is BaseMiner:
|
|
||||||
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
|
|
||||||
return object.__new__(cls)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
|
|
||||||
|
|
||||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
|
||||||
"""Create a new asyncssh connection"""
|
|
||||||
try:
|
|
||||||
conn = await asyncssh.connect(
|
|
||||||
str(self.ip),
|
|
||||||
known_hosts=None,
|
|
||||||
username=self.uname,
|
|
||||||
password=self.pwd,
|
|
||||||
server_host_key_algs=["ssh-rsa"],
|
|
||||||
)
|
|
||||||
return conn
|
|
||||||
except asyncssh.misc.PermissionDenied:
|
|
||||||
try:
|
|
||||||
conn = await asyncssh.connect(
|
|
||||||
str(self.ip),
|
|
||||||
known_hosts=None,
|
|
||||||
username="root",
|
|
||||||
password="admin",
|
|
||||||
server_host_key_algs=["ssh-rsa"],
|
|
||||||
)
|
|
||||||
return conn
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
except OSError as e:
|
|
||||||
logging.warning(f"Connection refused: {self}")
|
|
||||||
raise e
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def fault_light_on(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def fault_light_off(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# async def send_file(self, src, dest):
|
|
||||||
# async with (await self._get_ssh_connection()) as conn:
|
|
||||||
# await asyncssh.scp(src, (conn, dest))
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def check_light(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# @abstractmethod
|
|
||||||
async def get_board_info(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_config(self) -> MinerConfig:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_model(self) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def reboot(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def restart_backend(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def send_config(self, *args, **kwargs):
|
|
||||||
return None
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_mac(self) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_errors(self) -> list:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def get_data(self) -> MinerData:
|
|
||||||
return MinerData(ip=str(self.ip))
|
|
||||||
|
|||||||
@@ -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,9 @@ class BMMiner(BaseMiner):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
async def check_light(self) -> bool:
|
async def check_light(self) -> bool:
|
||||||
if not self.light:
|
if not self.light:
|
||||||
self.light = False
|
self.light = False
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ 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.API import APIError
|
||||||
|
|
||||||
@@ -215,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.")
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import ipaddress
|
|||||||
from typing import Union
|
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
|
||||||
|
|
||||||
|
|
||||||
class BOSMinerOld(BaseMiner):
|
class BOSMinerOld(BaseMiner):
|
||||||
@@ -92,3 +93,6 @@ class BOSMinerOld(BaseMiner):
|
|||||||
|
|
||||||
async def restart_backend(self) -> bool:
|
async def restart_backend(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ 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.API import APIError
|
||||||
|
|
||||||
from pyasic.data import MinerData
|
from pyasic.data import MinerData
|
||||||
@@ -144,6 +144,9 @@ class BTMiner(BaseMiner):
|
|||||||
async def fault_light_on(self) -> bool:
|
async def fault_light_on(self) -> bool:
|
||||||
try:
|
try:
|
||||||
data = await self.api.set_led(auto=False)
|
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:
|
except APIError:
|
||||||
return False
|
return False
|
||||||
if data:
|
if data:
|
||||||
@@ -162,12 +165,8 @@ class BTMiner(BaseMiner):
|
|||||||
async def restart_backend(self) -> bool:
|
async def restart_backend(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_config(self, yaml_config, ip_user: bool = False):
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
if ip_user:
|
conf = config.as_wm(user_suffix=user_suffix)
|
||||||
suffix = str(self.ip).split(".")[-1]
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_wm(user_suffix=suffix)
|
|
||||||
else:
|
|
||||||
conf = MinerConfig().from_yaml(yaml_config).as_wm()
|
|
||||||
|
|
||||||
await self.api.update_pools(
|
await self.api.update_pools(
|
||||||
conf[0]["url"],
|
conf[0]["url"],
|
||||||
|
|||||||
@@ -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.API import APIError
|
||||||
|
from pyasic.config import MinerConfig
|
||||||
|
|
||||||
from pyasic.data import MinerData
|
from pyasic.data import MinerData
|
||||||
|
|
||||||
@@ -165,6 +166,9 @@ class CGMiner(BaseMiner):
|
|||||||
async def get_errors(self) -> list:
|
async def get_errors(self) -> list:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
async def get_mac(self) -> str:
|
async def get_mac(self) -> str:
|
||||||
return "00:00:00:00:00:00"
|
return "00:00:00:00:00:00"
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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 M20(BaseMiner):
|
class M20(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 M20S(BaseMiner):
|
class M20S(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 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,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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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 M32(BaseMiner):
|
class M32(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 M32S(BaseMiner):
|
class M32S(BaseMiner):
|
||||||
|
|||||||
@@ -55,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(self.uname, self.pwd)
|
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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
142
pyasic/miners/base.py
Normal file
142
pyasic/miners/base.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# async def send_file(self, src, dest):
|
||||||
|
# async with (await self._get_ssh_connection()) as conn:
|
||||||
|
# await asyncssh.scp(src, (conn, dest))
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def check_light(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# @abstractmethod
|
||||||
|
async def get_board_info(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_config(self) -> MinerConfig:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_hostname(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_model(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def reboot(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def restart_backend(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
async def get_data(self) -> MinerData:
|
||||||
|
return MinerData(ip=str(self.ip))
|
||||||
|
|
||||||
|
|
||||||
|
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
# 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 *
|
||||||
@@ -42,8 +42,6 @@ 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,
|
||||||
@@ -205,43 +203,43 @@ MINER_CLASSES = {
|
|||||||
"BTMiner": BTMinerM32,
|
"BTMiner": BTMinerM32,
|
||||||
"20": BTMinerM32V20,
|
"20": BTMinerM32V20,
|
||||||
},
|
},
|
||||||
"AvalonMiner 721": {
|
"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,
|
||||||
},
|
},
|
||||||
@@ -306,10 +304,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):
|
||||||
@@ -330,6 +325,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:
|
||||||
@@ -365,11 +378,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:
|
||||||
@@ -380,13 +388,127 @@ 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 "minertype" in data.keys():
|
||||||
|
model = data["minertype"].upper()
|
||||||
|
if "bmminer" in "\t".join(data.keys()):
|
||||||
|
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
|
||||||
|
|
||||||
|
if 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
|
||||||
@@ -398,179 +520,58 @@ 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:
|
||||||
try:
|
board_name = None
|
||||||
async with asyncssh.connect(
|
cmd = await conn.run("cat /tmp/sysinfo/board_name")
|
||||||
str(ip),
|
if cmd:
|
||||||
known_hosts=None,
|
board_name = cmd.stdout.strip()
|
||||||
username="root",
|
if board_name == "am1-s9":
|
||||||
password="admin",
|
model = "ANTMINER S9"
|
||||||
server_host_key_algs=["ssh-rsa"],
|
if board_name == "am2-s17":
|
||||||
) as conn:
|
model = "ANTMINER S17"
|
||||||
board_name = None
|
return model
|
||||||
cmd = await conn.run("cat /tmp/sysinfo/board_name")
|
|
||||||
if cmd:
|
|
||||||
board_name = cmd.stdout.strip()
|
|
||||||
|
|
||||||
if board_name:
|
@staticmethod
|
||||||
if board_name == "am1-s9":
|
async def __get_system_info_from_web(ip) -> dict:
|
||||||
model = "ANTMINER S9"
|
url = f"http://{ip}/cgi-bin/get_system_info.cgi"
|
||||||
if board_name == "am2-s17":
|
auth = httpx.DigestAuth("root", "root")
|
||||||
model = "ANTMINER S17"
|
async with httpx.AsyncClient() as client:
|
||||||
api = "BOSMiner+"
|
data = await client.get(url, auth=auth)
|
||||||
return model, api, None
|
if data.status_code == 200:
|
||||||
|
data = data.json()
|
||||||
except asyncssh.misc.PermissionDenied:
|
return data
|
||||||
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"].upper()
|
|
||||||
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"].upper()
|
|
||||||
|
|
||||||
# if model fails, try driver
|
|
||||||
else:
|
|
||||||
# some avalonminers have model in driver
|
|
||||||
model = devdetails["DEVDETAILS"][0]["Driver"].upper()
|
|
||||||
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].upper()
|
|
||||||
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"].upper()
|
|
||||||
|
|
||||||
# braiins OS bug check just in case
|
|
||||||
elif "am2-s17" in version["STATUS"][0]["Description"]:
|
|
||||||
model = "ANTMINER S17"
|
|
||||||
|
|
||||||
if not model:
|
|
||||||
stats = await self._send_api_command(str(ip), "stats")
|
|
||||||
if stats:
|
|
||||||
if "STATS" in stats.keys():
|
|
||||||
if stats["STATS"][0].get("Type"):
|
|
||||||
_model = stats["STATS"][0]["Type"].upper()
|
|
||||||
if " BB" in _model:
|
|
||||||
_model = _model.split(" BB")[0]
|
|
||||||
if " XILINX" in _model:
|
|
||||||
_model = _model.split(" XILINX")[0]
|
|
||||||
if "PRO" in _model and not " PRO" in _model:
|
|
||||||
model = _model.replace("PRO", " PRO")
|
|
||||||
|
|
||||||
if model:
|
|
||||||
if " HIVEON" in model:
|
|
||||||
model = model.split(" HIVEON")[0]
|
|
||||||
api = "Hiveon"
|
|
||||||
# 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]]:
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
|
||||||
class UnknownMiner(BaseMiner):
|
class UnknownMiner(BaseMiner):
|
||||||
@@ -57,3 +58,6 @@ class UnknownMiner(BaseMiner):
|
|||||||
|
|
||||||
async def restart_backend(self) -> bool:
|
async def restart_backend(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pyasic"
|
name = "pyasic"
|
||||||
version = "0.15.2"
|
version = "0.16.2"
|
||||||
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
|
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
|
||||||
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
||||||
repository = "https://github.com/UpstreamData/pyasic"
|
repository = "https://github.com/UpstreamData/pyasic"
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Reference in New Issue
Block a user