Compare commits

..

23 Commits

Author SHA1 Message Date
UpstreamData
58234fcf7f version: bump version number. 2023-07-07 12:03:28 -06:00
UpstreamData
1bf863cca8 bug: set miner_mode to int instead of str to fix some issues with some X19 models. 2023-07-07 12:03:00 -06:00
UpstreamData
6482d04185 version: bump version number. 2023-07-07 11:56:41 -06:00
UpstreamData
3b58b11501 bug: remove 0 frequency level when setting sleep mode on X19, as it seems to bug some types. 2023-07-07 11:56:06 -06:00
UpstreamData
7485b8ef77 version: bump version number. 2023-07-04 10:04:20 -06:00
UpstreamData
d2bea227db bug: fix an issue with a possible SSH command in BOS+. 2023-07-04 10:01:24 -06:00
UpstreamData
1b7afaaf7e version: bump version number. 2023-06-30 08:49:31 -06:00
UpstreamData
96898d639c bug: fix some handling errors with graphql. 2023-06-30 08:49:08 -06:00
UpstreamData
eb439f4dcf version: bump version number. 2023-06-30 08:44:01 -06:00
UpstreamData
69f4349393 feature: create pwd and username property in miner object that sets web and api passwords and usernames. 2023-06-30 08:43:30 -06:00
UpstreamData
e371bb577c version: bump version number. 2023-06-29 18:10:35 -06:00
UpstreamData
2500ec3869 bug: fix possible None return from some bosminer webcommands. 2023-06-29 18:10:10 -06:00
UpstreamData
5be3187eec version: bump version number. 2023-06-29 16:51:29 -06:00
UpstreamData
be1e9127b0 bug: fix weird pool info when multiple groups are defined. 2023-06-29 16:51:15 -06:00
UpstreamData
13572c4770 version: bump version number. 2023-06-29 16:48:09 -06:00
UpstreamData
08fa3961fe bug: fix bosminer on X19 not reporting pause mode correctly. 2023-06-29 16:47:27 -06:00
UpstreamData
b5d2809e9c format: remove random print statements. 2023-06-29 15:53:53 -06:00
UpstreamData
aa538d3079 docs: add miner docs generation file. 2023-06-28 11:21:19 -06:00
UpstreamData
e1500bb75c docs: update docs. 2023-06-28 11:20:22 -06:00
UpstreamData
7f00a65598 version: bump version number. 2023-06-27 16:23:46 -06:00
UpstreamData
64c473a7d4 bug: fix improper stats key when getting uptime. 2023-06-27 16:23:21 -06:00
UpstreamData
96d9fe8e6c version: bump version number. 2023-06-27 14:56:11 -06:00
UpstreamData
0b27400d27 feature: add set_static_ip and set_dhcp for bosminer. 2023-06-27 14:55:05 -06:00
16 changed files with 357 additions and 69 deletions

163
docs/generate_miners.py Normal file
View File

