Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6482d04185 | ||
|
|
3b58b11501 | ||
|
|
7485b8ef77 | ||
|
|
d2bea227db | ||
|
|
1b7afaaf7e | ||
|
|
96898d639c | ||
|
|
eb439f4dcf | ||
|
|
69f4349393 | ||
|
|
e371bb577c | ||
|
|
2500ec3869 | ||
|
|
5be3187eec | ||
|
|
be1e9127b0 | ||
|
|
13572c4770 | ||
|
|
08fa3961fe | ||
|
|
b5d2809e9c | ||
|
|
aa538d3079 | ||
|
|
e1500bb75c | ||
|
|
7f00a65598 | ||
|
|
64c473a7d4 | ||
|
|
96d9fe8e6c | ||
|
|
0b27400d27 | ||
|
|
666b9dfe94 | ||
|
|
df3a080c9d | ||
|
|
bf3bd7c2b9 | ||
|
|
37fd60b462 | ||
|
|
2245904740 | ||
|
|
7b1b23016e | ||
|
|
b5fcd62e23 | ||
|
|
9057cde274 | ||
|
|
f6d35888fe | ||
|
|
f2abe9fd9e | ||
|
|
7d1a702804 | ||
|
|
65d1695ce4 | ||
|
|
65fd66b8bf | ||
|
|
5db52c46f3 | ||
|
|
d06cb19da3 | ||
|
|
4530d086da |
163
docs/generate_miners.py
Normal file
163
docs/generate_miners.py
Normal 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))
|
||||
@@ -126,6 +126,7 @@ These functions are
|
||||
[`restart_backend`](#restart-backend),
|
||||
[`stop_mining`](#stop-mining),
|
||||
[`resume_mining`](#resume-mining),
|
||||
[`is_mining`](#is-mining),
|
||||
[`send_config`](#send-config), and
|
||||
[`set_power_limit`](#set-power-limit).
|
||||
|
||||
@@ -227,6 +228,14 @@ These functions are
|
||||
|
||||
<br>
|
||||
|
||||
### Is Mining
|
||||
::: pyasic.miners.BaseMiner.is_mining
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Send Config
|
||||
::: pyasic.miners.BaseMiner.send_config
|
||||
handler: python
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
@@ -73,6 +75,11 @@ class BaseMinerAPI:
|
||||
# send the command
|
||||
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
|
||||
|
||||
if data == b"Socket connect failed: Connection refused\n":
|
||||
if not ignore_errors:
|
||||
raise APIError(data.decode("utf-8"))
|
||||
return {}
|
||||
|
||||
data = self._load_api_data(data)
|
||||
|
||||
# check for if the user wants to allow errors to return
|
||||
|
||||
759
pyasic/API/luxminer.py
Normal file
759
pyasic/API/luxminer.py
Normal file
@@ -0,0 +1,759 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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 typing import Literal
|
||||
|
||||
from pyasic.API import BaseMinerAPI
|
||||
|
||||
|
||||
class LUXMinerAPI(BaseMinerAPI):
|
||||
"""An abstraction of the LUXMiner API.
|
||||
|
||||
Each method corresponds to an API command in LUXMiner.
|
||||
|
||||
[LUXMiner API documentation](https://docs.firmware.luxor.tech/API/intro)
|
||||
|
||||
This class abstracts use of the LUXMiner API, as well as the
|
||||
methods for sending commands to it. The `self.send_command()`
|
||||
function handles sending a command to the miner asynchronously, and
|
||||
as such is the base for many of the functions in this class, which
|
||||
rely on it to send the command for them.
|
||||
|
||||
Parameters:
|
||||
ip: The IP of the miner to reference the API on.
|
||||
port: The port to reference the API on. Default is 4028.
|
||||
"""
|
||||
|
||||
def __init__(self, ip: str, api_ver: str = "0.0.0", port: int = 4028) -> None:
|
||||
super().__init__(ip, port=port)
|
||||
self.api_ver = api_ver
|
||||
|
||||
async def addgroup(self, name: str, quota: int) -> dict:
|
||||
"""Add a pool group.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
name: The group name.
|
||||
quota: The group quota.
|
||||
|
||||
Returns:
|
||||
Confirmation of adding a pool group.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("addgroup", parameters=f"{name},{quota}")
|
||||
|
||||
async def addpool(
|
||||
self, url: str, user: str, pwd: str = "", group_id: str = None
|
||||
) -> dict:
|
||||
"""Add a pool.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
url: The pool url.
|
||||
user: The pool username.
|
||||
pwd: The pool password.
|
||||
group_id: The group ID to use.
|
||||
|
||||
Returns:
|
||||
Confirmation of adding a pool.
|
||||
</details>
|
||||
"""
|
||||
pool_data = [url, user, pwd]
|
||||
if group_id is not None:
|
||||
pool_data.append(group_id)
|
||||
return await self.send_command("addpool", parameters=",".join(pool_data))
|
||||
|
||||
async def asc(self, n: int) -> dict:
|
||||
"""Get data for ASC device n.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
n: The device to get data for.
|
||||
|
||||
Returns:
|
||||
The data for ASC device n.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("asc", parameters=n)
|
||||
|
||||
async def asccount(self) -> dict:
|
||||
"""Get data on the number of ASC devices and their info.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data on all ASC devices.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("asccount")
|
||||
|
||||
async def check(self, command: str) -> dict:
|
||||
"""Check if the command `command` exists in LUXMiner.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
command: The command to check.
|
||||
|
||||
Returns:
|
||||
## Information about a command:
|
||||
* Exists (Y/N) <- the command exists in this version
|
||||
* Access (Y/N) <- you have access to use the command
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("check", parameters=command)
|
||||
|
||||
async def coin(self) -> dict:
|
||||
"""Get information on the current coin.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
## Information about the current coin being mined:
|
||||
* Hash Method <- the hashing algorithm
|
||||
* Current Block Time <- blocktime as a float, 0 means none
|
||||
* Current Block Hash <- the hash of the current block, blank means none
|
||||
* LP <- whether LP is in use on at least 1 pool
|
||||
* Network Difficulty: the current network difficulty
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("coin")
|
||||
|
||||
async def config(self) -> dict:
|
||||
"""Get some basic configuration info.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Miner configuration information.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("config")
|
||||
|
||||
async def curtail(self, session_id: str) -> dict:
|
||||
"""Put the miner into sleep mode. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
|
||||
Returns:
|
||||
A confirmation of putting the miner to sleep.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("curtail", parameters=session_id)
|
||||
|
||||
async def devdetails(self) -> dict:
|
||||
"""Get data on all devices with their static details.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data on all devices with their static details.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("devdetails")
|
||||
|
||||
async def devs(self) -> dict:
|
||||
"""Get data on each PGA/ASC with their details.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data on each PGA/ASC with their details.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("devs")
|
||||
|
||||
async def disablepool(self, n: int) -> dict:
|
||||
"""Disable a pool.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
n: Pool to disable.
|
||||
|
||||
Returns:
|
||||
A confirmation of diabling the pool.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("disablepool", parameters=n)
|
||||
|
||||
async def edevs(self) -> dict:
|
||||
"""Alias for devs"""
|
||||
return await self.devs()
|
||||
|
||||
async def enablepool(self, n: int) -> dict:
|
||||
"""Enable pool n.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
n: The pool to enable.
|
||||
|
||||
Returns:
|
||||
A confirmation of enabling pool n.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("enablepool", parameters=n)
|
||||
|
||||
async def estats(self) -> dict:
|
||||
"""Alias for stats"""
|
||||
return await self.stats()
|
||||
|
||||
async def fans(self) -> dict:
|
||||
"""Get fan data.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data on the fans of the miner.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("fans")
|
||||
|
||||
async def fanset(self, session_id: str, speed: int, min_fans: int = None) -> dict:
|
||||
"""Set fan control. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
speed: The fan speed to set. Use -1 to set automatically.
|
||||
min_fans: The minimum number of fans to use. Optional.
|
||||
|
||||
Returns:
|
||||
A confirmation of setting fan control values.
|
||||
</details>
|
||||
"""
|
||||
fanset_data = [str(session_id), str(speed)]
|
||||
if min_fans is not None:
|
||||
fanset_data.append(str(min_fans))
|
||||
return await self.send_command("fanset", parameters=",".join(fanset_data))
|
||||
|
||||
async def frequencyget(self, board_n: int, chip_n: int = None) -> dict:
|
||||
"""Get frequency data for a board and chips.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
board_n: The board number to get frequency info from.
|
||||
chip_n: The chip number to get frequency info from. Optional.
|
||||
|
||||
Returns:
|
||||
Board and/or chip frequency values.
|
||||
</details>
|
||||
"""
|
||||
frequencyget_data = [str(board_n)]
|
||||
if chip_n is not None:
|
||||
frequencyget_data.append(str(chip_n))
|
||||
return await self.send_command(
|
||||
"frequencyget", parameters=",".join(frequencyget_data)
|
||||
)
|
||||
|
||||
async def frequencyset(self, session_id: str, board_n: int, freq: int) -> dict:
|
||||
"""Set frequency. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
board_n: The board number to set frequency on.
|
||||
freq: The frequency to set.
|
||||
|
||||
Returns:
|
||||
A confirmation of setting frequency values.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command(
|
||||
"frequencyset", parameters=f"{session_id},{board_n},{freq}"
|
||||
)
|
||||
|
||||
async def frequencystop(self, session_id: str, board_n: int) -> dict:
|
||||
"""Stop set frequency. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
board_n: The board number to set frequency on.
|
||||
|
||||
Returns:
|
||||
A confirmation of stopping frequencyset value.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command(
|
||||
"frequencystop", parameters=f"{session_id},{board_n}"
|
||||
)
|
||||
|
||||
async def groupquota(self, group_n: int, quota: int) -> dict:
|
||||
"""Set a group's quota.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
group_n: The group number to set quota on.
|
||||
quota: The quota to use.
|
||||
|
||||
Returns:
|
||||
A confirmation of setting quota value.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("groupquota", parameters=f"{group_n},{quota}")
|
||||
|
||||
async def groups(self) -> dict:
|
||||
"""Get pool group data.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data on the pool groups on the miner.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("groups")
|
||||
|
||||
async def healthchipget(self, board_n: int, chip_n: int = None) -> dict:
|
||||
"""Get chip health.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
board_n: The board number to get chip health of.
|
||||
chip_n: The chip number to get chip health of. Optional.
|
||||
|
||||
Returns:
|
||||
Chip health data.
|
||||
</details>
|
||||
"""
|
||||
healthchipget_data = [str(board_n)]
|
||||
if chip_n is not None:
|
||||
healthchipget_data.append(str(chip_n))
|
||||
return await self.send_command(
|
||||
"healthchipget", parameters=",".join(healthchipget_data)
|
||||
)
|
||||
|
||||
async def healthchipset(
|
||||
self, session_id: str, board_n: int, chip_n: int = None
|
||||
) -> dict:
|
||||
"""Select the next chip to have its health checked. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
board_n: The board number to next get chip health of.
|
||||
chip_n: The chip number to next get chip health of. Optional.
|
||||
|
||||
Returns:
|
||||
Confirmation of selecting the next health check chip.
|
||||
</details>
|
||||
"""
|
||||
healthchipset_data = [session_id, str(board_n)]
|
||||
if chip_n is not None:
|
||||
healthchipset_data.append(str(chip_n))
|
||||
return await self.send_command(
|
||||
"healthchipset", parameters=",".join(healthchipset_data)
|
||||
)
|
||||
|
||||
async def healthctrl(self) -> dict:
|
||||
"""Get health check config.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Health check config.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("healthctrl")
|
||||
|
||||
async def healthctrlset(
|
||||
self, session_id: str, num_readings: int, amplified_factor: float
|
||||
) -> dict:
|
||||
"""Set health control config. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
num_readings: The minimum number of readings for evaluation.
|
||||
amplified_factor: Performance factor of the evaluation.
|
||||
|
||||
Returns:
|
||||
A confirmation of setting health control config.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command(
|
||||
"healthctrlset",
|
||||
parameters=f"{session_id},{num_readings},{amplified_factor}",
|
||||
)
|
||||
|
||||
async def kill(self) -> dict:
|
||||
"""Forced session kill. Use logoff instead.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
A confirmation of killing the active session.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("kill")
|
||||
|
||||
async def lcd(self) -> dict:
|
||||
"""Get a general all-in-one status summary of the miner. Always zeros on LUXMiner.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
An all-in-one status summary of the miner.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("lcd")
|
||||
|
||||
async def ledset(
|
||||
self,
|
||||
session_id: str,
|
||||
color: Literal["red"],
|
||||
state: Literal["on", "off", "blink"],
|
||||
) -> dict:
|
||||
"""Set led. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
color: The color LED to set. Can be "red".
|
||||
state: The state to set the LED to. Can be "on", "off", or "blink".
|
||||
|
||||
Returns:
|
||||
A confirmation of setting LED.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command(
|
||||
"ledset", parameters=f"{session_id},{color},{state}"
|
||||
)
|
||||
|
||||
async def limits(self) -> dict:
|
||||
"""Get max and min values of config parameters.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data on max and min values of config parameters.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("limits")
|
||||
|
||||
async def logoff(self, session_id: str) -> dict:
|
||||
"""Log off of a session. Requires a session id from an active session.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
|
||||
Returns:
|
||||
Confirmation of logging off a session.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("logoff", parameters=session_id)
|
||||
|
||||
async def logon(self) -> dict:
|
||||
"""Get or create a session.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
The Session ID to be used.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("logon")
|
||||
|
||||
async def pools(self) -> dict:
|
||||
"""Get pool information.
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Miner pool information.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("pools")
|
||||
|
||||
async def power(self) -> dict:
|
||||
"""Get the estimated power usage in watts.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Estimated power usage in watts.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("power")
|
||||
|
||||
async def profiles(self) -> dict:
|
||||
"""Get the available profiles.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data on available profiles.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("profiles")
|
||||
|
||||
async def profileset(self, session_id: str, board_n: int, profile: str) -> dict:
|
||||
"""Set active profile for a board. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
board_n: The board to set the profile on.
|
||||
profile: The profile name to use.
|
||||
|
||||
Returns:
|
||||
A confirmation of setting the profile on board_n.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command(
|
||||
"profileset", parameters=f"{session_id},{board_n},{profile}"
|
||||
)
|
||||
|
||||
async def reboot(self, session_id: str, board_n: int, delay_s: int = None) -> dict:
|
||||
"""Reboot a board. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
board_n: The board to reboot.
|
||||
delay_s: The number of seconds to delay until startup. If it is 0, the board will just stop. Optional.
|
||||
|
||||
Returns:
|
||||
A confirmation of rebooting board_n.
|
||||
</details>
|
||||
"""
|
||||
reboot_data = [session_id, str(board_n)]
|
||||
if delay_s is not None:
|
||||
reboot_data.append(str(delay_s))
|
||||
return await self.send_command("reboot", parameters=",".join(reboot_data))
|
||||
|
||||
async def rebootdevice(self, session_id: str) -> dict:
|
||||
"""Reboot the miner. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
|
||||
Returns:
|
||||
A confirmation of rebooting the miner.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("rebootdevice", parameters=session_id)
|
||||
|
||||
async def removegroup(self, group_id: str) -> dict:
|
||||
"""Remove a pool group.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
group_id: Group id to remove.
|
||||
|
||||
Returns:
|
||||
A confirmation of removing the pool group.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("removegroup", parameters=group_id)
|
||||
|
||||
async def resetminer(self, session_id: str) -> dict:
|
||||
"""Restart the mining process. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
|
||||
Returns:
|
||||
A confirmation of restarting the mining process.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("resetminer", parameters=session_id)
|
||||
|
||||
async def removepool(self, pool_id: int) -> dict:
|
||||
"""Remove a pool.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
pool_id: Pool to remove.
|
||||
|
||||
Returns:
|
||||
A confirmation of removing the pool.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("removepool", parameters=str(pool_id))
|
||||
|
||||
async def session(self) -> dict:
|
||||
"""Get the current session.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data on the current session.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("session")
|
||||
|
||||
async def tempctrlset(self, target: int, hot: int, dangerous: int) -> dict:
|
||||
"""Set temp control values.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
target: Target temp.
|
||||
hot: Hot temp.
|
||||
dangerous: Dangerous temp.
|
||||
|
||||
Returns:
|
||||
A confirmation of setting the temp control config.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command(
|
||||
"tempctrlset", parameters=f"{target},{hot},{dangerous}"
|
||||
)
|
||||
|
||||
async def stats(self) -> dict:
|
||||
"""Get stats of each device/pool with more than 1 getwork.
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Stats of each device/pool with more than 1 getwork.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("stats")
|
||||
|
||||
async def summary(self) -> dict:
|
||||
"""Get the status summary of the miner.
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
The status summary of the miner.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("summary")
|
||||
|
||||
async def switchpool(self, pool_id: int) -> dict:
|
||||
"""Switch to a pool.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
pool_id: Pool to switch to.
|
||||
|
||||
Returns:
|
||||
A confirmation of switching to the pool.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("switchpool", parameters=str(pool_id))
|
||||
|
||||
async def tempctrl(self) -> dict:
|
||||
"""Get temperature control data.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data about the temp control settings of the miner.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("tempctrl")
|
||||
|
||||
async def temps(self) -> dict:
|
||||
"""Get temperature data.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Data on the temps of the miner.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("temps")
|
||||
|
||||
async def version(self) -> dict:
|
||||
"""Get miner version info.
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Returns:
|
||||
Miner version information.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("version")
|
||||
|
||||
async def voltageget(self, board_n: int) -> dict:
|
||||
"""Get voltage data for a board.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
board_n: The board number to get voltage info from.
|
||||
|
||||
Returns:
|
||||
Board voltage values.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("frequencyget", parameters=str(board_n))
|
||||
|
||||
async def voltageset(self, session_id: str, board_n: int, voltage: float) -> dict:
|
||||
"""Set voltage values.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
board_n: The board to set the voltage on.
|
||||
voltage: The voltage to use.
|
||||
|
||||
Returns:
|
||||
A confirmation of setting the voltage.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command(
|
||||
"voltageset", parameters=f"{session_id},{board_n},{voltage}"
|
||||
)
|
||||
|
||||
async def wakeup(self, session_id: str) -> dict:
|
||||
"""Take the miner out of sleep mode. Requires a session_id from logon.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
Parameters:
|
||||
session_id: Session id from the logon command.
|
||||
|
||||
Returns:
|
||||
A confirmation of resuming mining.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("wakeup", parameters=session_id)
|
||||
@@ -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:
|
||||
|
||||
@@ -66,6 +66,7 @@ class MinerData:
|
||||
Attributes:
|
||||
ip: The IP of the miner as a str.
|
||||
datetime: The time and date this data was generated.
|
||||
uptime: The uptime of the miner in seconds.
|
||||
mac: The MAC address of the miner as a str.
|
||||
model: The model of the miner as a str.
|
||||
make: The make of the miner as a str.
|
||||
@@ -96,10 +97,12 @@ class MinerData:
|
||||
errors: A list of errors on the miner.
|
||||
fault_light: Whether the fault light is on as a boolean.
|
||||
efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically.
|
||||
is_mining: Whether the miner is mining.
|
||||
"""
|
||||
|
||||
ip: str
|
||||
datetime: datetime = None
|
||||
uptime: int = 0
|
||||
mac: str = "00:00:00:00:00:00"
|
||||
model: str = "Unknown"
|
||||
make: str = "Unknown"
|
||||
@@ -133,6 +136,7 @@ class MinerData:
|
||||
] = field(default_factory=list)
|
||||
fault_light: Union[bool, None] = None
|
||||
efficiency: int = field(init=False)
|
||||
is_mining: bool = True
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
|
||||
@@ -18,4 +18,5 @@ from .bmminer import *
|
||||
from .bosminer import *
|
||||
from .cgminer import *
|
||||
from .hiveon import *
|
||||
from .luxos import *
|
||||
from .vnish import *
|
||||
|
||||
22
pyasic/miners/antminer/luxos/X9/S9.py
Normal file
22
pyasic/miners/antminer/luxos/X9/S9.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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 LUXMiner
|
||||
from pyasic.miners.types import S9
|
||||
|
||||
|
||||
class LUXMinerS9(LUXMiner, S9):
|
||||
pass
|
||||
17
pyasic/miners/antminer/luxos/X9/__init__.py
Normal file
17
pyasic/miners/antminer/luxos/X9/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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 .S9 import LUXMinerS9
|
||||
17
pyasic/miners/antminer/luxos/__init__.py
Normal file
17
pyasic/miners/antminer/luxos/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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 .X9 import *
|
||||
@@ -15,13 +15,26 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pyasic.miners.backends import VNish
|
||||
from pyasic.miners.types import S19, S19XP, S19a, S19aPro, S19j, S19jPro, S19Pro
|
||||
from pyasic.miners.types import (
|
||||
S19,
|
||||
S19XP,
|
||||
S19a,
|
||||
S19aPro,
|
||||
S19j,
|
||||
S19jPro,
|
||||
S19NoPIC,
|
||||
S19Pro,
|
||||
)
|
||||
|
||||
|
||||
class VNishS19(VNish, S19):
|
||||
pass
|
||||
|
||||
|
||||
class VNishS19NoPIC(VNish, S19NoPIC):
|
||||
pass
|
||||
|
||||
|
||||
class VNishS19Pro(VNish, S19Pro):
|
||||
pass
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from .S19 import (
|
||||
VNishS19aPro,
|
||||
VNishS19j,
|
||||
VNishS19jPro,
|
||||
VNishS19NoPIC,
|
||||
VNishS19Pro,
|
||||
VNishS19XP,
|
||||
)
|
||||
|
||||
@@ -22,5 +22,6 @@ from .btminer import BTMiner
|
||||
from .cgminer import CGMiner
|
||||
from .cgminer_avalon import CGMinerAvalon
|
||||
from .hiveon import Hiveon
|
||||
from .luxminer import LUXMiner
|
||||
from .vnish import VNish
|
||||
from .whatsminer import M2X, M3X, M5X
|
||||
|
||||
@@ -45,6 +45,14 @@ ANTMINER_MODERN_DATA_LOC = {
|
||||
"errors": {"cmd": "get_errors", "kwargs": {}},
|
||||
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {
|
||||
"cmd": "is_mining",
|
||||
"kwargs": {"web_get_conf": {"web": "get_miner_conf"}},
|
||||
},
|
||||
"uptime": {
|
||||
"cmd": "get_uptime",
|
||||
"kwargs": {"api_stats": {"api": "stats"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +235,32 @@ class AntminerModern(BMMiner):
|
||||
protocol=protocol,
|
||||
)
|
||||
|
||||
async def is_mining(self, web_get_conf: dict = None) -> Optional[bool]:
|
||||
if not web_get_conf:
|
||||
try:
|
||||
web_get_conf = await self.web.get_miner_conf()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if web_get_conf:
|
||||
try:
|
||||
return False if int(web_get_conf["bitmain-work-mode"]) == 1 else True
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
|
||||
if not api_stats:
|
||||
try:
|
||||
api_stats = await self.api.stats()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_stats:
|
||||
try:
|
||||
return int(api_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
|
||||
ANTMINER_OLD_DATA_LOC = {
|
||||
"mac": {"cmd": "get_mac", "kwargs": {}},
|
||||
@@ -257,6 +291,14 @@ ANTMINER_OLD_DATA_LOC = {
|
||||
"kwargs": {"web_get_blink_status": {"web": "get_blink_status"}},
|
||||
},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {
|
||||
"cmd": "is_mining",
|
||||
"kwargs": {"web_get_conf": {"web": "get_miner_conf"}},
|
||||
},
|
||||
"uptime": {
|
||||
"cmd": "get_uptime",
|
||||
"kwargs": {"api_stats": {"api": "stats"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -425,3 +467,41 @@ class AntminerOld(CGMiner):
|
||||
pass
|
||||
|
||||
return hashboards
|
||||
|
||||
async def is_mining(self, web_get_conf: dict = None) -> Optional[bool]:
|
||||
if not web_get_conf:
|
||||
try:
|
||||
web_get_conf = await self.web.get_miner_conf()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if web_get_conf:
|
||||
try:
|
||||
return False if int(web_get_conf["bitmain-work-mode"]) == 1 else True
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
api_summary = None
|
||||
try:
|
||||
api_summary = await self.api.summary()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_summary is not None:
|
||||
if not api_summary == {}:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
|
||||
if not api_stats:
|
||||
try:
|
||||
api_stats = await self.api.stats()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_stats:
|
||||
try:
|
||||
return int(api_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
@@ -44,6 +44,8 @@ BFGMINER_DATA_LOC = {
|
||||
"errors": {"cmd": "get_errors", "kwargs": {}},
|
||||
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {"cmd": "is_mining", "kwargs": {}},
|
||||
"uptime": {"cmd": "get_uptime", "kwargs": {}},
|
||||
}
|
||||
|
||||
|
||||
@@ -318,3 +320,9 @@ class BFGMiner(BaseMiner):
|
||||
return round(ideal_rate, 2)
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
|
||||
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
return None
|
||||
|
||||
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
|
||||
return None
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from typing import List
|
||||
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
|
||||
|
||||
@@ -47,6 +48,8 @@ GOLDSHELL_DATA_LOC = {
|
||||
"errors": {"cmd": "get_errors", "kwargs": {}},
|
||||
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {"cmd": "is_mining", "kwargs": {}},
|
||||
"uptime": {"cmd": "get_uptime", "kwargs": {}},
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +139,7 @@ class BFGMinerGoldshell(BFGMiner):
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
print(self, api_devs)
|
||||
logger.error(self, api_devs)
|
||||
|
||||
if not api_devdetails:
|
||||
try:
|
||||
@@ -154,6 +157,12 @@ class BFGMinerGoldshell(BFGMiner):
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
print(self, api_devdetails)
|
||||
logger.error(self, api_devdetails)
|
||||
|
||||
return hashboards
|
||||
|
||||
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
return None
|
||||
|
||||
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
|
||||
return None
|
||||
|
||||
@@ -45,6 +45,11 @@ BMMINER_DATA_LOC = {
|
||||
"errors": {"cmd": "get_errors", "kwargs": {}},
|
||||
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {"cmd": "is_mining", "kwargs": {}},
|
||||
"uptime": {
|
||||
"cmd": "get_uptime",
|
||||
"kwargs": {"api_stats": {"api": "stats"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -352,3 +357,19 @@ class BMMiner(BaseMiner):
|
||||
return round(ideal_rate, 2)
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
|
||||
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
return None
|
||||
|
||||
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
|
||||
if not api_stats:
|
||||
try:
|
||||
api_stats = await self.web.get_miner_conf()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_stats:
|
||||
try:
|
||||
return int(api_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from typing import List, Optional, Tuple, Union
|
||||
@@ -29,7 +29,12 @@ from pyasic.miners.base import BaseMiner
|
||||
from pyasic.web.bosminer import BOSMinerWebAPI
|
||||
|
||||
BOSMINER_DATA_LOC = {
|
||||
"mac": {"cmd": "get_mac", "kwargs": {}},
|
||||
"mac": {
|
||||
"cmd": "get_mac",
|
||||
"kwargs": {
|
||||
"web_net_conf": {"web": "/cgi-bin/luci/admin/network/iface_status/lan"}
|
||||
},
|
||||
},
|
||||
"model": {"cmd": "get_model", "kwargs": {}},
|
||||
"api_ver": {
|
||||
"cmd": "get_api_ver",
|
||||
@@ -167,6 +172,14 @@ BOSMINER_DATA_LOC = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"is_mining": {
|
||||
"cmd": "is_mining",
|
||||
"kwargs": {"api_devdetails": {"api": "devdetails"}},
|
||||
},
|
||||
"uptime": {
|
||||
"cmd": "get_uptime",
|
||||
"kwargs": {"api_summary": {"api": "summary"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -192,8 +205,8 @@ class BOSMiner(BaseMiner):
|
||||
result = None
|
||||
|
||||
try:
|
||||
conn = await self._get_ssh_connection()
|
||||
except ConnectionError:
|
||||
conn = await asyncio.wait_for(self._get_ssh_connection(), timeout=10)
|
||||
except (ConnectionError, asyncio.TimeoutError):
|
||||
return None
|
||||
|
||||
# open an ssh connection
|
||||
@@ -360,14 +373,80 @@ 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}) ###
|
||||
##################################################
|
||||
|
||||
async def get_mac(self) -> Optional[str]:
|
||||
result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
|
||||
if result:
|
||||
return result.upper().strip()
|
||||
async def get_mac(self, web_net_conf: Union[dict, list] = None) -> Optional[str]:
|
||||
if not web_net_conf:
|
||||
try:
|
||||
web_net_conf = await self.web.send_command(
|
||||
"/cgi-bin/luci/admin/network/iface_status/lan"
|
||||
)
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
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[
|
||||
"/cgi-bin/luci/admin/network/iface_status/lan"
|
||||
]
|
||||
|
||||
if web_net_conf:
|
||||
try:
|
||||
return web_net_conf[0]["macaddr"]
|
||||
except LookupError:
|
||||
pass
|
||||
# could use ssh, but its slow and buggy
|
||||
# result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
|
||||
# if result:
|
||||
# return result.upper().strip()
|
||||
|
||||
async def get_model(self) -> Optional[str]:
|
||||
if self.model is not None:
|
||||
@@ -415,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:
|
||||
@@ -444,7 +523,7 @@ class BOSMiner(BaseMiner):
|
||||
try:
|
||||
hostname = graphql_hostname["data"]["bos"]["hostname"]
|
||||
return hostname
|
||||
except KeyError:
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
|
||||
try:
|
||||
@@ -484,7 +563,7 @@ class BOSMiner(BaseMiner):
|
||||
),
|
||||
2,
|
||||
)
|
||||
except (KeyError, IndexError, ValueError):
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# get hr from API
|
||||
@@ -538,7 +617,7 @@ class BOSMiner(BaseMiner):
|
||||
boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][
|
||||
"childSolvers"
|
||||
]
|
||||
except (KeyError, IndexError):
|
||||
except (TypeError, LookupError):
|
||||
boards = None
|
||||
|
||||
if boards:
|
||||
@@ -653,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:
|
||||
@@ -686,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:
|
||||
@@ -722,7 +801,7 @@ class BOSMiner(BaseMiner):
|
||||
]
|
||||
)
|
||||
)
|
||||
except KeyError:
|
||||
except (LookupError, TypeError):
|
||||
pass
|
||||
return fans
|
||||
|
||||
@@ -862,7 +941,7 @@ class BOSMiner(BaseMiner):
|
||||
boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][
|
||||
"childSolvers"
|
||||
]
|
||||
except (KeyError, IndexError):
|
||||
except (LookupError, TypeError):
|
||||
boards = None
|
||||
|
||||
if boards:
|
||||
@@ -955,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:
|
||||
@@ -992,3 +1074,29 @@ class BOSMiner(BaseMiner):
|
||||
)
|
||||
except (IndexError, KeyError):
|
||||
pass
|
||||
|
||||
async def is_mining(self, api_devdetails: dict = None) -> Optional[bool]:
|
||||
if not api_devdetails:
|
||||
try:
|
||||
api_devdetails = await self.api.send_command("devdetails", ignore_errors=True, allow_warning=False)
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_devdetails:
|
||||
try:
|
||||
return not api_devdetails["STATUS"][0]["Msg"] == "Unavailable"
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
async def get_uptime(self, api_summary: dict = None) -> Optional[int]:
|
||||
if not api_summary:
|
||||
try:
|
||||
api_summary = await self.api.summary()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_summary:
|
||||
try:
|
||||
return int(api_summary["SUMMARY"][0]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
@@ -150,3 +150,9 @@ class BOSMinerOld(BOSMiner):
|
||||
|
||||
async def get_data(self, allow_warning: bool = False, **kwargs) -> MinerData:
|
||||
return MinerData(ip=str(self.ip))
|
||||
|
||||
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
return None
|
||||
|
||||
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
|
||||
return None
|
||||
|
||||
@@ -88,6 +88,11 @@ BTMINER_DATA_LOC = {
|
||||
"kwargs": {"api_get_miner_info": {"api": "get_miner_info"}},
|
||||
},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {"cmd": "is_mining", "kwargs": {"api_status": {"api": "status"}}},
|
||||
"uptime": {
|
||||
"cmd": "get_uptime",
|
||||
"kwargs": {"api_summary": {"api": "summary"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -616,3 +621,35 @@ class BTMiner(BaseMiner):
|
||||
|
||||
async def set_hostname(self, hostname: str):
|
||||
await self.api.set_hostname(hostname)
|
||||
|
||||
async def is_mining(self, api_status: dict = None) -> Optional[bool]:
|
||||
if not api_status:
|
||||
try:
|
||||
api_status = await self.api.status()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_status:
|
||||
try:
|
||||
if api_status["Msg"].get("btmineroff"):
|
||||
try:
|
||||
await self.api.devdetails()
|
||||
except APIError:
|
||||
return False
|
||||
return True
|
||||
return True if api_status["Msg"]["mineroff"] == "false" else False
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
async def get_uptime(self, api_summary: dict = None) -> Optional[int]:
|
||||
if not api_summary:
|
||||
try:
|
||||
api_summary = await self.api.summary()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_summary:
|
||||
try:
|
||||
return int(api_summary["SUMMARY"][0]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
@@ -45,6 +45,11 @@ CGMINER_DATA_LOC = {
|
||||
"errors": {"cmd": "get_errors", "kwargs": {}},
|
||||
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {"cmd": "is_mining", "kwargs": {}},
|
||||
"uptime": {
|
||||
"cmd": "get_uptime",
|
||||
"kwargs": {"api_stats": {"api": "stats"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -375,3 +380,19 @@ class CGMiner(BaseMiner):
|
||||
return round(ideal_rate, 2)
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
|
||||
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
return None
|
||||
|
||||
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
|
||||
if not api_stats:
|
||||
try:
|
||||
api_stats = await self.api.stats()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_stats:
|
||||
try:
|
||||
return int(api_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
@@ -50,6 +50,8 @@ AVALON_DATA_LOC = {
|
||||
"kwargs": {"api_stats": {"api": "stats"}},
|
||||
},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {"cmd": "is_mining", "kwargs": {}},
|
||||
"uptime": {"cmd": "get_uptime", "kwargs": {}},
|
||||
}
|
||||
|
||||
|
||||
@@ -371,3 +373,6 @@ class CGMinerAvalon(CGMiner):
|
||||
except LookupError:
|
||||
pass
|
||||
return False
|
||||
|
||||
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
return None
|
||||
|
||||
447
pyasic/miners/backends/luxminer.py
Normal file
447
pyasic/miners/backends/luxminer.py
Normal file
@@ -0,0 +1,447 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright 2022 Upstream Data Inc -
|
||||
# -
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); -
|
||||
# you may not use this file except in compliance with the License. -
|
||||
# You may obtain a copy of the License at -
|
||||
# -
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 -
|
||||
# -
|
||||
# Unless required by applicable law or agreed to in writing, software -
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, -
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
import toml
|
||||
|
||||
from pyasic.API.bosminer import BOSMinerAPI
|
||||
from pyasic.API.luxminer import LUXMinerAPI
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.web.bosminer import BOSMinerWebAPI
|
||||
|
||||
LUXMINER_DATA_LOC = {
|
||||
"mac": {
|
||||
"cmd": "get_mac",
|
||||
"kwargs": {"api_config": {"api": "config"}},
|
||||
},
|
||||
"model": {"cmd": "get_model", "kwargs": {}},
|
||||
"api_ver": {
|
||||
"cmd": "get_api_ver",
|
||||
"kwargs": {},
|
||||
},
|
||||
"fw_ver": {
|
||||
"cmd": "get_fw_ver",
|
||||
"kwargs": {},
|
||||
},
|
||||
"hostname": {
|
||||
"cmd": "get_hostname",
|
||||
"kwargs": {},
|
||||
},
|
||||
"hashrate": {
|
||||
"cmd": "get_hashrate",
|
||||
"kwargs": {},
|
||||
},
|
||||
"nominal_hashrate": {
|
||||
"cmd": "get_nominal_hashrate",
|
||||
"kwargs": {},
|
||||
},
|
||||
"hashboards": {
|
||||
"cmd": "get_hashboards",
|
||||
"kwargs": {},
|
||||
},
|
||||
"wattage": {
|
||||
"cmd": "get_wattage",
|
||||
"kwargs": {},
|
||||
},
|
||||
"wattage_limit": {
|
||||
"cmd": "get_wattage_limit",
|
||||
"kwargs": {},
|
||||
},
|
||||
"fans": {
|
||||
"cmd": "get_fans",
|
||||
"kwargs": {},
|
||||
},
|
||||
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
|
||||
"env_temp": {"cmd": "get_env_temp", "kwargs": {}},
|
||||
"errors": {
|
||||
"cmd": "get_errors",
|
||||
"kwargs": {},
|
||||
},
|
||||
"fault_light": {
|
||||
"cmd": "get_fault_light",
|
||||
"kwargs": {},
|
||||
},
|
||||
"pools": {
|
||||
"cmd": "get_pools",
|
||||
"kwargs": {},
|
||||
},
|
||||
"is_mining": {
|
||||
"cmd": "is_mining",
|
||||
"kwargs": {},
|
||||
},
|
||||
"uptime": {
|
||||
"cmd": "get_uptime",
|
||||
"kwargs": {"api_stats": {"api": "stats"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class LUXMiner(BaseMiner):
|
||||
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
|
||||
super().__init__(ip)
|
||||
# interfaces
|
||||
self.api = LUXMinerAPI(ip, api_ver)
|
||||
# self.web = BOSMinerWebAPI(ip)
|
||||
|
||||
# static data
|
||||
self.api_type = "LUXMiner"
|
||||
# data gathering locations
|
||||
self.data_locations = LUXMINER_DATA_LOC
|
||||
# autotuning/shutdown support
|
||||
# self.supports_autotuning = True
|
||||
# self.supports_shutdown = True
|
||||
|
||||
# data storage
|
||||
self.api_ver = api_ver
|
||||
|
||||
async def _get_session(self) -> Optional[str]:
|
||||
try:
|
||||
data = await self.api.session()
|
||||
if not data["SESSION"][0]["SessionID"] == "":
|
||||
return data["SESSION"][0]["SessionID"]
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
try:
|
||||
data = await self.api.logon()
|
||||
return data["SESSION"][0]["SessionID"]
|
||||
except (LookupError, APIError):
|
||||
return
|
||||
|
||||
async def fault_light_on(self) -> bool:
|
||||
"""Sends command to turn on fault light on the miner."""
|
||||
try:
|
||||
session_id = await self._get_session()
|
||||
if session_id:
|
||||
await self.api.ledset(session_id, "red", "blink")
|
||||
return True
|
||||
except (APIError, LookupError):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
"""Sends command to turn off fault light on the miner."""
|
||||
try:
|
||||
session_id = await self._get_session()
|
||||
if session_id:
|
||||
await self.api.ledset(session_id, "red", "off")
|
||||
return True
|
||||
except (APIError, LookupError):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def restart_backend(self) -> bool:
|
||||
"""Restart luxminer hashing process. Wraps [`restart_luxminer`][pyasic.miners.backends.luxminer.LUXMiner.restart_luxminer] to standardize."""
|
||||
return await self.restart_luxminer()
|
||||
|
||||
async def restart_luxminer(self) -> bool:
|
||||
"""Restart luxminer hashing process."""
|
||||
try:
|
||||
session_id = await self._get_session()
|
||||
if session_id:
|
||||
await self.api.resetminer(session_id)
|
||||
return True
|
||||
except (APIError, LookupError):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
try:
|
||||
session_id = await self._get_session()
|
||||
if session_id:
|
||||
await self.api.curtail(session_id)
|
||||
return True
|
||||
except (APIError, LookupError):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
try:
|
||||
session_id = await self._get_session()
|
||||
if session_id:
|
||||
await self.api.wakeup(session_id)
|
||||
return True
|
||||
except (APIError, LookupError):
|
||||
pass
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
"""Reboots power to the physical miner."""
|
||||
try:
|
||||
session_id = await self._get_session()
|
||||
if session_id:
|
||||
await self.api.rebootdevice(session_id)
|
||||
return True
|
||||
except (APIError, LookupError):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def get_config(self) -> MinerConfig:
|
||||
"""Gets the config for the miner and sets it as `self.config`.
|
||||
|
||||
Returns:
|
||||
The config from `self.config`.
|
||||
"""
|
||||
return self.config
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
"""Configures miner with yaml config."""
|
||||
pass
|
||||
|
||||
async def set_power_limit(self, wattage: int) -> bool:
|
||||
return False
|
||||
|
||||
##################################################
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def get_mac(self, api_config: dict = None) -> Optional[str]:
|
||||
mac = None
|
||||
if not api_config:
|
||||
try:
|
||||
api_config = await self.api.config()
|
||||
except APIError:
|
||||
return None
|
||||
|
||||
if api_config:
|
||||
try:
|
||||
mac = api_config["CONFIG"][0]["MACAddr"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
return mac
|
||||
|
||||
async def get_model(self) -> Optional[str]:
|
||||
if self.model is not None:
|
||||
return self.model + " (LuxOS)"
|
||||
return "? (LuxOS)"
|
||||
|
||||
async def get_version(self) -> Tuple[Optional[str], Optional[str]]:
|
||||
pass
|
||||
|
||||
async def get_api_ver(self) -> Optional[str]:
|
||||
pass
|
||||
|
||||
async def get_fw_ver(self) -> Optional[str]:
|
||||
pass
|
||||
|
||||
async def get_hostname(self) -> Union[str, None]:
|
||||
pass
|
||||
|
||||
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
|
||||
if not api_summary:
|
||||
try:
|
||||
api_summary = await self.api.summary()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_summary:
|
||||
try:
|
||||
return round(float(api_summary["SUMMARY"][0]["GHS 5s"] / 1000), 2)
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
|
||||
hashboards = []
|
||||
|
||||
if not api_stats:
|
||||
try:
|
||||
api_stats = await self.api.stats()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_stats:
|
||||
try:
|
||||
board_offset = -1
|
||||
boards = api_stats["STATS"]
|
||||
if len(boards) > 1:
|
||||
for board_num in range(1, 16, 5):
|
||||
for _b_num in range(5):
|
||||
b = boards[1].get(f"chain_acn{board_num + _b_num}")
|
||||
|
||||
if b and not b == 0 and board_offset == -1:
|
||||
board_offset = board_num
|
||||
if board_offset == -1:
|
||||
board_offset = 1
|
||||
|
||||
for i in range(board_offset, board_offset + self.ideal_hashboards):
|
||||
hashboard = HashBoard(
|
||||
slot=i - board_offset, expected_chips=self.nominal_chips
|
||||
)
|
||||
|
||||
chip_temp = boards[1].get(f"temp{i}")
|
||||
if chip_temp:
|
||||
hashboard.chip_temp = round(chip_temp)
|
||||
|
||||
temp = boards[1].get(f"temp2_{i}")
|
||||
if temp:
|
||||
hashboard.temp = round(temp)
|
||||
|
||||
hashrate = boards[1].get(f"chain_rate{i}")
|
||||
if hashrate:
|
||||
hashboard.hashrate = round(float(hashrate) / 1000, 2)
|
||||
|
||||
chips = boards[1].get(f"chain_acn{i}")
|
||||
if chips:
|
||||
hashboard.chips = chips
|
||||
hashboard.missing = False
|
||||
if (not chips) or (not chips > 0):
|
||||
hashboard.missing = True
|
||||
hashboards.append(hashboard)
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return hashboards
|
||||
|
||||
async def get_env_temp(self) -> Optional[float]:
|
||||
return None
|
||||
|
||||
async def get_wattage(self, api_power: dict) -> Optional[int]:
|
||||
if not api_power:
|
||||
try:
|
||||
api_power = await self.api.power()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_power:
|
||||
try:
|
||||
return api_power["POWER"][0]["Watts"]
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
async def get_wattage_limit(self) -> Optional[int]:
|
||||
return None
|
||||
|
||||
async def get_fans(self, api_fans: dict = None) -> List[Fan]:
|
||||
if not api_fans:
|
||||
try:
|
||||
api_fans = await self.api.fans()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
fans = []
|
||||
|
||||
if api_fans:
|
||||
for fan in range(self.fan_count):
|
||||
try:
|
||||
fans.append(Fan(api_fans["FANS"][0]["RPM"]))
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
fans.append(Fan())
|
||||
return fans
|
||||
|
||||
async def get_fan_psu(self) -> Optional[int]:
|
||||
return None
|
||||
|
||||
async def get_pools(self, api_pools: dict = None) -> List[dict]:
|
||||
if not api_pools:
|
||||
try:
|
||||
api_pools = await self.api.pools()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_pools:
|
||||
seen = []
|
||||
groups = [{"quota": "0"}]
|
||||
if api_pools.get("POOLS"):
|
||||
for i, pool in enumerate(api_pools["POOLS"]):
|
||||
if len(seen) == 0:
|
||||
seen.append(pool["User"])
|
||||
if not pool["User"] in seen:
|
||||
# need to use get_config, as this will never read perfectly as there are some bad edge cases
|
||||
groups = []
|
||||
cfg = await self.get_config()
|
||||
if cfg:
|
||||
for group in cfg.pool_groups:
|
||||
pools = {"quota": group.quota}
|
||||
for _i, _pool in enumerate(group.pools):
|
||||
pools[f"pool_{_i + 1}_url"] = _pool.url.replace(
|
||||
"stratum+tcp://", ""
|
||||
).replace("stratum2+tcp://", "")
|
||||
pools[f"pool_{_i + 1}_user"] = _pool.username
|
||||
groups.append(pools)
|
||||
return groups
|
||||
else:
|
||||
groups[0][f"pool_{i + 1}_url"] = (
|
||||
pool["URL"]
|
||||
.replace("stratum+tcp://", "")
|
||||
.replace("stratum2+tcp://", "")
|
||||
)
|
||||
groups[0][f"pool_{i + 1}_user"] = pool["User"]
|
||||
else:
|
||||
groups = []
|
||||
cfg = await self.get_config()
|
||||
if cfg:
|
||||
for group in cfg.pool_groups:
|
||||
pools = {"quota": group.quota}
|
||||
for _i, _pool in enumerate(group.pools):
|
||||
pools[f"pool_{_i + 1}_url"] = _pool.url.replace(
|
||||
"stratum+tcp://", ""
|
||||
).replace("stratum2+tcp://", "")
|
||||
pools[f"pool_{_i + 1}_user"] = _pool.username
|
||||
groups.append(pools)
|
||||
return groups
|
||||
return groups
|
||||
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
pass
|
||||
|
||||
async def get_fault_light(self) -> bool:
|
||||
pass
|
||||
|
||||
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
|
||||
if not api_stats:
|
||||
try:
|
||||
api_stats = await self.api.stats()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_stats:
|
||||
try:
|
||||
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
|
||||
try:
|
||||
rate_unit = api_stats["STATS"][1]["rate_unit"]
|
||||
except KeyError:
|
||||
rate_unit = "GH"
|
||||
if rate_unit == "GH":
|
||||
return round(ideal_rate / 1000, 2)
|
||||
if rate_unit == "MH":
|
||||
return round(ideal_rate / 1000000, 2)
|
||||
else:
|
||||
return round(ideal_rate, 2)
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
|
||||
async def is_mining(self) -> Optional[bool]:
|
||||
pass
|
||||
|
||||
async def get_uptime(self, api_stats: dict = None) -> Optional[int]:
|
||||
if not api_stats:
|
||||
try:
|
||||
api_stats = await self.api.stats()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if api_stats:
|
||||
try:
|
||||
return int(api_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
@@ -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
|
||||
|
||||
@@ -43,6 +44,8 @@ VNISH_DATA_LOC = {
|
||||
"errors": {"cmd": "get_errors", "kwargs": {}},
|
||||
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {"cmd": "is_mining", "kwargs": {}},
|
||||
"uptime": {"cmd": "get_uptime", "kwargs": {}},
|
||||
}
|
||||
|
||||
|
||||
@@ -142,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]:
|
||||
@@ -169,3 +172,9 @@ class VNish(BMMiner):
|
||||
return fw_ver
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
return None
|
||||
|
||||
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
|
||||
return None
|
||||
|
||||
@@ -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:
|
||||
@@ -345,6 +392,24 @@ class BaseMiner(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
"""Check whether the miner is mining.
|
||||
|
||||
Returns:
|
||||
A boolean value representing if the miner is mining.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
|
||||
"""Get the uptime of the miner in seconds.
|
||||
|
||||
Returns:
|
||||
The uptime of the miner in seconds.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def _get_data(self, allow_warning: bool, data_to_get: list = None) -> dict:
|
||||
if not data_to_get:
|
||||
# everything
|
||||
@@ -365,6 +430,8 @@ class BaseMiner(ABC):
|
||||
"errors",
|
||||
"fault_light",
|
||||
"pools",
|
||||
"is_mining",
|
||||
"uptime",
|
||||
]
|
||||
api_multicommand = []
|
||||
web_multicommand = []
|
||||
@@ -377,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))
|
||||
@@ -406,22 +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.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 (KeyError, IndexError):
|
||||
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:
|
||||
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"])
|
||||
@@ -436,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']}"
|
||||
|
||||
@@ -35,6 +35,7 @@ from pyasic.miners.backends import (
|
||||
CGMiner,
|
||||
CGMinerAvalon,
|
||||
Hiveon,
|
||||
LUXMiner,
|
||||
VNish,
|
||||
)
|
||||
from pyasic.miners.base import AnyMiner
|
||||
@@ -56,6 +57,7 @@ class MinerTypes(enum.Enum):
|
||||
BRAIINS_OS = 5
|
||||
VNISH = 6
|
||||
HIVEON = 7
|
||||
LUX_OS = 8
|
||||
|
||||
|
||||
MINER_CLASSES = {
|
||||
@@ -325,6 +327,7 @@ MINER_CLASSES = {
|
||||
"ANTMINER S17+": VNishS17Plus,
|
||||
"ANTMINER S17 PRO": VNishS17Pro,
|
||||
"ANTMINER S19": VNishS19,
|
||||
"ANTMINER S19NOPIC": VNishS19NoPIC,
|
||||
"ANTMINER S19 PRO": VNishS19Pro,
|
||||
"ANTMINER S19J": VNishS19j,
|
||||
"ANTMINER S19J PRO": VNishS19jPro,
|
||||
@@ -336,6 +339,10 @@ MINER_CLASSES = {
|
||||
None: Hiveon,
|
||||
"ANTMINER T9": HiveonT9,
|
||||
},
|
||||
MinerTypes.LUX_OS: {
|
||||
None: LUXMiner,
|
||||
"ANTMINER S9": LUXMinerS9,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -419,6 +426,7 @@ class MinerFactory:
|
||||
MinerTypes.BRAIINS_OS: self.get_miner_model_braiins_os,
|
||||
MinerTypes.VNISH: self.get_miner_model_vnish,
|
||||
MinerTypes.HIVEON: self.get_miner_model_hiveon,
|
||||
MinerTypes.LUX_OS: self.get_miner_model_luxos,
|
||||
}
|
||||
fn = miner_model_fns.get(miner_type)
|
||||
|
||||
@@ -453,7 +461,6 @@ class MinerFactory:
|
||||
text, resp = await concurrent_get_first_result(
|
||||
tasks, lambda x: x[0] is not None
|
||||
)
|
||||
|
||||
if text is not None:
|
||||
return self._parse_web_type(text, resp)
|
||||
|
||||
@@ -462,7 +469,7 @@ class MinerFactory:
|
||||
session: aiohttp.ClientSession, url: str
|
||||
) -> Tuple[Optional[str], Optional[aiohttp.ClientResponse]]:
|
||||
try:
|
||||
resp = await session.get(url)
|
||||
resp = await session.get(url, allow_redirects=False)
|
||||
return await resp.text(), resp
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
||||
pass
|
||||
@@ -490,14 +497,15 @@ class MinerFactory:
|
||||
return MinerTypes.INNOSILICON
|
||||
|
||||
async def _get_miner_socket(self, ip: str):
|
||||
commands = ["devdetails", "version"]
|
||||
commands = ["version", "devdetails"]
|
||||
tasks = [asyncio.create_task(self._socket_ping(ip, cmd)) for cmd in commands]
|
||||
|
||||
data = await concurrent_get_first_result(
|
||||
tasks, lambda x: x is not None and self._parse_socket_type(x) is not None
|
||||
)
|
||||
if data is not None:
|
||||
return self._parse_socket_type(data)
|
||||
d = self._parse_socket_type(data)
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
async def _socket_ping(ip: str, cmd: str) -> Optional[str]:
|
||||
@@ -555,7 +563,9 @@ class MinerFactory:
|
||||
return MinerTypes.VNISH
|
||||
if "HIVEON" in upper_data:
|
||||
return MinerTypes.HIVEON
|
||||
if "ANTMINER" in upper_data:
|
||||
if "LUXMINER" in upper_data:
|
||||
return MinerTypes.LUX_OS
|
||||
if "ANTMINER" in upper_data and not "DEVDETAILS" in upper_data:
|
||||
return MinerTypes.ANTMINER
|
||||
if "INTCHAINS_QOMO" in upper_data:
|
||||
return MinerTypes.GOLDSHELL
|
||||
@@ -620,10 +630,15 @@ class MinerFactory:
|
||||
return
|
||||
except (ConnectionError, OSError):
|
||||
return
|
||||
if data == b"Socket connect failed: Connection refused\n":
|
||||
return
|
||||
|
||||
data = await self._fix_api_data(data)
|
||||
|
||||
data = json.loads(data)
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
return data
|
||||
|
||||
@@ -795,6 +810,9 @@ class MinerFactory:
|
||||
split_miner_model = miner_model.split(" (")
|
||||
miner_model = split_miner_model[0]
|
||||
|
||||
if "(88)" in miner_model:
|
||||
miner_model = miner_model.replace("(88)", "NOPIC")
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
@@ -808,5 +826,17 @@ class MinerFactory:
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
|
||||
async def get_miner_model_luxos(self, ip: str):
|
||||
sock_json_data = await self.send_api_command(ip, "version")
|
||||
try:
|
||||
miner_model = sock_json_data["VERSION"][0]["Type"]
|
||||
|
||||
if " (" in miner_model:
|
||||
split_miner_model = miner_model.split(" (")
|
||||
miner_model = split_miner_model[0]
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
|
||||
|
||||
miner_factory = MinerFactory()
|
||||
|
||||
@@ -26,6 +26,15 @@ class S19(AntMiner): # noqa - ignore ABC method implementation
|
||||
self.fan_count = 4
|
||||
|
||||
|
||||
class S19NoPIC(AntMiner): # noqa - ignore ABC method implementation
|
||||
def __init__(self, ip: str, api_ver: str = "0.0.0"):
|
||||
super().__init__(ip, api_ver)
|
||||
self.ip = ip
|
||||
self.model = "S19 No PIC"
|
||||
self.nominal_chips = 88
|
||||
self.fan_count = 4
|
||||
|
||||
|
||||
class S19Pro(AntMiner): # noqa - ignore ABC method implementation
|
||||
def __init__(self, ip: str, api_ver: str = "0.0.0"):
|
||||
super().__init__(ip, api_ver)
|
||||
|
||||
@@ -23,6 +23,7 @@ from .S19 import (
|
||||
S19j,
|
||||
S19jNoPIC,
|
||||
S19jPro,
|
||||
S19NoPIC,
|
||||
S19Pro,
|
||||
S19ProPlus,
|
||||
)
|
||||
|
||||
@@ -148,6 +148,12 @@ class UnknownMiner(BaseMiner):
|
||||
async def get_nominal_hashrate(self) -> Optional[float]:
|
||||
return None
|
||||
|
||||
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
return None
|
||||
|
||||
async def get_uptime(self, *args, **kwargs) -> Optional[int]:
|
||||
return None
|
||||
|
||||
async def get_data(
|
||||
self, allow_warning: bool = False, data_to_get: list = None
|
||||
) -> MinerData:
|
||||
|
||||
@@ -192,3 +192,12 @@ class AntminerOldWebAPI(BaseWebAPI):
|
||||
|
||||
async def set_miner_conf(self, conf: dict) -> dict:
|
||||
return await self.send_command("set_miner_conf", **conf)
|
||||
|
||||
async def stats(self) -> dict:
|
||||
return await self.send_command("miner_stats")
|
||||
|
||||
async def summary(self) -> dict:
|
||||
return await self.send_command("miner_summary")
|
||||
|
||||
async def pools(self) -> dict:
|
||||
return await self.send_command("miner_pools")
|
||||
|
||||
@@ -18,6 +18,7 @@ from typing import Union
|
||||
|
||||
import httpx
|
||||
|
||||
from pyasic import APIError
|
||||
from pyasic.settings import PyasicSettings
|
||||
from pyasic.web import BaseWebAPI
|
||||
|
||||
@@ -27,6 +28,18 @@ class BOSMinerWebAPI(BaseWebAPI):
|
||||
super().__init__(ip)
|
||||
self.pwd = PyasicSettings().global_bosminer_password
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: Union[str, dict],
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
**parameters: Union[str, int, bool],
|
||||
) -> dict:
|
||||
if isinstance(command, str):
|
||||
return await self.send_luci_command(command)
|
||||
else:
|
||||
return await self.send_gql_command(command)
|
||||
|
||||
def parse_command(self, graphql_command: Union[dict, set]) -> str:
|
||||
if isinstance(graphql_command, dict):
|
||||
data = []
|
||||
@@ -40,19 +53,18 @@ class BOSMinerWebAPI(BaseWebAPI):
|
||||
data = graphql_command
|
||||
return "{" + ",".join(data) + "}"
|
||||
|
||||
async def send_command(
|
||||
async def send_gql_command(
|
||||
self,
|
||||
command: dict,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
**parameters: Union[str, int, bool],
|
||||
) -> 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:
|
||||
@@ -63,8 +75,34 @@ class BOSMinerWebAPI(BaseWebAPI):
|
||||
pass
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
self, *commands: Union[dict, str], allow_warning: bool = True
|
||||
) -> dict:
|
||||
luci_commands = []
|
||||
gql_commands = []
|
||||
for cmd in commands:
|
||||
if isinstance(cmd, dict):
|
||||
gql_commands.append(cmd)
|
||||
if isinstance(cmd, str):
|
||||
luci_commands.append(cmd)
|
||||
|
||||
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
|
||||
|
||||
async def luci_multicommand(self, *commands: str) -> dict:
|
||||
data = {}
|
||||
for command in commands:
|
||||
data[command] = await self.send_luci_command(command, ignore_errors=True)
|
||||
return data
|
||||
|
||||
async def gql_multicommand(self, *commands: dict) -> dict:
|
||||
def merge(*d: dict):
|
||||
ret = {}
|
||||
for i in d:
|
||||
@@ -85,8 +123,8 @@ class BOSMinerWebAPI(BaseWebAPI):
|
||||
# noinspection PyTypeChecker
|
||||
commands.remove({"bos": {"faultLight": None}})
|
||||
command = merge(*commands)
|
||||
data = await self.send_command(command)
|
||||
except LookupError:
|
||||
data = await self.send_gql_command(command)
|
||||
except (LookupError, ValueError):
|
||||
pass
|
||||
if not data:
|
||||
data = {}
|
||||
@@ -105,3 +143,53 @@ class BOSMinerWebAPI(BaseWebAPI):
|
||||
+ '"){__typename}}}'
|
||||
},
|
||||
)
|
||||
|
||||
async def send_luci_command(self, path: str, ignore_errors: bool = False) -> dict:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
await self.luci_auth(client)
|
||||
data = await client.get(
|
||||
f"http://{self.ip}{path}", headers={"User-Agent": "BTC Tools v0.1"}
|
||||
)
|
||||
if data.status_code == 200:
|
||||
return data.json()
|
||||
if ignore_errors:
|
||||
return {}
|
||||
raise APIError(
|
||||
f"Web command failed: path={path}, code={data.status_code}"
|
||||
)
|
||||
except (httpx.HTTPError, json.JSONDecodeError):
|
||||
if ignore_errors:
|
||||
return {}
|
||||
raise APIError(f"Web command failed: path={path}")
|
||||
|
||||
async def luci_auth(self, session: httpx.AsyncClient):
|
||||
login = {"luci_username": self.username, "luci_password": self.pwd}
|
||||
url = f"http://{self.ip}/cgi-bin/luci"
|
||||
headers = {
|
||||
"User-Agent": "BTC Tools v0.1", # only seems to respond if this user-agent is set
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
await session.post(url, headers=headers, data=login)
|
||||
|
||||
async def get_net_conf(self):
|
||||
return await self.send_luci_command(
|
||||
"/cgi-bin/luci/admin/network/iface_status/lan"
|
||||
)
|
||||
|
||||
async def get_cfg_metadata(self):
|
||||
return await self.send_luci_command("/cgi-bin/luci/admin/miner/cfg_metadata")
|
||||
|
||||
async def get_cfg_data(self):
|
||||
return await self.send_luci_command("/cgi-bin/luci/admin/miner/cfg_data")
|
||||
|
||||
async def get_bos_info(self):
|
||||
return await self.send_luci_command("/cgi-bin/luci/bos/info")
|
||||
|
||||
async def get_overview(self):
|
||||
return await self.send_luci_command(
|
||||
"/cgi-bin/luci/admin/status/overview?status=1"
|
||||
) # needs status=1 or it fails
|
||||
|
||||
async def get_api_status(self):
|
||||
return await self.send_luci_command("/cgi-bin/luci/admin/miner/api_status")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pyasic"
|
||||
version = "0.35.0"
|
||||
version = "0.36.11"
|
||||
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"
|
||||
|
||||
@@ -23,157 +23,21 @@ from pyasic.miners.backends import CGMiner # noqa
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.miners.miner_factory import MINER_CLASSES, MinerFactory
|
||||
|
||||
# class MinersTest(unittest.TestCase):
|
||||
# def test_miner_model_creation(self):
|
||||
# warnings.filterwarnings("ignore")
|
||||
# for miner_model in MINER_CLASSES.keys():
|
||||
# for miner_api in MINER_CLASSES[miner_model].keys():
|
||||
# with self.subTest(
|
||||
# msg=f"Creation of miner using model={miner_model}, api={miner_api}",
|
||||
# miner_model=miner_model,
|
||||
# miner_api=miner_api,
|
||||
# ):
|
||||
# miner = MINER_CLASSES[miner_model][miner_api]("127.0.0.1")
|
||||
# self.assertTrue(
|
||||
# isinstance(miner, MINER_CLASSES[miner_model][miner_api])
|
||||
# )
|
||||
#
|
||||
# def test_miner_backend_backup_creation(self):
|
||||
# warnings.filterwarnings("ignore")
|
||||
#
|
||||
# backends = [
|
||||
# list(
|
||||
# inspect.getmembers(
|
||||
# sys.modules[f"pyasic.miners.backends"], inspect.isclass
|
||||
# )
|
||||
# )
|
||||
# ]
|
||||
# backends = [item for sublist in backends for item in sublist]
|
||||
# for backend in backends:
|
||||
# miner_class = backend[1]
|
||||
# with self.subTest(miner_class=miner_class):
|
||||
# miner = miner_class("127.0.0.1")
|
||||
# self.assertTrue(isinstance(miner, miner_class))
|
||||
#
|
||||
# def test_miner_type_creation_failure(self):
|
||||
# warnings.filterwarnings("ignore")
|
||||
#
|
||||
# backends = [
|
||||
# list(
|
||||
# inspect.getmembers(
|
||||
# sys.modules[f"pyasic.miners.{algo}._types"], inspect.isclass
|
||||
# )
|
||||
# )
|
||||
# for algo in ["btc", "zec", "ltc"]
|
||||
# ]
|
||||
# backends = [item for sublist in backends for item in sublist]
|
||||
# for backend in backends:
|
||||
# miner_class = backend[1]
|
||||
# with self.subTest(miner_class=miner_class):
|
||||
# with self.assertRaises(TypeError):
|
||||
# miner_class("127.0.0.1")
|
||||
# with self.assertRaises(TypeError):
|
||||
# BaseMiner("127.0.0.1")
|
||||
#
|
||||
# def test_miner_comparisons(self):
|
||||
# miner_1 = CGMiner("1.1.1.1")
|
||||
# miner_2 = CGMiner("2.2.2.2")
|
||||
# miner_3 = CGMiner("1.1.1.1")
|
||||
# self.assertEqual(miner_1, miner_3)
|
||||
# self.assertGreater(miner_2, miner_1)
|
||||
# self.assertLess(miner_3, miner_2)
|
||||
#
|
||||
#
|
||||
# class MinerFactoryTest(unittest.TestCase):
|
||||
# def test_miner_factory_creation(self):
|
||||
# warnings.filterwarnings("ignore")
|
||||
#
|
||||
# self.assertDictEqual(MinerFactory().miners, {})
|
||||
# miner_factory = MinerFactory()
|
||||
# self.assertIs(MinerFactory(), miner_factory)
|
||||
#
|
||||
# def test_get_miner_generator(self):
|
||||
# async def _coro():
|
||||
# gen = MinerFactory().get_miner_generator([])
|
||||
# miners = []
|
||||
# async for miner in gen:
|
||||
# miners.append(miner)
|
||||
# return miners
|
||||
#
|
||||
# _miners = asyncio.run(_coro())
|
||||
# self.assertListEqual(_miners, [])
|
||||
#
|
||||
# def test_miner_selection(self):
|
||||
# warnings.filterwarnings("ignore")
|
||||
#
|
||||
# for miner_model in MINER_CLASSES.keys():
|
||||
# with self.subTest():
|
||||
# miner = MinerFactory()._select_miner_from_classes(
|
||||
# "127.0.0.1", miner_model, None, None
|
||||
# )
|
||||
# self.assertIsInstance(miner, BaseMiner)
|
||||
# for api in ["BOSMiner+", "BOSMiner", "CGMiner", "BTMiner", "BMMiner"]:
|
||||
# with self.subTest():
|
||||
# miner = MinerFactory()._select_miner_from_classes(
|
||||
# "127.0.0.1", None, api, None
|
||||
# )
|
||||
# self.assertIsInstance(miner, BaseMiner)
|
||||
#
|
||||
# with self.subTest():
|
||||
# miner = MinerFactory()._select_miner_from_classes(
|
||||
# "127.0.0.1", "ANTMINER S17+", "Fake API", None
|
||||
# )
|
||||
# self.assertIsInstance(miner, BaseMiner)
|
||||
#
|
||||
# with self.subTest():
|
||||
# miner = MinerFactory()._select_miner_from_classes(
|
||||
# "127.0.0.1", "M30S", "BTMiner", "G20"
|
||||
# )
|
||||
# self.assertIsInstance(miner, BaseMiner)
|
||||
#
|
||||
# def test_validate_command(self):
|
||||
# bad_test_data_returns = [
|
||||
# {},
|
||||
# {
|
||||
# "cmd": [
|
||||
# {
|
||||
# "STATUS": [
|
||||
# {"STATUS": "E", "Msg": "Command failed for some reason."}
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
# },
|
||||
# {"STATUS": "E", "Msg": "Command failed for some reason."},
|
||||
# {
|
||||
# "STATUS": [{"STATUS": "E", "Msg": "Command failed for some reason."}],
|
||||
# "id": 1,
|
||||
# },
|
||||
# ]
|
||||
# for data in bad_test_data_returns:
|
||||
# with self.subTest():
|
||||
#
|
||||
# async def _coro(miner_ret):
|
||||
# _data = await MinerFactory()._validate_command(miner_ret)
|
||||
# return _data
|
||||
#
|
||||
# ret = asyncio.run(_coro(data))
|
||||
# self.assertFalse(ret[0])
|
||||
#
|
||||
# good_test_data_returns = [
|
||||
# {
|
||||
# "STATUS": [{"STATUS": "S", "Msg": "Yay! Command succeeded."}],
|
||||
# "id": 1,
|
||||
# },
|
||||
# ]
|
||||
# for data in good_test_data_returns:
|
||||
# with self.subTest():
|
||||
#
|
||||
# async def _coro(miner_ret):
|
||||
# _data = await MinerFactory()._validate_command(miner_ret)
|
||||
# return _data
|
||||
#
|
||||
# ret = asyncio.run(_coro(data))
|
||||
# self.assertTrue(ret[0])
|
||||
|
||||
class MinersTest(unittest.TestCase):
|
||||
def test_miner_model_creation(self):
|
||||
warnings.filterwarnings("ignore")
|
||||
for miner_model in MINER_CLASSES.keys():
|
||||
for miner_api in MINER_CLASSES[miner_model].keys():
|
||||
with self.subTest(
|
||||
msg=f"Creation of miner using model={miner_model}, api={miner_api}",
|
||||
miner_model=miner_model,
|
||||
miner_api=miner_api,
|
||||
):
|
||||
miner = MINER_CLASSES[miner_model][miner_api]("127.0.0.1")
|
||||
self.assertTrue(
|
||||
isinstance(miner, MINER_CLASSES[miner_model][miner_api])
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user