Compare commits

..

19 Commits

Author SHA1 Message Date
UpstreamData
b7d4891140 bump version number 2022-08-09 11:12:10 -06:00
UpstreamData
5f5cbd9060 add support for setting X19 web passwords and X17 web passwords. 2022-08-09 11:04:08 -06:00
UpstreamData
8379359caf update documentation and make BaseMiner and BaseMinerAPI unable to be instantiated directly. Add more unittests for miners. 2022-08-08 13:19:59 -06:00
UpstreamData
62238192ce bump version number 2022-08-05 16:34:00 -06:00
UpstreamData
1997003643 fix a bug with whatsminer crashing if hitting a S19 condition 2022-08-05 16:33:40 -06:00
UpstreamData
3a81844898 bump version number 2022-08-05 12:12:25 -06:00
UpstreamData
0ac80fb205 fix a bug with vnish miner identification 2022-08-05 12:12:10 -06:00
UpstreamData
9494018c12 bump version number 2022-08-05 12:08:05 -06:00
UpstreamData
0bc86c98c5 add support for some X19 models running vnish to be able to get miner type from them 2022-08-05 12:07:33 -06:00
UpstreamData
f0d69c9ca7 bump version number 2022-08-05 10:23:22 -06:00
UpstreamData
b81590bd2e add support for X19 miner errors codes shown on their dashboard 2022-08-05 10:23:03 -06:00
UpstreamData
a53e01df6f bump version number 2022-08-02 08:19:03 -06:00
UpstreamData
f63e063954 fix a bug with not capitalizing BITMAIN for a model check 2022-08-02 08:18:46 -06:00
upstreamdata
9cbaf7076a bump version number 2022-07-28 12:29:24 -06:00
upstreamdata
daa5ac5870 fixed a bug with capitalization of "Pro" in antminer models 2022-07-28 12:28:55 -06:00
upstreamdata
0b8c08016b bump version number. 2022-07-27 23:48:42 -06:00
upstreamdata
8c768d351b fix a bug with braiinsOS+ miners which return with a capital letter in their model instead of a lowercase letter. 2022-07-27 23:44:19 -06:00
UpstreamData
c9e7fa2629 bump version number 2022-07-22 13:05:13 -06:00
UpstreamData
9d3f2b5968 add support for M20 versions and update docs 2022-07-22 13:04:47 -06:00
35 changed files with 568 additions and 78 deletions

View File

@@ -2,11 +2,12 @@
## Miner APIs
Each miner has a unique API that is used to communicate with it.
Each of these API types has commands that differ between them, and some commands have data that others do not.
Each miner that is a subclass of `BaseMiner` 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.
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 -
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]

25
docs/data/error_codes.md Normal file
View 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

10
docs/miners/base_miner.md Normal file
View 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

View File

@@ -6,3 +6,15 @@
options:
show_root_heading: false
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.

View File

@@ -3,8 +3,6 @@
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:
* Braiins OS+ Devices:
* All devices supported by BraiinsOS+ are supported here.
@@ -27,6 +25,8 @@ Supported miner types are here on this list. If your miner (or miner version) i
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20]
* [M32S][pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S]
* 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]:
* [V10][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV10]
* [V20][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV20]

View File

@@ -1,6 +1,22 @@
# pyasic
## 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
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S

View File

@@ -1,7 +1,7 @@
# pyasic
## 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.
This allows this class to be the [`MinerNetwork.network`][pyasic.network.MinerNetwork] and hence be used for scanning.

View File

@@ -28,17 +28,21 @@ nav:
- Miner Network Range: "network/miner_network_range.md"
- Data:
- Miner Data: "data/miner_data.md"
- Error Codes: "data/error_codes.md"
- Config:
- Miner Config: "config/miner_config.md"
- Advanced:
- Miner APIs:
- Base: "API/api.md"
- Intro: "API/api.md"
- BMMiner: "API/bmminer.md"
- BOSMiner: "API/bosminer.md"
- BTMiner: "API/btminer.md"
- CGMiner: "API/cgminer.md"
- Unknown: "API/unknown.md"
- Base Miner: "miners/base_miner.md"
plugins:
- mkdocstrings
- search

View File