@@ -0,0 +1,163 @@
import asyncio
import importlib
import os
import warnings
from pyasic.miners.miner_factory import MINER_CLASSES, MinerTypes
warnings.filterwarnings("ignore")
def path(cls):
module = importlib.import_module(cls.__module__)
return module.__name__ + "." + cls.__name__
def make(cls):
p = path(cls)
return p.split(".")[2]
def model_type(cls):
p = path(cls)
return p.split(".")[4]
def backend_str(backend: MinerTypes) -> str:
match backend:
case MinerTypes.ANTMINER:
return "Stock Firmware Antminers"
case MinerTypes.AVALONMINER:
return "Stock Firmware Avalonminers"
case MinerTypes.VNISH:
return "Vnish Firmware Miners"
case MinerTypes.BRAIINS_OS:
return "BOS+ Firmware Miners"
case MinerTypes.HIVEON:
return "HiveOS Firmware Miners"
case MinerTypes.INNOSILICON:
return "Stock Firmware Innosilicons"
case MinerTypes.WHATSMINER:
return "Stock Firmware Whatsminers"
case MinerTypes.GOLDSHELL:
return "Stock Firmware Goldshells"
case MinerTypes.LUX_OS:
return "LuxOS Firmware Miners"
def create_url_str(mtype: str):
return (
mtype.lower()
.replace(" ", "-")
.replace("(", "")
.replace(")", "")
.replace("+", "_1")
)
HEADER_FORMAT = "# pyasic\n## {} Models\n\n"
MINER_HEADER_FORMAT = "## {}\n"
DATA_FORMAT = """::: {}
handler: python
options:
show_root_heading: false
heading_level: 4
"""
SUPPORTED_TYPES_HEADER = """# pyasic
## Supported Miners
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
##### pyasic currently supports the following miners and subtypes:
<style>
details {
margin:0px;
padding-top:0px;
padding-bottom:0px;
}
</style>
"""
BACKEND_TYPE_HEADER = """
<details>
<summary>{}:</summary>
<ul>"""
MINER_TYPE_HEADER = """
<details>
<summary>{} Series:</summary>
<ul>"""
MINER_DETAILS = """
<li><a href="../{}/{}#{}">{}</a></li>"""
MINER_TYPE_CLOSER = """
</ul>
</details>"""
BACKEND_TYPE_CLOSER = """
</ul>
</details>"""
m_data = {}
for m in MINER_CLASSES:
for t in MINER_CLASSES[m]:
if t is not None:
miner = MINER_CLASSES[m][t]
if make(miner) not in m_data:
m_data[make(miner)] = {}
if model_type(miner) not in m_data[make(miner)]:
m_data[make(miner)][model_type(miner)] = []
m_data[make(miner)][model_type(miner)].append(miner)
async def create_directory_structure(directory, data):
if not os.path.exists(directory):
os.makedirs(directory)
for key, value in data.items():
subdirectory = os.path.join(directory, key)
if isinstance(value, dict):
await create_directory_structure(subdirectory, value)
elif isinstance(value, list):
file_path = os.path.join(subdirectory + ".md")
with open(file_path, "w") as file:
file.write(HEADER_FORMAT.format(key))
for item in value:
header = await item("1.1.1.1").get_model()
file.write(MINER_HEADER_FORMAT.format(header))
file.write(DATA_FORMAT.format(path(item)))
async def create_supported_types(directory):
with open(os.path.join(directory, "supported_types.md"), "w") as file:
file.write(SUPPORTED_TYPES_HEADER)
for mback in MINER_CLASSES:
backend_types = {}
file.write(BACKEND_TYPE_HEADER.format(backend_str(mback)))
for mtype in MINER_CLASSES[mback]:
if mtype is None:
continue
m = MINER_CLASSES[mback][mtype]
if model_type(m) not in backend_types:
backend_types[model_type(m)] = []
backend_types[model_type(m)].append(m)
for mtype in backend_types:
file.write(MINER_TYPE_HEADER.format(mtype))
for minstance in backend_types[mtype]:
model = await minstance("1.1.1.1").get_model()
file.write(
MINER_DETAILS.format(
make(minstance), mtype, create_url_str(model), model
)
)
file.write(MINER_TYPE_CLOSER)
file.write(BACKEND_TYPE_CLOSER)
root_directory = os.path.join(os.getcwd(), "miners")
asyncio.run(create_directory_structure(root_directory, m_data))
asyncio.run(create_supported_types(root_directory))

View File

@@ -127,6 +127,13 @@
show_root_heading: false
heading_level: 4
## S19 No PIC (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19NoPIC
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19Pro
handler: python

View File

@@ -50,3 +50,10 @@
show_root_heading: false
heading_level: 4
## S9 (LuxOS)
::: pyasic.miners.antminer.luxos.X9.S9.LUXMinerS9
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -10,6 +10,9 @@ details {
padding-top:0px;
padding-bottom:0px;
}
ul {
margin:0px;
}
</style>
<details>
@@ -419,6 +422,7 @@ details {
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-vnish">S19 (VNish)</a></li>
<li><a href="../antminer/X19#s19-no-pic-vnish">S19 No PIC (VNish)</a></li>
<li><a href="../antminer/X19#s19-pro-vnish">S19 Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19j-vnish">S19j (VNish)</a></li>
<li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li>
@@ -439,4 +443,15 @@ details {
</ul>
</details>
</ul>
</details>
</details>
<details>
<summary>LuxOS Firmware Miners:</summary>
<ul>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9#s9-luxos">S9 (LuxOS)</a></li>
</ul>
</details>
</ul>
</details>

View File

@@ -32,6 +32,8 @@ class BaseMinerAPI:
# ip address of the miner
self.ip = ipaddress.ip_address(ip)
self.pwd = "admin"
def __new__(cls, *args, **kwargs):
if cls is BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")

View File

@@ -550,7 +550,7 @@ class MinerConfig:
"bitmain-fan-ctrl": False,
"bitmain-fan-pwn": "100",
"freq-level": "100",
"miner-mode": str(self.miner_mode.value),
"miner-mode": self.miner_mode.value,
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
}
@@ -560,9 +560,6 @@ class MinerConfig:
if self.fan_speed:
cfg["bitmain-fan-pwn"] = str(self.fan_speed)
if self.miner_mode == X19PowerMode.Sleep:
cfg["freq-level"] = "0"
return cfg
def as_x17(self, user_suffix: str = None) -> dict:

View File

@@ -257,7 +257,7 @@ class AntminerModern(BMMiner):
if api_stats:
try:
return int(api_stats["STATS"][0]["Elapsed"])
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass
@@ -502,6 +502,6 @@ class AntminerOld(CGMiner):
if api_stats:
try:
return int(api_stats["STATS"][0]["Elapsed"])
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass

View File

@@ -18,6 +18,7 @@ from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data import HashBoard
from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.miners.backends import BFGMiner
from pyasic.web.goldshell import GoldshellWebAPI
@@ -138,7 +139,7 @@ class BFGMinerGoldshell(BFGMiner):
except KeyError:
pass
else:
print(self, api_devs)
logger.error(self, api_devs)
if not api_devdetails:
try:
@@ -156,7 +157,7 @@ class BFGMinerGoldshell(BFGMiner):
except KeyError:
pass
else:
print(self, api_devdetails)
logger.error(self, api_devdetails)
return hashboards

View File

@@ -370,6 +370,6 @@ class BMMiner(BaseMiner):
if api_stats:
try:
return int(api_stats["STATS"][0]["Elapsed"])
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass

View File

@@ -174,7 +174,7 @@ BOSMINER_DATA_LOC = {
},
"is_mining": {
"cmd": "is_mining",
"kwargs": {"api_tunerstatus": {"api": "tunerstatus"}},
"kwargs": {"api_devdetails": {"api": "devdetails"}},
},
"uptime": {
"cmd": "get_uptime",
@@ -373,6 +373,52 @@ class BOSMiner(BaseMiner):
else:
return True
async def set_static_ip(
self,
ip: str,
dns: str,
gateway: str,
subnet_mask: str = "255.255.255.0",
):
cfg_data_lan = (
"config interface 'lan'\n\toption type 'bridge'\n\toption ifname 'eth0'\n\toption proto 'static'\n\toption ipaddr '"
+ ip
+ "'\n\toption netmask '"
+ subnet_mask
+ "'\n\toption gateway '"
+ gateway
+ "'\n\toption dns '"
+ dns
+ "'"
)
data = await self.send_ssh_command("cat /etc/config/network")
split_data = data.split("\n\n")
for idx in range(len(split_data)):
if "config interface 'lan'" in split_data[idx]:
split_data[idx] = cfg_data_lan
config = "\n\n".join(split_data)
conn = await self._get_ssh_connection()
async with conn:
await conn.run("echo '" + config + "' > /etc/config/network")
async def set_dhcp(self):
cfg_data_lan = "config interface 'lan'\n\toption type 'bridge'\n\toption ifname 'eth0'\n\toption proto 'dhcp'"
data = await self.send_ssh_command("cat /etc/config/network")
split_data = data.split("\n\n")
for idx in range(len(split_data)):
if "config interface 'lan'" in split_data[idx]:
split_data[idx] = cfg_data_lan
config = "\n\n".join(split_data)
conn = await self._get_ssh_connection()
async with conn:
await conn.run("echo '" + config + "' > /etc/config/network")
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
@@ -386,8 +432,6 @@ class BOSMiner(BaseMiner):
except APIError:
pass
print(web_net_conf)
if isinstance(web_net_conf, dict):
if "/cgi-bin/luci/admin/network/iface_status/lan" in web_net_conf.keys():
web_net_conf = web_net_conf[
@@ -450,7 +494,7 @@ class BOSMiner(BaseMiner):
if graphql_version:
try:
fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"]
except KeyError:
except (KeyError, TypeError):
pass
if not fw_ver:
@@ -479,7 +523,7 @@ class BOSMiner(BaseMiner):
try:
hostname = graphql_hostname["data"]["bos"]["hostname"]
return hostname
except KeyError:
except (TypeError, KeyError):
pass
try:
@@ -519,7 +563,7 @@ class BOSMiner(BaseMiner):
),
2,
)
except (KeyError, IndexError, ValueError):
except (LookupError, ValueError, TypeError):
pass
# get hr from API
@@ -573,7 +617,7 @@ class BOSMiner(BaseMiner):
boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][
"childSolvers"
]
except (KeyError, IndexError):
except (TypeError, LookupError):
boards = None
if boards:
@@ -688,7 +732,7 @@ class BOSMiner(BaseMiner):
return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][
"power"
]["approxConsumptionW"]
except (KeyError, TypeError):
except (LookupError, TypeError):
pass
if not api_tunerstatus:
@@ -721,7 +765,7 @@ class BOSMiner(BaseMiner):
return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][
"power"
]["limitW"]
except (KeyError, TypeError):
except (LookupError, TypeError):
pass
if not api_tunerstatus:
@@ -757,7 +801,7 @@ class BOSMiner(BaseMiner):
]
)
)
except KeyError:
except (LookupError, TypeError):
pass
return fans
@@ -897,7 +941,7 @@ class BOSMiner(BaseMiner):
boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][
"childSolvers"
]
except (KeyError, IndexError):
except (LookupError, TypeError):
boards = None
if boards:
@@ -990,17 +1034,20 @@ class BOSMiner(BaseMiner):
try:
self.light = graphql_fault_light["data"]["bos"]["faultLight"]
return self.light
except (TypeError, KeyError, ValueError, IndexError):
except (TypeError, ValueError, LookupError):
pass
# get light via ssh if that fails (10x slower)
data = (
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
).strip()
self.light = False
if data == "50":
self.light = True
return self.light
try:
data = (
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
).strip()
self.light = False
if data == "50":
self.light = True
return self.light
except TypeError:
return self.light
async def get_nominal_hashrate(self, api_devs: dict = None) -> Optional[float]:
if not api_devs:
@@ -1028,22 +1075,16 @@ class BOSMiner(BaseMiner):
except (IndexError, KeyError):
pass
async def is_mining(self, api_tunerstatus: dict = None) -> Optional[bool]:
if not api_tunerstatus:
async def is_mining(self, api_devdetails: dict = None) -> Optional[bool]:
if not api_devdetails:
try:
api_tunerstatus = await self.api.tunerstatus()
api_devdetails = await self.api.send_command("devdetails", ignore_errors=True, allow_warning=False)
except APIError:
pass
if api_tunerstatus:
if api_devdetails:
try:
running = any(
[
d["TunerRunning"]
for d in api_tunerstatus["TUNERSTATUS"][0]["TunerChainStatus"]
]
)
return running
return not api_devdetails["STATUS"][0]["Msg"] == "Unavailable"
except LookupError:
pass