@@ -55,6 +55,11 @@ class BaseMinerAPI:
# ip address of the miner
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:
"""Get a list of command accessible to a specific type of API on the miner.

View File

@@ -19,6 +19,7 @@ import hashlib
import binascii
import base64
import logging
from typing import Union
from passlib.handlers.md5_crypt import md5_crypt
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@@ -187,8 +188,8 @@ class BTMinerAPI(BaseMinerAPI):
async def send_command(
self,
command: str or bytes,
parameters: str or int or bool = None,
command: Union[str, bytes],
parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
**kwargs,
) -> dict:

View File

@@ -287,6 +287,15 @@ class MinerConfig:
self.pool_groups = pool_groups
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):
"""Convert an output dict of this class back into usable data and save it to this class.

View File

@@ -12,10 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Union
from typing import Union, List
from dataclasses import dataclass, field, asdict
from datetime import datetime
from .error_codes import X19Error, WhatsminerError, BraiinsOSError
@dataclass
class MinerData:
@@ -95,7 +97,9 @@ class MinerData:
pool_1_user: str = "Unknown"
pool_2_url: str = ""
pool_2_user: str = ""
errors: list = field(default_factory=list)
errors: List[Union[WhatsminerError, BraiinsOSError, X19Error]] = field(
default_factory=list
)
fault_light: Union[bool, None] = None
def __post_init__(self):

View 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)

View File

@@ -14,3 +14,4 @@
from .whatsminer import WhatsminerError
from .bos import BraiinsOSError
from .X19 import X19Error

View File

@@ -17,7 +17,11 @@ from dataclasses import dataclass, asdict
@dataclass
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

View File

@@ -17,7 +17,12 @@ from dataclasses import dataclass, field, asdict
@dataclass
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_message: str = field(init=False)

View File

@@ -15,11 +15,13 @@
import asyncssh
import logging
import ipaddress
from abc import ABC, abstractmethod
from pyasic.data import MinerData
from pyasic.config import MinerConfig
class BaseMiner:
class BaseMiner(ABC):
def __init__(self, *args) -> None:
self.ip = None
self.uname = "root"
@@ -34,6 +36,11 @@ class BaseMiner:
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)}"
@@ -75,42 +82,56 @@ class BaseMiner:
except Exception as e:
raise e
@abstractmethod
async def fault_light_on(self) -> bool:
return False
pass
@abstractmethod
async def fault_light_off(self) -> bool:
return False
pass
async def send_file(self, src, dest):
async with (await self._get_ssh_connection()) as conn:
await asyncssh.scp(src, (conn, dest))
# 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
@abstractmethod
async def check_light(self) -> bool:
pass
# @abstractmethod
async def get_board_info(self):
return None
async def get_config(self):
return None
@abstractmethod
async def get_config(self) -> MinerConfig:
pass
async def get_hostname(self):
return None
@abstractmethod
async def get_hostname(self) -> str:
pass
async def get_model(self):
return None
@abstractmethod
async def get_model(self) -> str:
pass
async def reboot(self):
return False
@abstractmethod
async def reboot(self) -> bool:
pass
async def restart_backend(self):
return False
@abstractmethod
async def restart_backend(self) -> bool:
pass
async def send_config(self, *args, **kwargs):
return None
async def get_mac(self):
return None
@abstractmethod
async def get_mac(self) -> str:
pass
@abstractmethod
async def get_errors(self) -> list:
pass
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))

View File

@@ -154,6 +154,26 @@ class BMMiner(BaseMiner):
return True
return False
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:
"""Get data from the miner.
@@ -168,6 +188,7 @@ class BMMiner(BaseMiner):
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
@@ -178,6 +199,10 @@ class BMMiner(BaseMiner):
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

View File

@@ -240,6 +240,49 @@ class BOSMiner(BaseMiner):
logging.debug(f"{self}: Restarting BOSMiner")
await conn.run("/etc/init.d/bosminer start")
async def check_light(self) -> bool:
if not self.light:
self.light = False
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:
"""Get data from the miner.

View File

@@ -15,6 +15,7 @@
import logging
import ipaddress
from typing import Union
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.miners import BaseMiner
@@ -29,7 +30,7 @@ class BOSMinerOld(BaseMiner):
self.uname = "root"
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.
:return: Result of the command or None.
@@ -61,3 +62,33 @@ class BOSMinerOld(BaseMiner):
async def update_to_plus(self):
result = await self.send_ssh_command("opkg update && opkg install bos_plus")
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