View File

@@ -393,6 +393,6 @@ class CGMiner(BaseMiner):
if api_stats:
try:
return int(api_stats["STATS"][0]["Elapsed"])
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass

View File

@@ -248,7 +248,6 @@ class LUXMiner(BaseMiner):
pass
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
# get hr from API
if not api_summary:
try:
api_summary = await self.api.summary()
@@ -443,6 +442,6 @@ class LUXMiner(BaseMiner):
if api_stats:
try:
return int(api_stats["STATS"][0]["Elapsed"])
return int(api_stats["STATS"][1]["Elapsed"])
except LookupError:
pass

View File

@@ -17,6 +17,7 @@
from typing import Optional
from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.miners.backends.bmminer import BMMiner
from pyasic.web.vnish import VNishWebAPI
@@ -144,7 +145,7 @@ class VNish(BMMiner):
float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2
)
except (IndexError, KeyError, ValueError, TypeError) as e:
print(e)
logger.error(e)
pass
async def get_wattage_limit(self, web_settings: dict = None) -> Optional[int]:

View File

@@ -24,6 +24,7 @@ import asyncssh
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData
from pyasic.logger import logger
class BaseMiner(ABC):
@@ -71,6 +72,52 @@ class BaseMiner(ABC):
def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
@property
def pwd(self): # noqa - Skip PyCharm inspection
data = []
try:
if self.web is not None:
data.append(f"web={self.web.pwd}")
except TypeError:
pass
try:
if self.api is not None:
data.append(f"api={self.api.pwd}")
except TypeError:
pass
return ",".join(data)
@pwd.setter
def pwd(self, val):
try:
if self.web is not None:
self.web.pwd = val
except TypeError:
pass
try:
if self.api is not None:
self.api.pwd = val
except TypeError:
pass
@property
def username(self): # noqa - Skip PyCharm inspection
data = []
try:
if self.web is not None:
data.append(f"web={self.web.username}")
except TypeError:
pass
return ",".join(data)
@username.setter
def username(self, val):
try:
if self.web is not None:
self.web.username = val
except TypeError:
pass
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
@@ -397,7 +444,7 @@ class BaseMiner(ABC):
if fn_args[arg_name].get("web"):
web_multicommand.append(fn_args[arg_name]["web"])
except KeyError as e:
print(e, data_name)
logger.error(e, data_name)
continue
api_multicommand = list(set(api_multicommand))
@@ -426,23 +473,26 @@ class BaseMiner(ABC):
fn_args = self.data_locations[data_name]["kwargs"]
args_to_send = {k: None for k in fn_args}
for arg_name in fn_args:
if fn_args[arg_name].get("api"):
if api_command_data.get("multicommand"):
args_to_send[arg_name] = api_command_data[
fn_args[arg_name]["api"]
][0]
else:
args_to_send[arg_name] = api_command_data
if fn_args[arg_name].get("web"):
if web_command_data is not None:
if web_command_data.get("multicommand"):
args_to_send[arg_name] = web_command_data[
fn_args[arg_name]["web"]
]
try:
if fn_args[arg_name].get("api"):
if api_command_data.get("multicommand"):
args_to_send[arg_name] = api_command_data[
fn_args[arg_name]["api"]
][0]
else:
if not web_command_data == {"multicommand": False}:
args_to_send[arg_name] = web_command_data
except (KeyError, IndexError):
args_to_send[arg_name] = api_command_data
if fn_args[arg_name].get("web"):
if web_command_data is not None:
if web_command_data.get("multicommand"):
args_to_send[arg_name] = web_command_data[
fn_args[arg_name]["web"]
]
else:
if not web_command_data == {"multicommand": False}:
args_to_send[arg_name] = web_command_data
except LookupError:
args_to_send[arg_name] = None
except LookupError as e:
continue
function = getattr(self, self.data_locations[data_name]["cmd"])
@@ -457,8 +507,8 @@ class BaseMiner(ABC):
except KeyError:
pass
if len(pools_data) > 1:
miner_data["pool_2_url"] = pools_data[1]["pool_2_url"]
miner_data["pool_2_user"] = pools_data[1]["pool_2_user"]
miner_data["pool_2_url"] = pools_data[1]["pool_1_url"]
miner_data["pool_2_user"] = pools_data[1]["pool_1_user"]
miner_data[
"pool_split"
] = f"{pools_data[0]['quota']}/{pools_data[1]['quota']}"

View File

@@ -58,11 +58,13 @@ class BOSMinerWebAPI(BaseWebAPI):
command: dict,
) -> dict:
url = f"http://{self.ip}/graphql"
query = self.parse_command(command)
query = command
if command.get("query") is None:
query = {"query": self.parse_command(command)}
try:
async with httpx.AsyncClient() as client:
await self.auth(client)
data = await client.post(url, json={"query": query})
data = await client.post(url, json=query)
except httpx.HTTPError:
pass
else:
@@ -74,7 +76,7 @@ class BOSMinerWebAPI(BaseWebAPI):
async def multicommand(
self, *commands: Union[dict, str], allow_warning: bool = True
):
) -> dict:
luci_commands = []
gql_commands = []
for cmd in commands:
@@ -86,6 +88,11 @@ class BOSMinerWebAPI(BaseWebAPI):
luci_data = await self.luci_multicommand(*luci_commands)
gql_data = await self.gql_multicommand(*gql_commands)
if gql_data is None:
gql_data = {}
if luci_data is None:
luci_data = {}
data = dict(**luci_data, **gql_data)
return data
@@ -144,8 +151,6 @@ class BOSMinerWebAPI(BaseWebAPI):
data = await client.get(
f"http://{self.ip}{path}", headers={"User-Agent": "BTC Tools v0.1"}
)
print(data.status_code)
print(data.text)
if data.status_code == 200:
return data.json()
if ignore_errors:

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyasic"
version = "0.36.2"
version = "0.36.12"
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"