View File

@@ -23,6 +23,7 @@ from pyasic.API import APIError
from pyasic.data import MinerData
from pyasic.data.error_codes import WhatsminerError
from pyasic.config import MinerConfig
from pyasic.settings import PyasicSettings
@@ -99,6 +100,40 @@ class BTMiner(BaseMiner):
return str(mac).upper()
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 reboot(self) -> bool:
return False
async def restart_backend(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_data(self) -> MinerData:
"""Get data from the miner.

View File

@@ -151,6 +151,23 @@ class CGMiner(BaseMiner):
self.config = result.stdout
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 get_mac(self) -> str:
return "00:00:00:00:00:00"
async def get_data(self) -> MinerData:
"""Get data from the miner.

View 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 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

View File

@@ -28,7 +28,7 @@ class M20SV10(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20S"
self.model = "M20S V10"
self.nominal_chips = 105
self.fan_count = 2
@@ -37,6 +37,6 @@ class M20SV20(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20S"
self.model = "M20S V20"
self.nominal_chips = 111
self.fan_count = 2

View File

@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .M20 import M20, M20V10
from .M20S import M20S, M20SV10, M20SV20
from .M20S_Plus import M20SPlus

View File

@@ -14,6 +14,8 @@
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
from pyasic.settings import PyasicSettings
import httpx
from typing import Union
@@ -22,11 +24,13 @@ class BMMinerX17(BMMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
self.uname = "root"
self.pwd = PyasicSettings().global_x17_password
async def get_hostname(self) -> Union[str, None]:
hostname = None
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:
data = await client.get(url, auth=auth)
if data.status_code == 200:
@@ -39,7 +43,7 @@ class BMMinerX17(BMMiner):
async def get_mac(self) -> Union[str, None]:
mac = None
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:
data = await client.get(url, auth=auth)
if data.status_code == 200:
@@ -51,7 +55,7 @@ class BMMinerX17(BMMiner):
async def fault_light_on(self) -> bool:
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:
try:
await client.post(url, data={"action": "startBlink"}, auth=auth)
@@ -68,7 +72,7 @@ class BMMinerX17(BMMiner):
async def fault_light_off(self) -> bool:
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:
await client.post(url, data={"action": "stopBlink"}, auth=auth)
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
@@ -83,7 +87,7 @@ class BMMinerX17(BMMiner):
if self.light:
return self.light
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:
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
@@ -98,7 +102,7 @@ class BMMinerX17(BMMiner):
async def reboot(self) -> bool:
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:
data = await client.get(url, auth=auth)
if data.status_code == 200:

View File

@@ -15,23 +15,27 @@
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
from pyasic.config import MinerConfig
from pyasic.data.error_codes import X19Error
from pyasic.settings import PyasicSettings
import httpx
import json
import asyncio
from typing import Union
from typing import Union, List
class BMMinerX19(BMMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
self.uname = "root"
self.pwd = PyasicSettings().global_x19_password
async def check_light(self) -> Union[bool, None]:
if self.light:
return self.light
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:
data = await client.get(url, auth=auth)
if data.status_code == 200:
@@ -43,7 +47,7 @@ class BMMinerX19(BMMiner):
async def get_config(self) -> MinerConfig:
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:
data = await client.get(url, auth=auth)
if data.status_code == 200:
@@ -53,7 +57,7 @@ class BMMinerX19(BMMiner):
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
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:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
@@ -74,7 +78,7 @@ class BMMinerX19(BMMiner):
async def get_hostname(self) -> Union[str, None]:
hostname = None
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:
data = await client.get(url, auth=auth)
if data.status_code == 200:
@@ -87,7 +91,7 @@ class BMMinerX19(BMMiner):
async def get_mac(self) -> Union[str, None]:
mac = None
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:
data = await client.get(url, auth=auth)
if data.status_code == 200:
@@ -99,7 +103,7 @@ class BMMinerX19(BMMiner):
async def fault_light_on(self) -> bool:
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"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
@@ -112,7 +116,7 @@ class BMMinerX19(BMMiner):
async def fault_light_off(self) -> bool:
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"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
@@ -125,9 +129,24 @@ class BMMinerX19(BMMiner):
async def reboot(self) -> bool:
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:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
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

View File

@@ -45,93 +45,98 @@ import asyncssh
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
MINER_CLASSES = {
"Antminer S9": {
"ANTMINER S9": {
"Default": BOSMinerS9,
"BOSMiner": BOSMinerOld,
"BOSMiner+": BOSMinerS9,
"BMMiner": BMMinerS9,
"CGMiner": CGMinerS9,
},
"Antminer S9i": {
"ANTMINER S9I": {
"Default": BMMinerS9i,
"BMMiner": BMMinerS9i,
},
"Antminer S17": {
"ANTMINER S17": {
"Default": BMMinerS17,
"BOSMiner+": BOSMinerS17,
"BMMiner": BMMinerS17,
"CGMiner": CGMinerS17,
},
"Antminer S17+": {
"ANTMINER S17+": {
"Default": BMMinerS17Plus,
"BOSMiner+": BOSMinerS17Plus,
"BMMiner": BMMinerS17Plus,
"CGMiner": CGMinerS17Plus,
},
"Antminer S17 Pro": {
"ANTMINER S17 PRO": {
"Default": BMMinerS17Pro,
"BOSMiner+": BOSMinerS17Pro,
"BMMiner": BMMinerS17Pro,
"CGMiner": CGMinerS17Pro,
},
"Antminer S17e": {
"ANTMINER S17E": {
"Default": BMMinerS17e,
"BOSMiner+": BOSMinerS17e,
"BMMiner": BMMinerS17e,
"CGMiner": CGMinerS17e,
},
"Antminer T17": {
"ANTMINER T17": {
"Default": BMMinerT17,
"BOSMiner+": BOSMinerT17,
"BMMiner": BMMinerT17,
"CGMiner": CGMinerT17,
},
"Antminer T17+": {
"ANTMINER T17+": {
"Default": BMMinerT17Plus,
"BOSMiner+": BOSMinerT17Plus,
"BMMiner": BMMinerT17Plus,
"CGMiner": CGMinerT17Plus,
},
"Antminer T17e": {
"ANTMINER T17E": {
"Default": BMMinerT17e,
"BOSMiner+": BOSMinerT17e,
"BMMiner": BMMinerT17e,
"CGMiner": CGMinerT17e,
},
"Antminer S19": {
"ANTMINER S19": {
"Default": BMMinerS19,
"BOSMiner+": BOSMinerS19,
"BMMiner": BMMinerS19,
"CGMiner": CGMinerS19,
},
"Antminer S19 Pro": {
"ANTMINER S19 PRO": {
"Default": BMMinerS19Pro,
"BOSMiner+": BOSMinerS19Pro,
"BMMiner": BMMinerS19Pro,
"CGMiner": CGMinerS19Pro,
},
"Antminer S19j": {
"ANTMINER S19J": {
"Default": BMMinerS19j,
"BOSMiner+": BOSMinerS19j,
"BMMiner": BMMinerS19j,
"CGMiner": CGMinerS19j,
},
"Antminer S19j Pro": {
"ANTMINER S19J PRO": {
"Default": BMMinerS19jPro,
"BOSMiner+": BOSMinerS19jPro,
"BMMiner": BMMinerS19jPro,
"CGMiner": CGMinerS19jPro,
},
"Antminer S19a": {
"ANTMINER S19A": {
"Default": BMMinerS19a,
"BMMiner": BMMinerS19a,
},
"Antminer T19": {
"ANTMINER T19": {
"Default": BMMinerT19,
"BOSMiner+": BOSMinerT19,
"BMMiner": BMMinerT19,
"CGMiner": CGMinerT19,
},
"M20": {
"Default": BTMinerM20,
"BTMiner": BTMinerM20,
"10": BTMinerM20V10,
},
"M20S": {
"Default": BTMinerM20S,
"BTMiner": BTMinerM20S,
@@ -230,6 +235,7 @@ MINER_CLASSES = {
"Default": CGMinerAvalon1066,
"CGMiner": CGMinerAvalon1066,
},
"Unknown": {"Default": UnknownMiner},
}
@@ -426,9 +432,9 @@ class MinerFactory(metaclass=Singleton):
if board_name:
if board_name == "am1-s9":
model = "Antminer S9"
model = "ANTMINER S9"
if board_name == "am2-s17":
model = "Antminer S17"
model = "ANTMINER S17"
api = "BOSMiner+"
return model, api, None
@@ -441,7 +447,7 @@ class MinerFactory(metaclass=Singleton):
if data.status_code == 200:
data = data.json()
if "minertype" in data.keys():
model = data["minertype"]
model = data["minertype"].upper()
if "bmminer" in "\t".join(data.keys()):
api = "BMMiner"
except Exception as e:
@@ -454,15 +460,15 @@ class MinerFactory(metaclass=Singleton):
# check for model, for most miners
if not devdetails["DEVDETAILS"][0]["Model"] == "":
# model of most miners
model = devdetails["DEVDETAILS"][0]["Model"]
model = devdetails["DEVDETAILS"][0]["Model"].upper()
# if model fails, try driver
else:
# some avalonminers have model in driver
model = devdetails["DEVDETAILS"][0]["Driver"]
model = devdetails["DEVDETAILS"][0]["Driver"].upper()
else:
if "s9" in devdetails["STATUS"][0]["Description"]:
model = "Antminer S9"
model = "ANTMINER S9"
# if we have version we can get API type from here
if version:
@@ -497,7 +503,7 @@ class MinerFactory(metaclass=Singleton):
# check for avalonminers
if version["VERSION"][0].get("PROD"):
_data = version["VERSION"][0]["PROD"].split("-")
model = _data[0]
model = _data[0].upper()
if len(data) > 1:
ver = _data[1]
elif version["VERSION"][0].get("MODEL"):
@@ -522,11 +528,24 @@ class MinerFactory(metaclass=Singleton):
):
# try to get "Type" which is model
if version["VERSION"][0].get("Type"):
model = version["VERSION"][0]["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"
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:
# whatsminer have a V in their version string (M20SV41), remove everything after it
@@ -535,9 +554,9 @@ class MinerFactory(metaclass=Singleton):
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 ", "")
# don't need "Bitmain", just "ANTMINER XX" as model
if "BITMAIN " in model:
model = model.replace("BITMAIN ", "")
return model, api, ver
@staticmethod

View File

@@ -31,3 +31,29 @@ class UnknownMiner(BaseMiner):
async def get_hostname(self):
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

View File

@@ -0,0 +1,31 @@
# 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 BTMiner # noqa - Ignore access to _module
from pyasic.miners._types import ( # noqa - Ignore access to _module
M20,
M20V10,
)
class BTMinerM20(BTMiner, M20):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM20V10(BTMiner, M20V10):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .M20 import BTMinerM20, BTMinerM20V10
from .M20S import BTMinerM20S, BTMinerM20SV10, BTMinerM20SV20
from .M20S_Plus import BTMinerM20SPlus

View File

@@ -35,6 +35,8 @@ class PyasicSettings(metaclass=Singleton):
miner_get_data_retries: int = 1
global_whatsminer_password = "admin"
global_x19_password = "root"
global_x17_password = "root"
debug: bool = False
logfile: bool = False

View File

@@ -13,6 +13,8 @@
# limitations under the License.
import unittest
from pyasic.tests.miners_tests import MinersTest
from pyasic.tests.network_tests import NetworkTest
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,54 @@
# 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 unittest
from pyasic.miners.miner_factory import MINER_CLASSES
import inspect
import sys
class MinersTest(unittest.TestCase):
def test_miner_model_creation(self):
for miner_model in MINER_CLASSES.keys():
for miner_api in MINER_CLASSES[miner_model].keys():
with self.subTest(miner_model=miner_model, miner_api=miner_api):
miner = MINER_CLASSES[miner_model][miner_api]("0.0.0.0")
self.assertTrue(
isinstance(miner, MINER_CLASSES[miner_model][miner_api])
)
def test_miner_backend_backup_creation(self):
backends = inspect.getmembers(
sys.modules["pyasic.miners._backends"], inspect.isclass
)
for backend in backends:
miner_class = backend[1]
with self.subTest(miner_class=miner_class):
miner = miner_class("0.0.0.0")
self.assertTrue(isinstance(miner, miner_class))
def test_miner_type_creation_failure(self):
backends = inspect.getmembers(
sys.modules["pyasic.miners._types"], inspect.isclass
)
for backend in backends:
miner_class = backend[1]
with self.subTest(miner_class=miner_class):
with self.assertRaises(TypeError):
miner_class("0.0.0.0")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyasic"
version = "0.13.2"
version = "0.15.0"
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>"]
repository = "https://github.com/UpstreamData/pyasic"