Compare commits

..

81 Commits

Author SHA1 Message Date
UpstreamData
39e3e249f8 version: bump version number. 2023-10-02 13:14:21 -06:00
UpstreamData
118c5b056e refactor: improve settings handling to not use a dataclass, and not use singleton. 2023-10-02 13:13:31 -06:00
UpstreamData
2c3b5599fe version: bump version number. 2023-10-02 09:20:24 -06:00
UpstreamData
e421eaa324 feature: add support for M20P, and add chips for M20SV30. 2023-10-02 09:20:01 -06:00
UpstreamData
53f3fc5ee9 version: bump version number. 2023-09-28 15:47:49 -06:00
UpstreamData
1b36de4131 bug: fAdd new commands added in whatsminer API 2.0.5. 2023-09-28 15:47:20 -06:00
UpstreamData
6f0c6f6284 bug: fix whatsminer identification to work with backwards incompatible changes in API 2.0.5. 2023-09-28 15:42:12 -06:00
UpstreamData
b7dda5bf87 Update README.md 2023-09-26 11:50:56 -06:00
UpstreamData
53a3bbf531 version: bump version number. 2023-09-19 13:59:56 -06:00
UpstreamData
50586f1ce7 feature: add S19+. 2023-09-19 13:59:03 -06:00
UpstreamData
9f6235a0fc feature: add S19i. 2023-09-19 13:56:40 -06:00
UpstreamData
4d21f150ce version: bump version number. 2023-09-18 09:35:38 -06:00
UpstreamData
7c0dfc49dd bug: fix wrong fault light setting when setting fault light to off. 2023-09-18 09:35:19 -06:00
UpstreamData
269b13f6c1 version: bump version number. 2023-09-15 08:57:56 -06:00
Elias Kunnas
a9bb7d2e5a Fix btminer pre_power_on (#62) 2023-09-15 08:56:29 -06:00
Upstream Data
11295f27a7 version: bump version number. 2023-09-12 19:21:04 -06:00
Upstream Data
55aa3dd85b bug: handle edge cases where a missed get_config on bosminer can cause an empty config to be applied to a miner. 2023-09-12 19:20:48 -06:00
UpstreamData
20272d4360 version: bump version number. 2023-09-11 13:45:52 -06:00
UpstreamData
623dc92ef2 feature: Add MinerData.as_dict(). 2023-09-11 13:45:23 -06:00
Upstream Data
2d59394b1e version: bump version number. 2023-09-07 19:07:11 -06:00
Upstream Data
26c2095ff1 bug: fix uncaught error in get_hashboards with BMMiner if a key doesnt exist. 2023-09-07 19:06:51 -06:00
Upstream Data
ec7d241caa version: bump version number. 2023-09-05 17:22:23 -06:00
Upstream Data
d0432ed1aa bug: handle for some weird edge cases with boards plugged into the wrong slots on X19. 2023-09-05 17:22:02 -06:00
Upstream Data
8c5503d002 version: bump version number. 2023-08-30 17:47:20 -06:00
Upstream Data
6d6f950c95 bug: add modified changed from [Issue 57](https://github.com/UpstreamData/pyasic/issues/57#issuecomment-1699984187) 2023-08-30 17:46:23 -06:00
UpstreamData
30745e54ba feature: add chip count for M30S+VE50 2023-08-30 11:18:25 -06:00
UpstreamData
c3fd94e79e version: bump version number. 2023-08-28 08:53:59 -06:00
UpstreamData
2924a8d67b feature: add more whatsminer error codes. 2023-08-28 08:53:27 -06:00
UpstreamData
9f4c4bb9cf feature: add exclude to get_data, and change data_to_get to include. 2023-08-28 08:32:29 -06:00
UpstreamData
3d6eebf06e bug: fix a bug with hostname gathering on some Avalons. 2023-08-28 08:31:54 -06:00
Upstream Data
b3d9b6ff7e version: bump version number. 2023-08-26 11:21:21 -06:00
Upstream Data
60facacc48 bug: fix a bug with bosminer commands. 2023-08-26 11:21:10 -06:00
Upstream Data
b8a6063838 version: bumnp version number. 2023-08-26 10:57:40 -06:00
Upstream Data
bcba2be524 bug: remove bad await calls to httpx response.json(). 2023-08-26 10:56:53 -06:00
UpstreamData
f7187d2017 bug: add chip count for M29V10. 2023-08-25 08:58:34 -06:00
Upstream Data
d91b7c4406 version: bump version number. 2023-08-07 17:02:50 -06:00
Upstream Data
248a7e6d69 bug: fix some WM models reporting https first and being identified as BOS+. 2023-08-07 17:02:26 -06:00
Upstream Data
4f2c3e772a version: bump version number. 2023-08-06 17:25:21 -06:00
Upstream Data
95f7146eef feature: add VNish pause/resume commands. 2023-08-06 17:24:36 -06:00
UpstreamData
9d5d19cc6b version: bump version number. 2023-07-27 20:45:42 -06:00
UpstreamData
cc38129571 bug: add back pwd for ssh connections. 2023-07-27 20:45:08 -06:00
UpstreamData
3dfd9f237d version: bump version number. 2023-07-27 20:18:58 -06:00
UpstreamData
f3fe478dbb feature: add support for S19J Pro No PIC. 2023-07-27 20:18:36 -06:00
UpstreamData
e10f32ae3d feature: speed up getting older antminer types with concurrent web and api requests. 2023-07-24 21:05:07 -06:00
UpstreamData
4e0924aa0e feature: add support for AML vnish miners. 2023-07-24 20:45:30 -06:00
UpstreamData
d0d3fd3117 bug: fix failed verification of SSL cert on whatsminer. 2023-07-24 20:19:00 -06:00
UpstreamData
4de950d8f4 feature: revert miner_factory to use httpx, as it now seems to be the same speed, and aiohttp doesnt support digest auth. 2023-07-24 13:09:30 -06:00
UpstreamData
03f2a1f9ba feature: optimize multicommand on new X19 models. 2023-07-24 11:34:16 -06:00
UpstreamData
2653db90e3 feature: optimize the way multicommand is handled on BTMiner. 2023-07-24 09:44:23 -06:00
UpstreamData
ddc8c53eb9 feature: add chip count for M50 VH60. 2023-07-13 10:59:27 -06:00
UpstreamData
eb5d1a24ea version: bump version number. 2023-07-12 08:56:59 -06:00
UpstreamData
6c0e80265b bug: revert X19 miner mode to string. 2023-07-12 08:56:23 -06:00
UpstreamData
ad3a4ae414 docs: update some bad code, and add references to new miner types and API types. 2023-07-11 11:18:28 -06:00
UpstreamData
3484d43510 version: bump version number. 2023-07-07 14:09:22 -06:00
UpstreamData
dd7e352391 bug: fix some addition issues with MinerData sums. 2023-07-07 14:09:08 -06:00
UpstreamData
a32b61fe5d version: bump version number. 2023-07-07 12:37:34 -06:00
UpstreamData
597a178009 feature: Update MinerData to use None. 2023-07-07 12:37:20 -06:00
Michael Schmid
409b2527f0 use None instead of -1 for temps and wattages (#55)
* use `None` instead of `-1` for temps and wattages
this way it's easier for other tools like HomeAssistant to understand if the temperature is really negative or not available

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

View File

@@ -1,5 +1,5 @@
# pyasic # pyasic
*A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH.* *A simplified and standardized interface for Bitcoin ASICs.*
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![pypi](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/) [![pypi](https://img.shields.io/pypi/v/pyasic.svg)](https://pypi.org/project/pyasic/)

View File

@@ -15,6 +15,7 @@ Use these instead -
#### [BOSMiner API][pyasic.API.bosminer.BOSMinerAPI] #### [BOSMiner API][pyasic.API.bosminer.BOSMinerAPI]
#### [BTMiner API][pyasic.API.btminer.BTMinerAPI] #### [BTMiner API][pyasic.API.btminer.BTMinerAPI]
#### [CGMiner API][pyasic.API.cgminer.CGMinerAPI] #### [CGMiner API][pyasic.API.cgminer.CGMinerAPI]
#### [LUXMiner API][pyasic.API.luxminer.LUXMinerAPI]
#### [Unknown API][pyasic.API.unknown.UnknownAPI] #### [Unknown API][pyasic.API.unknown.UnknownAPI]
<br> <br>

7
docs/API/luxminer.md Normal file
View File

@@ -0,0 +1,7 @@
# pyasic
## LUXMinerAPI
::: pyasic.API.luxminer.LUXMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4

163
docs/generate_miners.py Normal file
View File

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

View File

@@ -76,13 +76,14 @@ This function will return an instance of the dataclass [`MinerData`][pyasic.data
Each piece of data in a [`MinerData`][pyasic.data.MinerData] instance can be referenced by getting it as an attribute, such as [`MinerData().hashrate`][pyasic.data.MinerData]. Each piece of data in a [`MinerData`][pyasic.data.MinerData] instance can be referenced by getting it as an attribute, such as [`MinerData().hashrate`][pyasic.data.MinerData].
```python ```python
import asyncio import asyncio
from pyasic.miners.miner_factory import MinerFactory from pyasic import get_miner
async def gather_miner_data(): async def gather_miner_data():
miner = await MinerFactory().get_miner("192.168.1.75") miner = await get_miner("192.168.1.75")
miner_data = await miner.get_data() if miner is not None:
print(miner_data) # all data from the dataclass miner_data = await miner.get_data()
print(miner_data.hashrate) # hashrate of the miner in TH/s print(miner_data) # all data from the dataclass
print(miner_data.hashrate) # hashrate of the miner in TH/s
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(gather_miner_data()) asyncio.run(gather_miner_data())

View File

@@ -127,6 +127,13 @@
show_root_heading: false show_root_heading: false
heading_level: 4 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) ## S19 Pro (VNish)
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19Pro ::: pyasic.miners.antminer.vnish.X19.S19.VNishS19Pro
handler: python handler: python

View File

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

View File

@@ -0,0 +1,8 @@
# pyasic
## LUXMiner Backend
::: pyasic.miners.backends.luxminer.LUXMiner
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,8 @@
# pyasic
## VNish Backend
::: pyasic.miners.backends.vnish.VNish
handler: python
options:
show_root_heading: false
heading_level: 4

View File

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

View File

@@ -20,6 +20,7 @@ nav:
- BOSMiner: "API/bosminer.md" - BOSMiner: "API/bosminer.md"
- BTMiner: "API/btminer.md" - BTMiner: "API/btminer.md"
- CGMiner: "API/cgminer.md" - CGMiner: "API/cgminer.md"
- LUXMiner: "API/luxminer.md"
- Unknown: "API/unknown.md" - Unknown: "API/unknown.md"
- Backends: - Backends:
- BMMiner: "miners/backends/bmminer.md" - BMMiner: "miners/backends/bmminer.md"
@@ -27,6 +28,8 @@ nav:
- BFGMiner: "miners/backends/bfgminer.md" - BFGMiner: "miners/backends/bfgminer.md"
- BTMiner: "miners/backends/btminer.md" - BTMiner: "miners/backends/btminer.md"
- CGMiner: "miners/backends/cgminer.md" - CGMiner: "miners/backends/cgminer.md"
- LUXMiner: "miners/backends/luxminer.md"
- VNish: "miners/backends/vnish.md"
- Hiveon: "miners/backends/hiveon.md" - Hiveon: "miners/backends/hiveon.md"
- Classes: - Classes:
- Antminer X3: "miners/antminer/X3.md" - Antminer X3: "miners/antminer/X3.md"
@@ -40,14 +43,15 @@ nav:
- Avalon 8X: "miners/avalonminer/A8X.md" - Avalon 8X: "miners/avalonminer/A8X.md"
- Avalon 9X: "miners/avalonminer/A9X.md" - Avalon 9X: "miners/avalonminer/A9X.md"
- Avalon 10X: "miners/avalonminer/A10X.md" - Avalon 10X: "miners/avalonminer/A10X.md"
- Avalon 11X: "miners/avalonminer/A11X.md"
- Avalon 12X: "miners/avalonminer/A12X.md"
- Whatsminer M2X: "miners/whatsminer/M2X.md" - Whatsminer M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md" - Whatsminer M3X: "miners/whatsminer/M3X.md"
- Whatsminer M5X: "miners/whatsminer/M5X.md" - Whatsminer M5X: "miners/whatsminer/M5X.md"
- Innosilicon T3X: "miners/innosilicon/T3X.md" - Innosilicon T3X: "miners/innosilicon/T3X.md"
- Innosilicon A10X: "miners/innosilicon/A10X.md" - Innosilicon A10X: "miners/innosilicon/A10X.md"
- Goldshell CKX: "miners/goldshell/CKX.md" - Goldshell X5: "miners/goldshell/X5.md"
- Goldshell HSX: "miners/goldshell/HSX.md" - Goldshell XMax: "miners/goldshell/XMax.md"
- Goldshell KDX: "miners/goldshell/KDX.md"
- Base Miner: "miners/base_miner.md" - Base Miner: "miners/base_miner.md"

View File

@@ -20,7 +20,7 @@ import json
import logging import logging
import re import re
import warnings import warnings
from typing import Union from typing import Tuple, Union
from pyasic.errors import APIError, APIWarning from pyasic.errors import APIError, APIWarning
@@ -32,6 +32,8 @@ class BaseMinerAPI:
# ip address of the miner # ip address of the miner
self.ip = ipaddress.ip_address(ip) self.ip = ipaddress.ip_address(ip)
self.pwd = "admin"
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls is BaseMinerAPI: if cls is BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated") raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
@@ -126,6 +128,18 @@ class BaseMinerAPI:
data["multicommand"] = True data["multicommand"] = True
return data return data
async def _handle_multicommand(self, command: str, allow_warning: bool = True):
try:
data = await self.send_command(command, allow_warning=allow_warning)
if not "+" in command:
return {command: [data]}
return data
except APIError:
if "+" in command:
return {command: [{}] for command in command.split("+")}
return {command: [{}]}
@property @property
def commands(self) -> list: def commands(self) -> list:
return self.get_commands() return self.get_commands()
@@ -169,7 +183,11 @@ If you are sure you want to use this command please use API.send_command("{comma
) )
return return_commands return return_commands
async def _send_bytes(self, data: bytes, timeout: int = 100) -> bytes: async def _send_bytes(
self,
data: bytes,
timeout: int = 100,
) -> bytes:
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending") logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
try: try:
# get reader and writer streams # get reader and writer streams
@@ -240,9 +258,12 @@ If you are sure you want to use this command please use API.send_command("{comma
return False, data["Msg"] return False, data["Msg"]
else: else:
# make sure the command succeeded # make sure the command succeeded
if type(data["STATUS"]) == str: if isinstance(data["STATUS"], str):
if data["STATUS"] in ["RESTART"]: if data["STATUS"] in ["RESTART"]:
return True, None return True, None
elif isinstance(data["STATUS"], dict):
if data["STATUS"].get("STATUS") in ["S", "I"]:
return True, None
elif data["STATUS"][0]["STATUS"] not in ("S", "I"): elif data["STATUS"][0]["STATUS"] not in ("S", "I"):
# this is an error # this is an error
if data["STATUS"][0]["STATUS"] not in ("S", "I"): if data["STATUS"][0]["STATUS"] not in ("S", "I"):

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import logging import logging
from pyasic.API import APIError, BaseMinerAPI from pyasic.API import APIError, BaseMinerAPI
@@ -56,19 +56,19 @@ class BFGMinerAPI(BaseMinerAPI):
return data return data
async def _x19_multicommand(self, *commands) -> dict: async def _x19_multicommand(self, *commands) -> dict:
data = None tasks = []
try: # send all commands individually
data = {} for cmd in commands:
# send all commands individually tasks.append(
for cmd in commands: asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
) )
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data return data
async def version(self) -> dict: async def version(self) -> dict:

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import logging import logging
from pyasic.API import APIError, BaseMinerAPI from pyasic.API import APIError, BaseMinerAPI
@@ -57,21 +58,19 @@ class BMMinerAPI(BaseMinerAPI):
return data return data
async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict: async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict:
data = None tasks = []
try: # send all commands individually
data = {} for cmd in commands:
# send all commands individually tasks.append(
for cmd in commands: asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
data[cmd] = []
data[cmd].append(
await self.send_command(cmd, allow_warning=allow_warning)
)
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
) )
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data return data
async def version(self) -> dict: async def version(self) -> dict:

View File

@@ -22,15 +22,15 @@ import hashlib
import json import json
import logging import logging
import re import re
from typing import Union from typing import Literal, Union
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from passlib.handlers.md5_crypt import md5_crypt from passlib.handlers.md5_crypt import md5_crypt
from pyasic import settings
from pyasic.API import BaseMinerAPI from pyasic.API import BaseMinerAPI
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.misc import api_min_version from pyasic.misc import api_min_version
from pyasic.settings import PyasicSettings
### IMPORTANT ### ### IMPORTANT ###
# you need to change the password of the miners using the Whatsminer # you need to change the password of the miners using the Whatsminer
@@ -40,6 +40,12 @@ from pyasic.settings import PyasicSettings
# you change the password, you can pass that to this class as pwd, # you change the password, you can pass that to this class as pwd,
# or add it as the Whatsminer_pwd in the settings.toml file. # or add it as the Whatsminer_pwd in the settings.toml file.
PrePowerOnMessage = Union[
Literal["wait for adjust temp"],
Literal["adjust complete"],
Literal["adjust continue"],
]
def _crypt(word: str, salt: str) -> str: def _crypt(word: str, salt: str) -> str:
"""Encrypts a word with a salt, using a standard salt format. """Encrypts a word with a salt, using a standard salt format.
@@ -186,7 +192,7 @@ class BTMinerAPI(BaseMinerAPI):
ip: str, ip: str,
api_ver: str = "0.0.0", api_ver: str = "0.0.0",
port: int = 4028, port: int = 4028,
pwd: str = PyasicSettings().global_whatsminer_password, pwd: str = settings.get("default_whatsminer_password", "admin"),
): ):
super().__init__(ip, port) super().__init__(ip, port)
self.pwd = pwd self.pwd = pwd
@@ -203,27 +209,35 @@ class BTMinerAPI(BaseMinerAPI):
# make sure we can actually run each command, otherwise they will fail # make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands) commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2" # standard multicommand format is "command1+command2"
# commands starting with "get_" aren't supported, but we can fake that # commands starting with "get_" and the "status" command aren't supported, but we can fake that
get_commands_data = {}
tasks = []
for command in list(commands): for command in list(commands):
if command.startswith("get_"): if command.startswith("get_") or command == "status":
commands.remove(command) commands.remove(command)
# send seperately and append later # send seperately and append later
try: tasks.append(
get_commands_data[command] = [ asyncio.create_task(
await self.send_command(command, allow_warning=allow_warning) self._handle_multicommand(command, allow_warning=allow_warning)
] )
except APIError: )
get_commands_data[command] = [{}]
command = "+".join(commands) command = "+".join(commands)
try: tasks.append(
main_data = await self.send_command(command, allow_warning=allow_warning) asyncio.create_task(
except APIError: self._handle_multicommand(command, allow_warning=allow_warning)
main_data = {command: [{}] for command in commands} )
)
all_data = await asyncio.gather(*tasks)
logging.debug(f"{self} - (Multicommand) - Received data") logging.debug(f"{self} - (Multicommand) - Received data")
data = dict(**main_data, **get_commands_data) data = {}
for item in all_data:
data.update(item)
data["multicommand"] = True data["multicommand"] = True
return data return data
@@ -685,7 +699,7 @@ class BTMinerAPI(BaseMinerAPI):
) )
return await self.send_privileged_command("set_power_pct", percent=str(percent)) return await self.send_privileged_command("set_power_pct", percent=str(percent))
async def pre_power_on(self, complete: bool, msg: str) -> dict: async def pre_power_on(self, complete: bool, msg: PrePowerOnMessage) -> dict:
"""Configure or check status of pre power on. """Configure or check status of pre power on.
<details> <details>
@@ -705,7 +719,7 @@ class BTMinerAPI(BaseMinerAPI):
</details> </details>
""" """
if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue": if msg not in ("wait for adjust temp", "adjust complete", "adjust continue"):
raise APIError( raise APIError(
"Message is incorrect, please choose one of " "Message is incorrect, please choose one of "
'["wait for adjust temp", ' '["wait for adjust temp", '
@@ -721,6 +735,34 @@ class BTMinerAPI(BaseMinerAPI):
) )
### ADDED IN V2.0.5 Whatsminer API ### ### ADDED IN V2.0.5 Whatsminer API ###
@api_min_version("2.0.5")
async def set_power_pct_v2(self, percent: int) -> dict:
"""Set the power percentage of the miner based on current power. Used for temporary adjustment. Added in API v2.0.5.
<details>
<summary>Expand</summary>
Set the power percentage of the miner, only works after changing
the password of the miner using the Whatsminer tool.
Parameters:
percent: The power percentage to set.
Returns:
A reply informing of the status of setting the power percentage.
</details>
"""
if not 0 < percent < 100:
raise APIError(
f"Power PCT % is outside of the allowed "
f"range. Please set a % between 0 and "
f"100"
)
return await self.send_privileged_command(
"set_power_pct_v2", percent=str(percent)
)
@api_min_version("2.0.5") @api_min_version("2.0.5")
async def set_temp_offset(self, temp_offset: int) -> dict: async def set_temp_offset(self, temp_offset: int) -> dict:
"""Set the offset of miner hash board target temperature. """Set the offset of miner hash board target temperature.

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import logging import logging
from pyasic.API import APIError, BaseMinerAPI from pyasic.API import APIError, BaseMinerAPI
@@ -56,19 +56,19 @@ class CGMinerAPI(BaseMinerAPI):
return data return data
async def _x19_multicommand(self, *commands) -> dict: async def _x19_multicommand(self, *commands) -> dict:
data = None tasks = []
try: # send all commands individually
data = {} for cmd in commands:
# send all commands individually tasks.append(
for cmd in commands: asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
) )
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data return data
async def version(self) -> dict: async def version(self) -> dict:

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic import settings
from pyasic.API.bmminer import BMMinerAPI from pyasic.API.bmminer import BMMinerAPI
from pyasic.API.bosminer import BOSMinerAPI from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API.btminer import BTMinerAPI from pyasic.API.btminer import BTMinerAPI
@@ -32,7 +33,6 @@ from pyasic.miners.base import AnyMiner
from pyasic.miners.miner_factory import MinerFactory from pyasic.miners.miner_factory import MinerFactory
from pyasic.miners.miner_listener import MinerListener from pyasic.miners.miner_listener import MinerListener
from pyasic.network import MinerNetwork from pyasic.network import MinerNetwork
from pyasic.settings import PyasicSettings
__all__ = [ __all__ = [
"BMMinerAPI", "BMMinerAPI",
@@ -53,5 +53,5 @@ __all__ = [
"MinerFactory", "MinerFactory",
"MinerListener", "MinerListener",
"MinerNetwork", "MinerNetwork",
"PyasicSettings", "settings",
] ]

View File

@@ -560,9 +560,6 @@ class MinerConfig:
if self.fan_speed: if self.fan_speed:
cfg["bitmain-fan-pwn"] = str(self.fan_speed) cfg["bitmain-fan-pwn"] = str(self.fan_speed)
if self.miner_mode == X19PowerMode.Sleep:
cfg["freq-level"] = "0"
return cfg return cfg
def as_x17(self, user_suffix: str = None) -> dict: def as_x17(self, user_suffix: str = None) -> dict:

View File

@@ -20,7 +20,7 @@ import logging
import time import time
from dataclasses import asdict, dataclass, field, fields from dataclasses import asdict, dataclass, field, fields
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import List, Union from typing import Any, List, Union
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
@@ -40,13 +40,28 @@ class HashBoard:
""" """
slot: int = 0 slot: int = 0
hashrate: float = 0.0 hashrate: float = None
temp: int = -1 temp: int = None
chip_temp: int = -1 chip_temp: int = None
chips: int = 0 chips: int = None
expected_chips: int = 0 expected_chips: int = None
missing: bool = True missing: bool = True
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"{item}")
@dataclass @dataclass
class Fan: class Fan:
@@ -56,7 +71,22 @@ class Fan:
speed: The speed of the fan. speed: The speed of the fan.
""" """
speed: int = -1 speed: int = None
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"{item}")
@dataclass @dataclass
@@ -102,26 +132,26 @@ class MinerData:
ip: str ip: str
datetime: datetime = None datetime: datetime = None
uptime: int = 0 uptime: int = None
mac: str = "00:00:00:00:00:00" mac: str = None
model: str = "Unknown" model: str = None
make: str = "Unknown" make: str = None
api_ver: str = "Unknown" api_ver: str = None
fw_ver: str = "Unknown" fw_ver: str = None
hostname: str = "Unknown" hostname: str = None
hashrate: float = field(init=False) hashrate: float = field(init=False)
_hashrate: float = 0 _hashrate: float = None
nominal_hashrate: float = 0 nominal_hashrate: float = None
hashboards: List[HashBoard] = field(default_factory=list) hashboards: List[HashBoard] = field(default_factory=list)
ideal_hashboards: int = 1 ideal_hashboards: int = None
temperature_avg: int = field(init=False) temperature_avg: int = field(init=False)
env_temp: float = -1.0 env_temp: float = None
wattage: int = -1 wattage: int = None
wattage_limit: int = -1 wattage_limit: int = None
fans: List[Fan] = field(default_factory=list) fans: List[Fan] = field(default_factory=list)
fan_psu: int = -1 fan_psu: int = None
total_chips: int = field(init=False) total_chips: int = field(init=False)
ideal_chips: int = 1 ideal_chips: int = None
percent_ideal_chips: float = field(init=False) percent_ideal_chips: float = field(init=False)
percent_ideal_hashrate: float = field(init=False) percent_ideal_hashrate: float = field(init=False)
percent_ideal_wattage: float = field(init=False) percent_ideal_wattage: float = field(init=False)
@@ -145,7 +175,16 @@ class MinerData:
def __post_init__(self): def __post_init__(self):
self.datetime = datetime.now(timezone.utc).astimezone() self.datetime = datetime.now(timezone.utc).astimezone()
def __getitem__(self, item): def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try: try:
return getattr(self, item) return getattr(self, item)
except AttributeError: except AttributeError:
@@ -197,7 +236,12 @@ class MinerData:
@property @property
def hashrate(self): # noqa - Skip PyCharm inspection def hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) > 0: if len(self.hashboards) > 0:
return round(sum(map(lambda x: x.hashrate, self.hashboards)), 2) hr_data = []
for item in self.hashboards:
if item.hashrate is not None:
hr_data.append(item.hashrate)
if len(hr_data) > 0:
return sum(hr_data)
return self._hashrate return self._hashrate
@hashrate.setter @hashrate.setter
@@ -206,7 +250,14 @@ class MinerData:
@property @property
def total_chips(self): # noqa - Skip PyCharm inspection def total_chips(self): # noqa - Skip PyCharm inspection
return sum([hb.chips for hb in self.hashboards]) if len(self.hashboards) > 0:
chip_data = []
for item in self.hashboards:
if item.chips is not None:
chip_data.append(item.chips)
if len(chip_data) > 0:
return sum(chip_data)
return None
@total_chips.setter @total_chips.setter
def total_chips(self, val): def total_chips(self, val):
@@ -214,6 +265,8 @@ class MinerData:
@property @property
def nominal(self): # noqa - Skip PyCharm inspection def nominal(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.ideal_chips is None:
return None
return self.ideal_chips == self.total_chips return self.ideal_chips == self.total_chips
@nominal.setter @nominal.setter
@@ -222,6 +275,8 @@ class MinerData:
@property @property
def percent_ideal_chips(self): # noqa - Skip PyCharm inspection def percent_ideal_chips(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.ideal_chips is None:
return None
if self.total_chips == 0 or self.ideal_chips == 0: if self.total_chips == 0 or self.ideal_chips == 0:
return 0 return 0
return round((self.total_chips / self.ideal_chips) * 100) return round((self.total_chips / self.ideal_chips) * 100)
@@ -232,6 +287,8 @@ class MinerData:
@property @property
def percent_ideal_hashrate(self): # noqa - Skip PyCharm inspection def percent_ideal_hashrate(self): # noqa - Skip PyCharm inspection
if self.hashrate is None or self.nominal_hashrate is None:
return None
if self.hashrate == 0 or self.nominal_hashrate == 0: if self.hashrate == 0 or self.nominal_hashrate == 0:
return 0 return 0
return round((self.hashrate / self.nominal_hashrate) * 100) return round((self.hashrate / self.nominal_hashrate) * 100)
@@ -242,6 +299,8 @@ class MinerData:
@property @property
def percent_ideal_wattage(self): # noqa - Skip PyCharm inspection def percent_ideal_wattage(self): # noqa - Skip PyCharm inspection
if self.wattage_limit is None or self.wattage is None:
return None
if self.wattage_limit == 0 or self.wattage == 0: if self.wattage_limit == 0 or self.wattage == 0:
return 0 return 0
return round((self.wattage / self.wattage_limit) * 100) return round((self.wattage / self.wattage_limit) * 100)
@@ -255,11 +314,11 @@ class MinerData:
total_temp = 0 total_temp = 0
temp_count = 0 temp_count = 0
for hb in self.hashboards: for hb in self.hashboards:
if hb.temp and not hb.temp == -1: if hb.temp is not None:
total_temp += hb.temp total_temp += hb.temp
temp_count += 1 temp_count += 1
if not temp_count > 0: if not temp_count > 0:
return 0 return None
return round(total_temp / temp_count) return round(total_temp / temp_count)
@temperature_avg.setter @temperature_avg.setter
@@ -268,7 +327,9 @@ class MinerData:
@property @property
def efficiency(self): # noqa - Skip PyCharm inspection def efficiency(self): # noqa - Skip PyCharm inspection
if self.hashrate == 0 or self.wattage == -1: if self.hashrate is None or self.wattage is None:
return None
if self.hashrate == 0 or self.wattage == 0:
return 0 return 0
return round(self.wattage / self.hashrate) return round(self.wattage / self.hashrate)
@@ -277,13 +338,16 @@ class MinerData:
pass pass
def asdict(self) -> dict: def asdict(self) -> dict:
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
return asdict(self)
def as_dict(self) -> dict:
"""Get this dataclass as a dictionary. """Get this dataclass as a dictionary.
Returns: Returns:
A dictionary version of this class. A dictionary version of this class.
""" """
logging.debug(f"MinerData - (To Dict) - Dumping Dict data") return self.asdict()
return asdict(self)
def as_json(self) -> str: def as_json(self) -> str:
"""Get this dataclass as JSON. """Get this dataclass as JSON.
@@ -328,7 +392,7 @@ class MinerData:
tags = ["ip", "mac", "model", "hostname"] tags = ["ip", "mac", "model", "hostname"]
for attribute in self: for attribute in self:
if attribute in tags: if attribute in tags:
escaped_data = self[attribute].replace(" ", "\\ ") escaped_data = self.get(attribute, "Unknown").replace(" ", "\\ ")
tag_data.append(f"{attribute}={escaped_data}") tag_data.append(f"{attribute}={escaped_data}")
continue continue
elif str(attribute).startswith("_"): elif str(attribute).startswith("_"):
@@ -345,26 +409,28 @@ class MinerData:
elif isinstance(self[attribute], float): elif isinstance(self[attribute], float):
field_data.append(f"{attribute}={self[attribute]}") field_data.append(f"{attribute}={self[attribute]}")
continue continue
elif attribute == "fault_light" and not self[attribute]:
field_data.append(f"{attribute}=false")
continue
elif attribute == "errors": elif attribute == "errors":
for idx, item in enumerate(self[attribute]): for idx, item in enumerate(self[attribute]):
field_data.append(f'error_{idx+1}="{item.error_message}"') field_data.append(f'error_{idx+1}="{item.error_message}"')
elif attribute == "hashboards": elif attribute == "hashboards":
for idx, item in enumerate(self[attribute]): for idx, item in enumerate(self[attribute]):
field_data.append(f"hashboard_{idx+1}_hashrate={item.hashrate}")
field_data.append(f"hashboard_{idx+1}_temperature={item.temp}")
field_data.append( field_data.append(
f"hashboard_{idx+1}_chip_temperature={item.chip_temp}" f"hashboard_{idx+1}_hashrate={item.get('hashrate', 0.0)}"
) )
field_data.append(f"hashboard_{idx+1}_chips={item.chips}")
field_data.append( field_data.append(
f"hashboard_{idx+1}_expected_chips={item.expected_chips}" f"hashboard_{idx+1}_temperature={item.get('temp', 0)}"
)
field_data.append(
f"hashboard_{idx+1}_chip_temperature={item.get('chip_temp', 0)}"
)
field_data.append(f"hashboard_{idx+1}_chips={item.get('chips', 0)}")
field_data.append(
f"hashboard_{idx+1}_expected_chips={item.get('expected_chips', 0)}"
) )
elif attribute == "fans": elif attribute == "fans":
for idx, item in enumerate(self[attribute]): for idx, item in enumerate(self[attribute]):
field_data.append(f"fan_{idx+1}={item.speed}") if item.speed is not None:
field_data.append(f"fan_{idx+1}={item.speed}")
tags_str = ",".join(tag_data) tags_str = ",".join(tag_data)
field_str = ",".join(field_data) field_str = ",".join(field_data)

View File

@@ -16,8 +16,6 @@
from dataclasses import asdict, dataclass, field, fields from dataclasses import asdict, dataclass, field, fields
C_N_CODES = ["52", "53", "54", "55", "56"]
@dataclass @dataclass
class WhatsminerError: class WhatsminerError:
@@ -37,10 +35,8 @@ class WhatsminerError:
@property @property
def error_message(self): # noqa - Skip PyCharm inspection def error_message(self): # noqa - Skip PyCharm inspection
if len(str(self.error_code)) > 3 and str(self.error_code)[:2] in C_N_CODES: if len(str(self.error_code)) == 6 and not str(self.error_code)[:1] == "1":
# 55 error code base has chip numbers, so the format is err_type = int(str(self.error_code)[:2])
# 55 -> board num len 1 -> chip num len 3
err_type = 55
err_subtype = int(str(self.error_code)[2:3]) err_subtype = int(str(self.error_code)[2:3])
err_value = int(str(self.error_code)[3:]) err_value = int(str(self.error_code)[3:])
else: else:
@@ -88,7 +84,9 @@ class WhatsminerError:
ERROR_CODES = { ERROR_CODES = {
1: { # Fan error 1: { # Fan error
0: {0: "Fan unknown."}, 0: {
0: "Fan unknown.",
},
1: { # Fan speed error of 1000+ 1: { # Fan speed error of 1000+
0: "Intake fan speed error.", 0: "Intake fan speed error.",
1: "Exhaust fan speed error.", 1: "Exhaust fan speed error.",
@@ -101,7 +99,9 @@ ERROR_CODES = {
0: "Intake fan speed error. Fan speed deviates by more than 3000.", 0: "Intake fan speed error. Fan speed deviates by more than 3000.",
1: "Exhaust fan speed error. Fan speed deviates by more than 3000.", 1: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
}, },
4: {0: "Fan speed too high."}, # High speed 4: {
0: "Fan speed too high.",
}, # High speed
}, },
2: { # Power error 2: { # Power error
0: { 0: {
@@ -126,6 +126,7 @@ ERROR_CODES = {
6: "Power remained unchanged for a long time.", 6: "Power remained unchanged for a long time.",
7: "Power set enable error.", 7: "Power set enable error.",
8: "Power input voltage is lower than 230V for high power mode.", 8: "Power input voltage is lower than 230V for high power mode.",
9: "Power input current is incorrect.",
}, },
3: { 3: {
3: "Power output high temperature protection error.", 3: "Power output high temperature protection error.",
@@ -159,6 +160,8 @@ ERROR_CODES = {
6: { 6: {
3: "Power communication warning.", 3: "Power communication warning.",
4: "Power communication error.", 4: "Power communication error.",
5: "Power unknown error.",
6: "Power unknown error.",
7: "Power watchdog protection.", 7: "Power watchdog protection.",
8: "Power output high current protection.", 8: "Power output high current protection.",
9: "Power input high current protection.", 9: "Power input high current protection.",
@@ -170,57 +173,134 @@ ERROR_CODES = {
3: "Power input too high warning.", 3: "Power input too high warning.",
4: "Power fan warning.", 4: "Power fan warning.",
5: "Power high temperature warning.", 5: "Power high temperature warning.",
6: "Power unknown error.",
7: "Power unknown error.",
8: "Power unknown error.",
9: "Power unknown error.",
},
8: {
0: "Power unknown error.",
1: "Power vendor status 1 bit 0 error.",
2: "Power vendor status 1 bit 1 error.",
3: "Power vendor status 1 bit 2 error.",
4: "Power vendor status 1 bit 3 error.",
5: "Power vendor status 1 bit 4 error.",
6: "Power vendor status 1 bit 5 error.",
7: "Power vendor status 1 bit 6 error.",
8: "Power vendor status 1 bit 7 error.",
9: "Power vendor status 2 bit 0 error.",
},
9: {
0: "Power vendor status 2 bit 1 error.",
1: "Power vendor status 2 bit 2 error.",
2: "Power vendor status 2 bit 3 error.",
3: "Power vendor status 2 bit 4 error.",
4: "Power vendor status 2 bit 5 error.",
5: "Power vendor status 2 bit 6 error.",
6: "Power vendor status 2 bit 7 error.",
}, },
}, },
3: { # temperature error 3: { # temperature error
0: { # sensor detection error 0: { # sensor detection error
"n": "Slot {n} temperature sensor detection error." "n": "Slot {n} temperature sensor detection error.",
}, },
2: { # temperature reading error 2: { # temperature reading error
"n": "Slot {n} temperature reading error.", "n": "Slot {n} temperature reading error.",
9: "Control board temperature sensor communication error.", 9: "Control board temperature sensor communication error.",
}, },
5: {"n": "Slot {n} temperature protecting."}, # temperature protection 5: {
6: {0: "Hashboard high temperature error."}, # high temp "n": "Slot {n} temperature protecting.",
}, # temperature protection
6: {
0: "Hashboard high temperature error.",
1: "Hashboard high temperature error.",
2: "Hashboard high temperature error.",
3: "Hashboard high temperature error.",
}, # high temp
7: {
0: "The environment temperature fluctuates too much.",
}, # env temp
8: { 8: {
0: "Humidity sensor not found.", 0: "Humidity sensor not found.",
1: "Humidity sensor read error.", 1: "Humidity sensor read error.",
2: "Humidity sensor read error.", 2: "Humidity sensor read error.",
3: "Humidity sensor protecting.", 3: "Humidity sensor protecting.",
}, }, # humidity
}, },
4: { # EEPROM error 4: { # EEPROM error
0: {0: "Eeprom unknown error."}, 0: {
1: {"n": "Slot {n} eeprom detection error."}, # EEPROM detection error 0: "Eeprom unknown error.",
2: {"n": "Slot {n} eeprom parsing error."}, # EEPROM parsing error },
3: {"n": "Slot {n} chip bin type error."}, # chip bin error 1: {
4: {"n": "Slot {n} eeprom chip number X error."}, # EEPROM chip number error "n": "Slot {n} eeprom detection error.",
5: {"n": "Slot {n} eeprom xfer error."}, # EEPROM xfer error }, # EEPROM detection error
2: {
"n": "Slot {n} eeprom parsing error.",
}, # EEPROM parsing error
3: {
"n": "Slot {n} chip bin type error.",
}, # chip bin error
4: {
"n": "Slot {n} eeprom chip number X error.",
}, # EEPROM chip number error
5: {
"n": "Slot {n} eeprom xfer error.",
}, # EEPROM xfer error
}, },
5: { # hashboard error 5: { # hashboard error
0: {0: "Board unknown error."}, 0: {
1: {"n": "Slot {n} miner type error."}, # board miner type error 0: "Board unknown error.",
2: {"n": "Slot {n} bin type error."}, # chip bin type error },
3: {"n": "Slot {n} not found."}, # board not found error 1: {
4: {"n": "Slot {n} error reading chip id."}, # reading chip id error "n": "Slot {n} miner type error.",
5: {"n": "Slot {n} has bad chips."}, # board has bad chips error }, # board miner type error
6: {"n": "Slot {n} loss of balance error."}, # loss of balance error 2: {
7: {"n": "Slot {n} xfer error chip."}, # xfer error "n": "Slot {n} bin type error.",
8: {"n": "Slot {n} reset error."}, # reset error }, # chip bin type error
9: {"n": "Slot {n} frequency too low."}, # freq error 3: {
"n": "Slot {n} not found.",
}, # board not found error
4: {
"n": "Slot {n} error reading chip id.",
}, # reading chip id error
5: {
"n": "Slot {n} has bad chips.",
}, # board has bad chips error
6: {
"n": "Slot {n} loss of balance error.",
}, # loss of balance error
7: {
"n": "Slot {n} xfer error chip.",
}, # xfer error
8: {
"n": "Slot {n} reset error.",
}, # reset error
9: {
"n": "Slot {n} frequency too low.",
}, # freq error
}, },
6: { # env temp error 6: { # env temp error
0: {0: "Environment temperature is too high."}, # normal env temp error 0: {
0: "Environment temperature is too high.",
}, # normal env temp error
1: { # high power env temp error 1: { # high power env temp error
0: "Environment temperature is too high for high performance mode." 0: "Environment temperature is too high for high performance mode.",
}, },
}, },
7: { # control board error 7: { # control board error
0: {0: "MAC address invalid", 1: "Control board no support chip."}, 0: {
0: "MAC address invalid",
1: "Control board no support chip.",
},
1: { 1: {
0: "Control board rebooted as an exception.", 0: "Control board rebooted as an exception.",
1: "Control board rebooted as exception and cpufreq reduced, please upgrade the firmware", 1: "Control board rebooted as exception and cpufreq reduced, please upgrade the firmware",
2: "Control board rebooted as an exception.", 2: "Control board rebooted as an exception.",
3: "The network is unstable, change time.",
4: "Unknown error.",
},
2: {
"n": "Control board slot {n} frame error.",
}, },
}, },
8: { # checksum error 8: { # checksum error
@@ -228,63 +308,152 @@ ERROR_CODES = {
0: "CGMiner checksum error.", 0: "CGMiner checksum error.",
1: "System monitor checksum error.", 1: "System monitor checksum error.",
2: "Remote daemon checksum error.", 2: "Remote daemon checksum error.",
} },
1: {0: "Air to liquid PCB serial # does not match."},
}, },
9: {0: {1: "Power rate error."}}, # power rate error 9: {
0: {0: "Unknown error.", 1: "Power rate error.", 2: "Unknown error."}
}, # power rate error
20: { # pool error 20: { # pool error
1: {0: "All pools are disabled."}, # all disabled error 0: {
2: {"n": "Pool {n} connection failed."}, # pool connection failed error 0: "No pool information configured.",
3: {0: "High rejection rate on pool."}, # rejection rate error },
1: {
0: "All pools are disabled.",
}, # all disabled error
2: {
"n": "Pool {n} connection failed.",
}, # pool connection failed error
3: {
0: "High rejection rate on pool.",
}, # rejection rate error
4: { # asicboost not supported error 4: { # asicboost not supported error
0: "The pool does not support asicboost mode." 0: "The pool does not support asicboost mode.",
}, },
}, },
21: {1: {"n": "Slot {n} factory test step failed."}}, 21: {
1: {
"n": "Slot {n} factory test step failed.",
}
},
23: { # hashrate error 23: { # hashrate error
1: {0: "Hashrate is too low."}, 1: {
2: {0: "Hashrate is too low."}, 0: "Hashrate is too low.",
3: {0: "Hashrate loss is too high."}, },
4: {0: "Hashrate loss is too high."}, 2: {
5: {0: "Hashrate loss."}, 0: "Hashrate is too low.",
},
3: {
0: "Hashrate loss is too high.",
},
4: {
0: "Hashrate loss is too high.",
},
5: {
0: "Hashrate loss.",
},
}, },
50: { # water velocity error/voltage error 50: { # water velocity error/voltage error
1: {"n": "Slot {n} chip voltage too low."}, 1: {
2: {"n": "Slot {n} chip voltage changed."}, "n": "Slot {n} chip voltage too low.",
3: {"n": "Slot {n} chip temperature difference is too large."}, },
4: {"n": "Slot {n} chip hottest temperature difference is too large."}, 2: {
7: {"n": "Slot {n} water velocity is abnormal."}, # abnormal water velocity "n": "Slot {n} chip voltage changed.",
8: {0: "Chip temp calibration failed, please restore factory settings."}, },
9: {"n": "Slot {n} chip temp calibration check no balance."}, 3: {
"n": "Slot {n} chip temperature difference is too large.",
},
4: {
"n": "Slot {n} chip hottest temperature difference is too large.",
},
5: {"n": "Slot {n} stopped hashing, chips temperature protecting."},
7: {
"n": "Slot {n} water velocity is abnormal.",
}, # abnormal water velocity
8: {
0: "Chip temp calibration failed, please restore factory settings.",
},
9: {
"n": "Slot {n} chip temp calibration check no balance.",
},
}, },
51: { # frequency error 51: { # frequency error
1: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout 1: {
7: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout "n": "Slot {n} frequency up timeout.",
}, # frequency up timeout
2: {"n": "Slot {n} too many CRC errors."},
3: {"n": "Slot {n} unstable."},
7: {
"n": "Slot {n} frequency up timeout.",
}, # frequency up timeout
},
52: {
"n": {
"c": "Slot {n} chip {c} error nonce.",
},
},
53: {
"n": {
"c": "Slot {n} chip {c} too few nonce.",
},
},
54: {
"n": {
"c": "Slot {n} chip {c} temp protected.",
},
},
55: {
"n": {
"c": "Slot {n} chip {c} has been reset.",
},
},
56: {
"n": {
"c": "Slot {n} chip {c} zero nonce.",
},
}, },
52: {"n": {"c": "Slot {n} chip {c} error nonce."}},
53: {"n": {"c": "Slot {n} chip {c} too few nonce."}},
54: {"n": {"c": "Slot {n} chip {c} temp protected."}},
55: {"n": {"c": "Slot {n} chip {c} has been reset."}},
56: {"n": {"c": "Slot {n} chip {c} does not return to the nonce."}},
80: { 80: {
0: {0: "The tool version is too low, please update."}, 0: {
1: {0: "Low freq."}, 0: "The tool version is too low, please update.",
2: {0: "Low hashrate."}, },
3: {5: "High env temp."}, 1: {
0: "Low freq.",
},
2: {
0: "Low hashrate.",
},
3: {
5: "High env temp.",
},
}, },
81: { 81: {
0: {0: "Chip data error."}, 0: {
0: "Chip data error.",
},
}, },
82: { 82: {
0: {0: "Power version error."}, 0: {
1: {0: "Miner type error."}, 0: "Power version error.",
2: {0: "Version info error."}, },
1: {
0: "Miner type error.",
},
2: {
0: "Version info error.",
},
}, },
83: { 83: {
0: {0: "Empty level error."}, 0: {
0: "Empty level error.",
},
}, },
84: { 84: {
0: {0: "Old firmware."}, 0: {
1: {0: "Software version error."}, 0: "Old firmware.",
},
1: {
0: "Software version error.",
},
}, },
85: { 85: {
"n": { "n": {
@@ -296,8 +465,12 @@ ERROR_CODES = {
}, },
}, },
86: { 86: {
0: {0: "Missing product serial #."}, 0: {
1: {0: "Missing product type."}, 0: "Missing product serial #.",
},
1: {
0: "Missing product type.",
},
2: { 2: {
0: "Missing miner serial #.", 0: "Missing miner serial #.",
1: "Wrong miner serial # length.", 1: "Wrong miner serial # length.",
@@ -314,12 +487,34 @@ ERROR_CODES = {
3: "Wrong power model rate.", 3: "Wrong power model rate.",
4: "Wrong power model format.", 4: "Wrong power model format.",
}, },
5: {0: "Wrong hash board struct."}, 5: {
6: {0: "Wrong miner cooling type."}, 0: "Wrong hash board struct.",
7: {0: "Missing PCB serial #."}, },
6: {
0: "Wrong miner cooling type.",
},
7: {
0: "Missing PCB serial #.",
},
},
87: {
0: {
0: "Miner power mismatch.",
},
},
90: {
0: {
0: "Process error, exited with signal: 3.",
},
1: {
0: "Process error, exited with signal: 3.",
},
},
99: {
9: {
9: "Miner unknown error.",
},
}, },
87: {0: {0: "Miner power mismatch."}},
99: {9: {9: "Miner unknown error."}},
1000: { 1000: {
0: { 0: {
0: "Security library error, please upgrade firmware", 0: "Security library error, please upgrade firmware",
@@ -328,7 +523,11 @@ ERROR_CODES = {
3: "/antiv/dig/pf_partial.dig illegal.", 3: "/antiv/dig/pf_partial.dig illegal.",
}, },
}, },
1001: {0: {0: "Security BTMiner removed, please upgrade firmware."}}, 1001: {
0: {
0: "Security BTMiner removed, please upgrade firmware.",
},
},
1100: { 1100: {
0: { 0: {
0: "Security illegal file, please upgrade firmware.", 0: "Security illegal file, please upgrade firmware.",

View File

@@ -149,10 +149,10 @@ class _MinerPhaseBalancer:
not self.miners[data_point.ip]["shutdown"] not self.miners[data_point.ip]["shutdown"]
): ):
# cant do anything with it so need to find a semi-accurate power limit # cant do anything with it so need to find a semi-accurate power limit
if not data_point.wattage_limit == -1: if not data_point.wattage_limit == None:
self.miners[data_point.ip]["max"] = int(data_point.wattage_limit) self.miners[data_point.ip]["max"] = int(data_point.wattage_limit)
self.miners[data_point.ip]["min"] = int(data_point.wattage_limit) self.miners[data_point.ip]["min"] = int(data_point.wattage_limit)
elif not data_point.wattage == -1: elif not data_point.wattage == None:
self.miners[data_point.ip]["max"] = int(data_point.wattage) self.miners[data_point.ip]["max"] = int(data_point.wattage)
self.miners[data_point.ip]["min"] = int(data_point.wattage) self.miners[data_point.ip]["min"] = int(data_point.wattage)

View File

@@ -16,31 +16,29 @@
import logging import logging
from pyasic.settings import PyasicSettings
def init_logger(): def init_logger():
if PyasicSettings().logfile: # if PyasicSettings().logfile:
logging.basicConfig( # logging.basicConfig(
filename="logfile.txt", # filename="logfile.txt",
filemode="a", # filemode="a",
format="%(pathname)s:%(lineno)d in %(funcName)s\n[%(levelname)s][%(asctime)s](%(name)s) - %(message)s", # format="%(pathname)s:%(lineno)d in %(funcName)s\n[%(levelname)s][%(asctime)s](%(name)s) - %(message)s",
datefmt="%x %X", # datefmt="%x %X",
) # )
else: # else:
logging.basicConfig( logging.basicConfig(
format="%(pathname)s:%(lineno)d in %(funcName)s\n[%(levelname)s][%(asctime)s](%(name)s) - %(message)s", format="%(pathname)s:%(lineno)d in %(funcName)s\n[%(levelname)s][%(asctime)s](%(name)s) - %(message)s",
datefmt="%x %X", datefmt="%x %X",
) )
_logger = logging.getLogger() _logger = logging.getLogger()
if PyasicSettings().debug: # if PyasicSettings().debug:
_logger.setLevel(logging.DEBUG) # _logger.setLevel(logging.DEBUG)
logging.getLogger("asyncssh").setLevel(logging.DEBUG) # logging.getLogger("asyncssh").setLevel(logging.DEBUG)
else: # else:
_logger.setLevel(logging.WARNING) _logger.setLevel(logging.WARNING)
logging.getLogger("asyncssh").setLevel(logging.WARNING) logging.getLogger("asyncssh").setLevel(logging.WARNING)
return _logger return _logger

View File

@@ -21,9 +21,11 @@ from pyasic.miners.types import (
S19XP, S19XP,
S19a, S19a,
S19aPro, S19aPro,
S19i,
S19j, S19j,
S19jNoPIC, S19jNoPIC,
S19jPro, S19jPro,
S19Plus,
S19Pro, S19Pro,
S19ProPlus, S19ProPlus,
) )
@@ -33,6 +35,14 @@ class BMMinerS19(AntminerModern, S19):
pass pass
class BMMinerS19Plus(AntminerModern, S19Plus):
pass
class BMMinerS19i(AntminerModern, S19i):
pass
class BMMinerS19Pro(AntminerModern, S19Pro): class BMMinerS19Pro(AntminerModern, S19Pro):
pass pass

View File

@@ -18,10 +18,12 @@ from .S19 import (
BMMinerS19, BMMinerS19,
BMMinerS19a, BMMinerS19a,
BMMinerS19aPro, BMMinerS19aPro,
BMMinerS19i,
BMMinerS19j, BMMinerS19j,
BMMinerS19jNoPIC, BMMinerS19jNoPIC,
BMMinerS19jPro, BMMinerS19jPro,
BMMinerS19L, BMMinerS19L,
BMMinerS19Plus,
BMMinerS19Pro, BMMinerS19Pro,
BMMinerS19ProPlus, BMMinerS19ProPlus,
BMMinerS19XP, BMMinerS19XP,

View File

@@ -58,7 +58,7 @@ class HiveonT9(Hiveon, T9):
hashrate = 0 hashrate = 0
chips = 0 chips = 0
for chipset in board_map[board]: for chipset in board_map[board]:
if hashboard.chip_temp == -1: if hashboard.chip_temp == None:
try: try:
hashboard.board_temp = api_stats["STATS"][1][f"temp{chipset}"] hashboard.board_temp = api_stats["STATS"][1][f"temp{chipset}"]
hashboard.chip_temp = api_stats["STATS"][1][f"temp2_{chipset}"] hashboard.chip_temp = api_stats["STATS"][1][f"temp2_{chipset}"]

View File

@@ -26,11 +26,17 @@ from pyasic.miners.backends.cgminer import CGMiner
from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
ANTMINER_MODERN_DATA_LOC = { ANTMINER_MODERN_DATA_LOC = {
"mac": {"cmd": "get_mac", "kwargs": {}}, "mac": {
"cmd": "get_mac",
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
},
"model": {"cmd": "get_model", "kwargs": {}}, "model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}}, "api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}},
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}}, "fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {}}, "hostname": {
"cmd": "get_hostname",
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}}, "hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": { "nominal_hashrate": {
"cmd": "get_nominal_hashrate", "cmd": "get_nominal_hashrate",
@@ -42,8 +48,11 @@ ANTMINER_MODERN_DATA_LOC = {
"wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}}, "wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}},
"fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}}, "fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}},
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}}, "fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
"errors": {"cmd": "get_errors", "kwargs": {}}, "errors": {"cmd": "get_errors", "kwargs": {"web_summary": {"web": "summary"}}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {}}, "fault_light": {
"cmd": "get_fault_light",
"kwargs": {"web_get_blink_status": {"web": "get_blink_status"}},
},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}}, "pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": { "is_mining": {
"cmd": "is_mining", "cmd": "is_mining",
@@ -100,7 +109,7 @@ class AntminerModern(BMMiner):
data = await self.web.blink(blink=False) data = await self.web.blink(blink=False)
if data: if data:
if data.get("code") == "B100": if data.get("code") == "B100":
self.light = True self.light = False
return self.light return self.light
async def reboot(self) -> bool: async def reboot(self) -> bool:
@@ -121,21 +130,31 @@ class AntminerModern(BMMiner):
await self.send_config(cfg) await self.send_config(cfg)
return True return True
async def get_hostname(self) -> Union[str, None]: async def get_hostname(self, web_get_system_info: dict = None) -> Union[str, None]:
try: if not web_get_system_info:
data = await self.web.get_system_info() try:
if data: web_get_system_info = await self.web.get_system_info()
return data["hostname"] except APIError:
except KeyError: pass
pass
async def get_mac(self) -> Union[str, None]: if web_get_system_info:
try: try:
data = await self.web.get_system_info() return web_get_system_info["hostname"]
if data: except KeyError:
return data["macaddr"] pass
except KeyError:
pass async def get_mac(self, web_get_system_info: dict = None) -> Union[str, None]:
if not web_get_system_info:
try:
web_get_system_info = await self.web.get_system_info()
except APIError:
pass
if web_get_system_info:
try:
return web_get_system_info["macaddr"]
except KeyError:
pass
try: try:
data = await self.web.get_network_info() data = await self.web.get_network_info()
@@ -144,12 +163,17 @@ class AntminerModern(BMMiner):
except KeyError: except KeyError:
pass pass
async def get_errors(self) -> List[MinerErrorData]: async def get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
errors = [] if not web_summary:
data = await self.web.summary()
if data:
try: try:
for item in data["SUMMARY"][0]["status"]: web_summary = await self.web.summary()
except APIError:
pass
errors = []
if web_summary:
try:
for item in web_summary["SUMMARY"][0]["status"]:
try: try:
if not item["status"] == "s": if not item["status"] == "s":
errors.append(X19Error(item["msg"])) errors.append(X19Error(item["msg"]))
@@ -159,15 +183,21 @@ class AntminerModern(BMMiner):
pass pass
return errors return errors
async def get_fault_light(self) -> bool: async def get_fault_light(self, web_get_blink_status: dict = None) -> bool:
if self.light: if self.light:
return self.light return self.light
try:
data = await self.web.get_blink_status() if not web_get_blink_status:
if data: try:
self.light = data["blink"] web_get_blink_status = await self.web.get_blink_status()
except KeyError: except APIError:
pass pass
if web_get_blink_status:
try:
self.light = web_get_blink_status["blink"]
except KeyError:
pass
return self.light return self.light
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]: async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
@@ -257,7 +287,7 @@ class AntminerModern(BMMiner):
if api_stats: if api_stats:
try: try:
return int(api_stats["STATS"][0]["Elapsed"]) return int(api_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass pass
@@ -502,6 +532,6 @@ class AntminerOld(CGMiner):
if api_stats: if api_stats:
try: try:
return int(api_stats["STATS"][0]["Elapsed"]) return int(api_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass pass

View File

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

View File

@@ -235,7 +235,22 @@ class BMMiner(BaseMiner):
if board_offset == -1: if board_offset == -1:
board_offset = 1 board_offset = 1
for i in range(board_offset, board_offset + self.ideal_hashboards): real_slots = []
for i in range(board_offset, board_offset + 4):
try:
key = f"chain_acs{i}"
if boards[1].get(key, "") != "":
real_slots.append(i)
except LookupError:
pass
if len(real_slots) < 3:
real_slots = list(
range(board_offset, board_offset + self.ideal_hashboards)
)
for i in real_slots:
hashboard = HashBoard( hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips slot=i - board_offset, expected_chips=self.nominal_chips
) )
@@ -259,7 +274,7 @@ class BMMiner(BaseMiner):
if (not chips) or (not chips > 0): if (not chips) or (not chips > 0):
hashboard.missing = True hashboard.missing = True
hashboards.append(hashboard) hashboards.append(hashboard)
except (IndexError, KeyError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass pass
return hashboards return hashboards
@@ -370,6 +385,6 @@ class BMMiner(BaseMiner):
if api_stats: if api_stats:
try: try:
return int(api_stats["STATS"][0]["Elapsed"]) return int(api_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass pass

View File

@@ -174,7 +174,7 @@ BOSMINER_DATA_LOC = {
}, },
"is_mining": { "is_mining": {
"cmd": "is_mining", "cmd": "is_mining",
"kwargs": {"api_tunerstatus": {"api": "tunerstatus"}}, "kwargs": {"api_devdetails": {"api": "devdetails"}},
}, },
"uptime": { "uptime": {
"cmd": "get_uptime", "cmd": "get_uptime",
@@ -303,17 +303,12 @@ class BOSMiner(BaseMiner):
The config from `self.config`. The config from `self.config`.
""" """
logging.debug(f"{self}: Getting config.") logging.debug(f"{self}: Getting config.")
conn = None
try: try:
conn = await self._get_ssh_connection() conn = await self._get_ssh_connection()
except ConnectionError: except ConnectionError:
try: conn = None
pools = await self.api.pools()
except APIError:
return self.config
if pools:
self.config = MinerConfig().from_api(pools["POOLS"])
return self.config
if conn: if conn:
async with conn: async with conn:
# good ol' BBB compatibility :/ # good ol' BBB compatibility :/
@@ -365,6 +360,8 @@ class BOSMiner(BaseMiner):
async def set_power_limit(self, wattage: int) -> bool: async def set_power_limit(self, wattage: int) -> bool:
try: try:
cfg = await self.get_config() cfg = await self.get_config()
if cfg is None:
return False
cfg.autotuning_wattage = wattage cfg.autotuning_wattage = wattage
await self.send_config(cfg) await self.send_config(cfg)
except Exception as e: except Exception as e:
@@ -373,6 +370,52 @@ class BOSMiner(BaseMiner):
else: else:
return True 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}) ### ### DATA GATHERING FUNCTIONS (get_{some_data}) ###
################################################## ##################################################
@@ -386,8 +429,6 @@ class BOSMiner(BaseMiner):
except APIError: except APIError:
pass pass
print(web_net_conf)
if isinstance(web_net_conf, dict): if isinstance(web_net_conf, dict):
if "/cgi-bin/luci/admin/network/iface_status/lan" in web_net_conf.keys(): if "/cgi-bin/luci/admin/network/iface_status/lan" in web_net_conf.keys():
web_net_conf = web_net_conf[ web_net_conf = web_net_conf[
@@ -450,7 +491,7 @@ class BOSMiner(BaseMiner):
if graphql_version: if graphql_version:
try: try:
fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"] fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"]
except KeyError: except (KeyError, TypeError):
pass pass
if not fw_ver: if not fw_ver:
@@ -479,7 +520,7 @@ class BOSMiner(BaseMiner):
try: try:
hostname = graphql_hostname["data"]["bos"]["hostname"] hostname = graphql_hostname["data"]["bos"]["hostname"]
return hostname return hostname
except KeyError: except (TypeError, KeyError):
pass pass
try: try:
@@ -519,7 +560,7 @@ class BOSMiner(BaseMiner):
), ),
2, 2,
) )
except (KeyError, IndexError, ValueError): except (LookupError, ValueError, TypeError):
pass pass
# get hr from API # get hr from API
@@ -573,7 +614,7 @@ class BOSMiner(BaseMiner):
boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][ boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][
"childSolvers" "childSolvers"
] ]
except (KeyError, IndexError): except (TypeError, LookupError):
boards = None boards = None
if boards: if boards:
@@ -688,7 +729,7 @@ class BOSMiner(BaseMiner):
return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][ return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][
"power" "power"
]["approxConsumptionW"] ]["approxConsumptionW"]
except (KeyError, TypeError): except (LookupError, TypeError):
pass pass
if not api_tunerstatus: if not api_tunerstatus:
@@ -721,7 +762,7 @@ class BOSMiner(BaseMiner):
return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][ return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][
"power" "power"
]["limitW"] ]["limitW"]
except (KeyError, TypeError): except (LookupError, TypeError):
pass pass
if not api_tunerstatus: if not api_tunerstatus:
@@ -757,7 +798,7 @@ class BOSMiner(BaseMiner):
] ]
) )
) )
except KeyError: except (LookupError, TypeError):
pass pass
return fans return fans
@@ -897,7 +938,7 @@ class BOSMiner(BaseMiner):
boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][ boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][
"childSolvers" "childSolvers"
] ]
except (KeyError, IndexError): except (LookupError, TypeError):
boards = None boards = None
if boards: if boards:
@@ -990,17 +1031,20 @@ class BOSMiner(BaseMiner):
try: try:
self.light = graphql_fault_light["data"]["bos"]["faultLight"] self.light = graphql_fault_light["data"]["bos"]["faultLight"]
return self.light return self.light
except (TypeError, KeyError, ValueError, IndexError): except (TypeError, ValueError, LookupError):
pass pass
# get light via ssh if that fails (10x slower) # get light via ssh if that fails (10x slower)
data = ( try:
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off") data = (
).strip() await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
self.light = False ).strip()
if data == "50": self.light = False
self.light = True if data == "50":
return self.light self.light = True
return self.light
except TypeError:
return self.light
async def get_nominal_hashrate(self, api_devs: dict = None) -> Optional[float]: async def get_nominal_hashrate(self, api_devs: dict = None) -> Optional[float]:
if not api_devs: if not api_devs:
@@ -1028,22 +1072,18 @@ class BOSMiner(BaseMiner):
except (IndexError, KeyError): except (IndexError, KeyError):
pass pass
async def is_mining(self, api_tunerstatus: dict = None) -> Optional[bool]: async def is_mining(self, api_devdetails: dict = None) -> Optional[bool]:
if not api_tunerstatus: if not api_devdetails:
try: try:
api_tunerstatus = await self.api.tunerstatus() api_devdetails = await self.api.send_command(
"devdetails", ignore_errors=True, allow_warning=False
)
except APIError: except APIError:
pass pass
if api_tunerstatus: if api_devdetails:
try: try:
running = any( return not api_devdetails["STATUS"][0]["Msg"] == "Unavailable"
[
d["TunerRunning"]
for d in api_tunerstatus["TUNERSTATUS"][0]["TunerChainStatus"]
]
)
return running
except LookupError: except LookupError:
pass pass

View File

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

View File

@@ -179,11 +179,12 @@ class CGMinerAvalon(CGMiner):
pass pass
async def get_hostname(self, mac: str = None) -> Optional[str]: async def get_hostname(self, mac: str = None) -> Optional[str]:
if not mac: return None
mac = await self.get_mac() # if not mac:
# mac = await self.get_mac()
if mac: #
return f"Avalon{mac.replace(':', '')[-6:]}" # if mac:
# return f"Avalon{mac.replace(':', '')[-6:]}"
async def get_hashrate(self, api_devs: dict = None) -> Optional[float]: async def get_hashrate(self, api_devs: dict = None) -> Optional[float]:
if not api_devs: if not api_devs:

View File

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

View File

@@ -17,6 +17,7 @@
from typing import Optional from typing import Optional
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.miners.backends.bmminer import BMMiner from pyasic.miners.backends.bmminer import BMMiner
from pyasic.web.vnish import VNishWebAPI from pyasic.web.vnish import VNishWebAPI
@@ -73,6 +74,24 @@ class VNish(BMMiner):
pass pass
return False return False
async def stop_mining(self) -> bool:
data = await self.web.stop_mining()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def resume_mining(self) -> bool:
data = await self.web.resume_mining()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def reboot(self) -> bool: async def reboot(self) -> bool:
data = await self.web.reboot() data = await self.web.reboot()
if data: if data:
@@ -144,7 +163,7 @@ class VNish(BMMiner):
float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2 float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2
) )
except (IndexError, KeyError, ValueError, TypeError) as e: except (IndexError, KeyError, ValueError, TypeError) as e:
print(e) logger.error(e)
pass pass
async def get_wattage_limit(self, web_settings: dict = None) -> Optional[int]: async def get_wattage_limit(self, web_settings: dict = None) -> Optional[int]:

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import ipaddress import ipaddress
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@@ -24,6 +24,7 @@ import asyncssh
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data.error_codes import MinerErrorData from pyasic.data.error_codes import MinerErrorData
from pyasic.logger import logger
class BaseMiner(ABC): class BaseMiner(ABC):
@@ -32,6 +33,8 @@ class BaseMiner(ABC):
self.api = None self.api = None
self.web = None self.web = None
self.ssh_pwd = "root"
# static data # static data
self.ip = ip self.ip = ip
self.api_type = None self.api_type = None
@@ -71,6 +74,53 @@ class BaseMiner(ABC):
def __eq__(self, other): def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip) 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):
self.ssh_pwd = 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: async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection""" """Create a new asyncssh connection"""
try: try:
@@ -78,7 +128,7 @@ class BaseMiner(ABC):
str(self.ip), str(self.ip),
known_hosts=None, known_hosts=None,
username="root", username="root",
password="root", password=self.ssh_pwd,
server_host_key_algs=["ssh-rsa"], server_host_key_algs=["ssh-rsa"],
) )
return conn return conn
@@ -363,86 +413,81 @@ class BaseMiner(ABC):
""" """
pass pass
async def _get_data(self, allow_warning: bool, data_to_get: list = None) -> dict: async def _get_data(
if not data_to_get: self, allow_warning: bool, include: list = None, exclude: list = None
) -> dict:
if include is None:
# everything # everything
data_to_get = [ include = list(self.data_locations.keys())
"mac",
"model", if exclude is not None:
"api_ver", for item in exclude:
"fw_ver", if item in include:
"hostname", include.remove(item)
"hashrate",
"nominal_hashrate", api_multicommand = set()
"hashboards",
"env_temp",
"wattage",
"wattage_limit",
"fans",
"fan_psu",
"errors",
"fault_light",
"pools",
"is_mining",
"uptime",
]
api_multicommand = []
web_multicommand = [] web_multicommand = []
for data_name in data_to_get: for data_name in include:
try: try:
fn_args = self.data_locations[data_name]["kwargs"] fn_args = self.data_locations[data_name]["kwargs"]
for arg_name in fn_args: for arg_name in fn_args:
if fn_args[arg_name].get("api"): if fn_args[arg_name].get("api"):
api_multicommand.append(fn_args[arg_name]["api"]) api_multicommand.add(fn_args[arg_name]["api"])
if fn_args[arg_name].get("web"): if fn_args[arg_name].get("web"):
web_multicommand.append(fn_args[arg_name]["web"]) if not fn_args[arg_name]["web"] in web_multicommand:
web_multicommand.append(fn_args[arg_name]["web"])
except KeyError as e: except KeyError as e:
print(e, data_name) logger.error(e, data_name)
continue continue
api_multicommand = list(set(api_multicommand))
_web_multicommand = web_multicommand
for item in web_multicommand:
if item not in _web_multicommand:
_web_multicommand.append(item)
web_multicommand = _web_multicommand
if len(api_multicommand) > 0: if len(api_multicommand) > 0:
api_command_data = await self.api.multicommand( api_command_task = asyncio.create_task(
*api_multicommand, allow_warning=allow_warning self.api.multicommand(*api_multicommand, allow_warning=allow_warning)
) )
else: else:
api_command_data = {} api_command_task = asyncio.sleep(0)
if len(web_multicommand) > 0: if len(web_multicommand) > 0:
web_command_data = await self.web.multicommand( web_command_task = asyncio.create_task(
*web_multicommand, allow_warning=allow_warning self.web.multicommand(*web_multicommand, allow_warning=allow_warning)
) )
else: else:
web_command_task = asyncio.sleep(0)
web_command_data = await web_command_task
if web_command_data is None:
web_command_data = {} web_command_data = {}
api_command_data = await api_command_task
if api_command_data is None:
api_command_data = {}
miner_data = {} miner_data = {}
for data_name in data_to_get: for data_name in include:
try: try:
fn_args = self.data_locations[data_name]["kwargs"] fn_args = self.data_locations[data_name]["kwargs"]
args_to_send = {k: None for k in fn_args} args_to_send = {k: None for k in fn_args}
for arg_name in fn_args: for arg_name in fn_args:
if fn_args[arg_name].get("api"): try:
if api_command_data.get("multicommand"): if fn_args[arg_name].get("api"):
args_to_send[arg_name] = api_command_data[ if api_command_data.get("multicommand"):
fn_args[arg_name]["api"] args_to_send[arg_name] = api_command_data[
][0] fn_args[arg_name]["api"]
else: ][0]
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: else:
if not web_command_data == {"multicommand": False}: args_to_send[arg_name] = api_command_data
args_to_send[arg_name] = web_command_data if fn_args[arg_name].get("web"):
except (KeyError, IndexError): 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:
continue continue
function = getattr(self, self.data_locations[data_name]["cmd"]) function = getattr(self, self.data_locations[data_name]["cmd"])
@@ -457,8 +502,8 @@ class BaseMiner(ABC):
except KeyError: except KeyError:
pass pass
if len(pools_data) > 1: if len(pools_data) > 1:
miner_data["pool_2_url"] = pools_data[1]["pool_2_url"] miner_data["pool_2_url"] = pools_data[1]["pool_1_url"]
miner_data["pool_2_user"] = pools_data[1]["pool_2_user"] miner_data["pool_2_user"] = pools_data[1]["pool_1_user"]
miner_data[ miner_data[
"pool_split" "pool_split"
] = f"{pools_data[0]['quota']}/{pools_data[1]['quota']}" ] = f"{pools_data[0]['quota']}/{pools_data[1]['quota']}"
@@ -472,13 +517,14 @@ class BaseMiner(ABC):
return miner_data return miner_data
async def get_data( async def get_data(
self, allow_warning: bool = False, data_to_get: list = None self, allow_warning: bool = False, include: list = None, exclude: list = None
) -> MinerData: ) -> MinerData:
"""Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData]. """Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData].
Parameters: Parameters:
allow_warning: Allow warning when an API command fails. allow_warning: Allow warning when an API command fails.
data_to_get: Names of data items you want to gather. Defaults to all data. include: Names of data items you want to gather. Defaults to all data.
exclude: Names of data items to exclude. Exclusion happens after considering included items.
Returns: Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing data from the miner. A [`MinerData`][pyasic.data.MinerData] instance containing data from the miner.
@@ -494,7 +540,9 @@ class BaseMiner(ABC):
], ],
) )
gathered_data = await self._get_data(allow_warning, data_to_get=data_to_get) gathered_data = await self._get_data(
allow_warning, include=include, exclude=exclude
)
for item in gathered_data: for item in gathered_data:
if gathered_data[item] is not None: if gathered_data[item] is not None:
setattr(data, item, gathered_data[item]) setattr(data, item, gathered_data[item])

View File

@@ -22,7 +22,7 @@ import json
import re import re
from typing import Callable, List, Optional, Tuple, Union from typing import Callable, List, Optional, Tuple, Union
import aiohttp import httpx
from pyasic.logger import logger from pyasic.logger import logger
from pyasic.miners.antminer import * from pyasic.miners.antminer import *
@@ -85,6 +85,8 @@ MINER_CLASSES = {
"ANTMINER S19L": BMMinerS19L, "ANTMINER S19L": BMMinerS19L,
"ANTMINER S19 PRO": BMMinerS19Pro, "ANTMINER S19 PRO": BMMinerS19Pro,
"ANTMINER S19J": BMMinerS19j, "ANTMINER S19J": BMMinerS19j,
"ANTMINER S19I": BMMinerS19i,
"ANTMINER S19+": BMMinerS19Plus,
"ANTMINER S19J88NOPIC": BMMinerS19jNoPIC, "ANTMINER S19J88NOPIC": BMMinerS19jNoPIC,
"ANTMINER S19PRO+": BMMinerS19ProPlus, "ANTMINER S19PRO+": BMMinerS19ProPlus,
"ANTMINER S19J PRO": BMMinerS19jPro, "ANTMINER S19J PRO": BMMinerS19jPro,
@@ -99,6 +101,8 @@ MINER_CLASSES = {
"M20SV10": BTMinerM20SV10, "M20SV10": BTMinerM20SV10,
"M20SV20": BTMinerM20SV20, "M20SV20": BTMinerM20SV20,
"M20SV30": BTMinerM20SV30, "M20SV30": BTMinerM20SV30,
"M20PV10": BTMinerM20PV10,
"M20PV30": BTMinerM20PV30,
"M20S+V30": BTMinerM20SPlusV30, "M20S+V30": BTMinerM20SPlusV30,
"M21V10": BTMinerM21V10, "M21V10": BTMinerM21V10,
"M21SV20": BTMinerM21SV20, "M21SV20": BTMinerM21SV20,
@@ -319,6 +323,7 @@ MINER_CLASSES = {
"ANTMINER S19J": BOSMinerS19j, "ANTMINER S19J": BOSMinerS19j,
"ANTMINER S19J88NOPIC": BOSMinerS19jNoPIC, "ANTMINER S19J88NOPIC": BOSMinerS19jNoPIC,
"ANTMINER S19J PRO": BOSMinerS19jPro, "ANTMINER S19J PRO": BOSMinerS19jPro,
"ANTMINER S19J PRO NOPIC": BOSMinerS19jPro,
"ANTMINER T19": BOSMinerT19, "ANTMINER T19": BOSMinerT19,
}, },
MinerTypes.VNISH: { MinerTypes.VNISH: {
@@ -455,7 +460,7 @@ class MinerFactory:
async def _get_miner_web(self, ip: str): async def _get_miner_web(self, ip: str):
urls = [f"http://{ip}/", f"https://{ip}/"] urls = [f"http://{ip}/", f"https://{ip}/"]
async with aiohttp.ClientSession() as session: async with httpx.AsyncClient(verify=False) as session:
tasks = [asyncio.create_task(self._web_ping(session, url)) for url in urls] tasks = [asyncio.create_task(self._web_ping(session, url)) for url in urls]
text, resp = await concurrent_get_first_result( text, resp = await concurrent_get_first_result(
@@ -466,26 +471,26 @@ class MinerFactory:
@staticmethod @staticmethod
async def _web_ping( async def _web_ping(
session: aiohttp.ClientSession, url: str session: httpx.AsyncClient, url: str
) -> Tuple[Optional[str], Optional[aiohttp.ClientResponse]]: ) -> Tuple[Optional[str], Optional[httpx.Response]]:
try: try:
resp = await session.get(url, allow_redirects=False) resp = await session.get(url, follow_redirects=False)
return await resp.text(), resp return resp.text, resp
except (aiohttp.ClientError, asyncio.TimeoutError): except (httpx.HTTPError, asyncio.TimeoutError):
pass pass
return None, None return None, None
@staticmethod @staticmethod
def _parse_web_type(web_text: str, web_resp: aiohttp.ClientResponse) -> MinerTypes: def _parse_web_type(web_text: str, web_resp: httpx.Response) -> MinerTypes:
if web_resp.status == 401 and 'realm="antMiner' in web_resp.headers.get( if web_resp.status_code == 401 and 'realm="antMiner' in web_resp.headers.get(
"www-authenticate", "" "www-authenticate", ""
): ):
return MinerTypes.ANTMINER return MinerTypes.ANTMINER
if web_resp.status == 307 and "https://" in web_resp.headers.get( if web_resp.status_code == 307 and "https://" in web_resp.headers.get(
"location", "" "location", ""
): ):
return MinerTypes.WHATSMINER return MinerTypes.WHATSMINER
if "Braiins OS" in web_text or 'href="/cgi-bin/luci"' in web_text: if "Braiins OS" in web_text:
return MinerTypes.BRAIINS_OS return MinerTypes.BRAIINS_OS
if "cloud-box" in web_text: if "cloud-box" in web_text:
return MinerTypes.GOLDSHELL return MinerTypes.GOLDSHELL
@@ -576,26 +581,26 @@ class MinerFactory:
self, self,
ip: Union[ipaddress.ip_address, str], ip: Union[ipaddress.ip_address, str],
location: str, location: str,
auth: Optional[aiohttp.BasicAuth] = None, auth: Optional[httpx.DigestAuth] = None,
) -> Optional[dict]: ) -> Optional[dict]:
async with aiohttp.ClientSession() as session: async with httpx.AsyncClient(verify=False) as session:
try: try:
data = await session.get( data = await session.get(
f"http://{str(ip)}{location}", f"http://{str(ip)}{location}",
auth=auth, auth=auth,
timeout=30, timeout=30,
) )
except (aiohttp.ClientError, asyncio.TimeoutError): except (httpx.HTTPError, asyncio.TimeoutError):
logger.info(f"{ip}: Web command timeout.") logger.info(f"{ip}: Web command timeout.")
return return
if data is None: if data is None:
return return
try: try:
json_data = await data.json() json_data = data.json()
except (aiohttp.ContentTypeError, asyncio.TimeoutError): except (json.JSONDecodeError, asyncio.TimeoutError):
try: try:
return json.loads(await data.text()) return json.loads(data.text)
except (json.JSONDecodeError, aiohttp.ClientError): except (json.JSONDecodeError, httpx.HTTPError):
return return
else: else:
return json_data return json_data
@@ -691,6 +696,28 @@ class MinerFactory:
return UnknownMiner(str(ip)) return UnknownMiner(str(ip))
async def get_miner_model_antminer(self, ip: str): async def get_miner_model_antminer(self, ip: str):
tasks = [
asyncio.create_task(self._get_model_antminer_web(ip)),
asyncio.create_task(self._get_model_antminer_sock(ip)),
]
return await concurrent_get_first_result(tasks, lambda x: x is not None)
async def _get_model_antminer_web(self, ip: str):
# last resort, this is slow
auth = httpx.DigestAuth("root", "root")
web_json_data = await self.send_web_command(
ip, "/cgi-bin/get_system_info.cgi", auth=auth
)
try:
miner_model = web_json_data["minertype"]
return miner_model
except (TypeError, LookupError):
pass
async def _get_model_antminer_sock(self, ip: str):
sock_json_data = await self.send_api_command(ip, "version") sock_json_data = await self.send_api_command(ip, "version")
try: try:
miner_model = sock_json_data["VERSION"][0]["Type"] miner_model = sock_json_data["VERSION"][0]["Type"]
@@ -715,19 +742,6 @@ class MinerFactory:
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
# last resort, this is slow
auth = aiohttp.BasicAuth("root", "root")
web_json_data = await self.send_web_command(
ip, "/cgi-bin/get_system_info.cgi", auth=auth
)
try:
miner_model = web_json_data["minertype"]
return miner_model
except (TypeError, LookupError):
pass
async def get_miner_model_goldshell(self, ip: str): async def get_miner_model_goldshell(self, ip: str):
json_data = await self.send_web_command(ip, "/mcb/status") json_data = await self.send_web_command(ip, "/mcb/status")
@@ -741,7 +755,7 @@ class MinerFactory:
async def get_miner_model_whatsminer(self, ip: str): async def get_miner_model_whatsminer(self, ip: str):
sock_json_data = await self.send_api_command(ip, "devdetails") sock_json_data = await self.send_api_command(ip, "devdetails")
try: try:
miner_model = sock_json_data["DEVDETAILS"][0]["Model"] miner_model = sock_json_data["DEVDETAILS"][0]["Model"].replace("_", "")
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
@@ -760,14 +774,14 @@ class MinerFactory:
async def get_miner_model_innosilicon(self, ip: str) -> Optional[str]: async def get_miner_model_innosilicon(self, ip: str) -> Optional[str]:
try: try:
async with aiohttp.ClientSession() as session: async with httpx.AsyncClient(verify=False) as session:
auth_req = await session.post( auth_req = await session.post(
f"http://{ip}/api/auth", f"http://{ip}/api/auth",
data={"username": "admin", "password": "admin"}, data={"username": "admin", "password": "admin"},
) )
auth = (await auth_req.json())["jwt"] auth = auth_req.json()["jwt"]
web_data = await ( web_data = (
await session.post( await session.post(
f"http://{ip}/api/type", f"http://{ip}/api/type",
headers={"Authorization": "Bearer " + auth}, headers={"Authorization": "Bearer " + auth},
@@ -775,7 +789,7 @@ class MinerFactory:
) )
).json() ).json()
return web_data["type"] return web_data["type"]
except (aiohttp.ClientError, LookupError): except (httpx.HTTPError, LookupError):
pass pass
async def get_miner_model_braiins_os(self, ip: str) -> Optional[str]: async def get_miner_model_braiins_os(self, ip: str) -> Optional[str]:
@@ -790,16 +804,16 @@ class MinerFactory:
pass pass
try: try:
async with aiohttp.ClientSession() as session: async with httpx.AsyncClient(verify=False) as session:
d = await session.post( d = await session.post(
f"http://{ip}/graphql", f"http://{ip}/graphql",
json={"query": "{bosminer {info{modelName}}}"}, json={"query": "{bosminer {info{modelName}}}"},
) )
if d.status == 200: if d.status_code == 200:
json_data = await d.json() json_data = d.json()
miner_model = json_data["data"]["bosminer"]["info"]["modelName"] miner_model = json_data["data"]["bosminer"]["info"]["modelName"]
return miner_model return miner_model
except (aiohttp.ClientError, LookupError): except (httpx.HTTPError, LookupError):
pass pass
async def get_miner_model_vnish(self, ip: str) -> Optional[str]: async def get_miner_model_vnish(self, ip: str) -> Optional[str]:
@@ -813,6 +827,9 @@ class MinerFactory:
if "(88)" in miner_model: if "(88)" in miner_model:
miner_model = miner_model.replace("(88)", "NOPIC") miner_model = miner_model.replace("(88)", "NOPIC")
if " AML" in miner_model:
miner_model = miner_model.replace(" AML", "")
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass

View File

@@ -44,6 +44,24 @@ class S19Pro(AntMiner): # noqa - ignore ABC method implementation
self.fan_count = 4 self.fan_count = 4
class S19i(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 = "S19i"
self.nominal_chips = 80
self.fan_count = 4
class S19Plus(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+"
self.nominal_chips = 80
self.fan_count = 4
class S19ProPlus(AntMiner): # noqa - ignore ABC method implementation class S19ProPlus(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str, api_ver: str = "0.0.0"): def __init__(self, ip: str, api_ver: str = "0.0.0"):
super().__init__(ip, api_ver) super().__init__(ip, api_ver)

View File

@@ -20,10 +20,12 @@ from .S19 import (
S19XP, S19XP,
S19a, S19a,
S19aPro, S19aPro,
S19i,
S19j, S19j,
S19jNoPIC, S19jNoPIC,
S19jPro, S19jPro,
S19NoPIC, S19NoPIC,
S19Plus,
S19Pro, S19Pro,
S19ProPlus, S19ProPlus,
) )

View File

@@ -0,0 +1,35 @@
# ------------------------------------------------------------------------------
# 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.makes import WhatsMiner
class M20PV10(WhatsMiner): # 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 = "M20P V10"
self.nominal_chips = 156
self.fan_count = 2
class M20PV30(WhatsMiner): # 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 = "M20P V30"
self.nominal_chips = 148
self.fan_count = 2

View File

@@ -42,8 +42,5 @@ class M20SV30(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver) super().__init__(ip, api_ver)
self.ip = ip self.ip = ip
self.model = "M20S V30" self.model = "M20S V30"
self.nominal_chips = 0 self.nominal_chips = 140
warnings.warn(
"Unknown chip count for miner type M20SV30, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2 self.fan_count = 2

View File

@@ -24,8 +24,5 @@ class M29V10(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver) super().__init__(ip, api_ver)
self.ip = ip self.ip = ip
self.model = "M29 V10" self.model = "M29 V10"
self.nominal_chips = 0 self.nominal_chips = 50
warnings.warn(
"Unknown chip count for miner type M29V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2 self.fan_count = 2

View File

@@ -15,6 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .M20 import M20V10 from .M20 import M20V10
from .M20P import M20PV10, M20PV30
from .M20S import M20SV10, M20SV20, M20SV30 from .M20S import M20SV10, M20SV20, M20SV30
from .M20S_Plus import M20SPlusV30 from .M20S_Plus import M20SPlusV30
from .M21 import M21V10 from .M21 import M21V10

View File

@@ -165,10 +165,7 @@ class M30SPlusVE50(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver) super().__init__(ip, api_ver)
self.ip = ip self.ip = ip
self.model = "M30S+ VE50" self.model = "M30S+ VE50"
self.nominal_chips = 0 self.nominal_chips = 164
warnings.warn(
"Unknown chip count for miner type M30S+ VE50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2 self.fan_count = 2

View File

@@ -87,10 +87,7 @@ class M50VH60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver) super().__init__(ip, api_ver)
self.ip = ip self.ip = ip
self.model = "M50 VH60" self.model = "M50 VH60"
self.nominal_chips = 0 self.nominal_chips = 84
warnings.warn(
"Unknown chip count for miner type M50 VH60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2 self.fan_count = 2

View File

@@ -0,0 +1,26 @@
# ------------------------------------------------------------------------------
# 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 M2X
from pyasic.miners.types import M20PV10, M20PV30
class BTMinerM20PV10(M2X, M20PV10):
pass
class BTMinerM20PV30(M2X, M20PV30):
pass

View File

@@ -15,6 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .M20 import BTMinerM20V10 from .M20 import BTMinerM20V10
from .M20P import BTMinerM20PV10, BTMinerM20PV30
from .M20S import BTMinerM20SV10, BTMinerM20SV20, BTMinerM20SV30 from .M20S import BTMinerM20SV10, BTMinerM20SV20, BTMinerM20SV30
from .M20S_Plus import BTMinerM20SPlusV30 from .M20S_Plus import BTMinerM20SPlusV30
from .M21 import BTMinerM21V10 from .M21 import BTMinerM21V10

View File

@@ -19,9 +19,9 @@ import ipaddress
import logging import logging
from typing import AsyncIterator, List, Union from typing import AsyncIterator, List, Union
from pyasic import settings
from pyasic.miners.miner_factory import AnyMiner, miner_factory from pyasic.miners.miner_factory import AnyMiner, miner_factory
from pyasic.network.net_range import MinerNetworkRange from pyasic.network.net_range import MinerNetworkRange
from pyasic.settings import PyasicSettings
class MinerNetwork: class MinerNetwork:
@@ -108,7 +108,7 @@ class MinerNetwork:
# clear cached miners # clear cached miners
miner_factory.clear_cached_miners() miner_factory.clear_cached_miners()
limit = asyncio.Semaphore(PyasicSettings().network_scan_threads) limit = asyncio.Semaphore(settings.get("network_scan_threads", 300))
miners = await asyncio.gather( miners = await asyncio.gather(
*[self.ping_and_get_miner(host, limit) for host in local_network.hosts()] *[self.ping_and_get_miner(host, limit) for host in local_network.hosts()]
) )
@@ -136,7 +136,7 @@ class MinerNetwork:
local_network = self.get_network() local_network = self.get_network()
# create a list of scan tasks # create a list of scan tasks
limit = asyncio.Semaphore(PyasicSettings().network_scan_threads) limit = asyncio.Semaphore(settings.get("network_scan_threads", 300))
miners = asyncio.as_completed( miners = asyncio.as_completed(
[ [
loop.create_task(self.ping_and_get_miner(host, limit)) loop.create_task(self.ping_and_get_miner(host, limit))
@@ -191,12 +191,12 @@ class MinerNetwork:
async def ping_miner( async def ping_miner(
ip: ipaddress.ip_address, port=4028 ip: ipaddress.ip_address, port=4028
) -> Union[None, ipaddress.ip_address]: ) -> Union[None, ipaddress.ip_address]:
for i in range(PyasicSettings().network_ping_retries): for i in range(settings.get("network_ping_retries", 1)):
try: try:
connection_fut = asyncio.open_connection(str(ip), port) connection_fut = asyncio.open_connection(str(ip), port)
# get the read and write streams from the connection # get the read and write streams from the connection
reader, writer = await asyncio.wait_for( reader, writer = await asyncio.wait_for(
connection_fut, timeout=PyasicSettings().network_ping_timeout connection_fut, timeout=settings.get("network_ping_timeout", 3)
) )
# immediately close connection, we know connection happened # immediately close connection, we know connection happened
writer.close() writer.close()
@@ -220,12 +220,12 @@ async def ping_miner(
async def ping_and_get_miner( async def ping_and_get_miner(
ip: ipaddress.ip_address, port=4028 ip: ipaddress.ip_address, port=4028
) -> Union[None, AnyMiner]: ) -> Union[None, AnyMiner]:
for i in range(PyasicSettings().network_ping_retries): for i in range(settings.get("network_ping_retries", 1)):
try: try:
connection_fut = asyncio.open_connection(str(ip), port) connection_fut = asyncio.open_connection(str(ip), port)
# get the read and write streams from the connection # get the read and write streams from the connection
reader, writer = await asyncio.wait_for( reader, writer = await asyncio.wait_for(
connection_fut, timeout=PyasicSettings().network_ping_timeout connection_fut, timeout=settings.get("network_ping_timeout", 3)
) )
# immediately close connection, we know connection happened # immediately close connection, we know connection happened
writer.close() writer.close()

View File

@@ -14,27 +14,26 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from dataclasses import dataclass from typing import Any
from pyasic.misc import Singleton _settings = { # defaults
"network_ping_retries": 1,
"network_ping_timeout": 3,
"network_scan_threads": 300,
"factory_get_retries": 1,
"get_data_retries": 1,
"default_whatsminer_password": "admin",
"default_innosilicon_password": "admin",
"default_antminer_password": "root",
"default_bosminer_password": "root",
"default_vnish_password": "admin",
"default_goldshell_password": "123456789",
}
@dataclass def get(key: str, other: Any = None) -> Any:
class PyasicSettings(metaclass=Singleton): return _settings.get(key, other)
network_ping_retries: int = 1
network_ping_timeout: int = 3
network_scan_threads: int = 300
miner_factory_get_version_retries: int = 1
miner_get_data_retries: int = 1 def update(key: str, val: Any) -> Any:
_settings[key] = val
global_whatsminer_password = "admin"
global_innosilicon_password = "admin"
global_antminer_password = "root"
global_bosminer_password = "root"
global_vnish_password = "admin"
global_goldshell_password = "123456789"
debug: bool = False
logfile: bool = False

View File

@@ -24,7 +24,7 @@ from pyasic.errors import APIWarning
class BaseWebAPI(ABC): class BaseWebAPI(ABC):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
# ip address of the miner # ip address of the miner
self.ip = ipaddress.ip_address(ip) self.ip = ip # ipaddress.ip_address(ip)
self.username = "root" self.username = "root"
self.pwd = "root" self.pwd = "root"

View File

@@ -13,19 +13,20 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import json import json
from typing import Union from typing import Union
import httpx import httpx
from pyasic.settings import PyasicSettings from pyasic import settings
from pyasic.web import BaseWebAPI from pyasic.web import BaseWebAPI
class AntminerModernWebAPI(BaseWebAPI): class AntminerModernWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.pwd = PyasicSettings().global_antminer_password self.pwd = settings.get("default_antminer_password", "root")
async def send_command( async def send_command(
self, self,
@@ -56,25 +57,37 @@ class AntminerModernWebAPI(BaseWebAPI):
async def multicommand( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict: ) -> dict:
data = {k: None for k in commands}
data["multicommand"] = True
auth = httpx.DigestAuth(self.username, self.pwd)
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
for command in commands: tasks = [
try: asyncio.create_task(self._handle_multicommand(client, command))
url = f"http://{self.ip}/cgi-bin/{command}.cgi" for command in commands
ret = await client.get(url, auth=auth) ]
except httpx.HTTPError: all_data = await asyncio.gather(*tasks)
pass
else: data = {}
if ret.status_code == 200: for item in all_data:
try: data.update(item)
json_data = ret.json()
data[command] = json_data data["multicommand"] = True
except json.decoder.JSONDecodeError:
pass
return data return data
async def _handle_multicommand(self, client: httpx.AsyncClient, command: str):
auth = httpx.DigestAuth(self.username, self.pwd)
try:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
ret = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if ret.status_code == 200:
try:
json_data = ret.json()
return {command: json_data}
except json.decoder.JSONDecodeError:
pass
return {command: {}}
async def get_miner_conf(self) -> dict: async def get_miner_conf(self) -> dict:
return await self.send_command("get_miner_conf") return await self.send_command("get_miner_conf")
@@ -124,7 +137,7 @@ class AntminerModernWebAPI(BaseWebAPI):
class AntminerOldWebAPI(BaseWebAPI): class AntminerOldWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.pwd = PyasicSettings().global_antminer_password self.pwd = settings.get("default_antminer_password", "root")
async def send_command( async def send_command(
self, self,

View File

@@ -18,15 +18,14 @@ from typing import Union
import httpx import httpx
from pyasic import APIError from pyasic import APIError, settings
from pyasic.settings import PyasicSettings
from pyasic.web import BaseWebAPI from pyasic.web import BaseWebAPI
class BOSMinerWebAPI(BaseWebAPI): class BOSMinerWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.pwd = PyasicSettings().global_bosminer_password self.pwd = settings.get("default_bosminer_password", "root")
async def send_command( async def send_command(
self, self,
@@ -58,11 +57,13 @@ class BOSMinerWebAPI(BaseWebAPI):
command: dict, command: dict,
) -> dict: ) -> dict:
url = f"http://{self.ip}/graphql" 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: try:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
await self.auth(client) await self.auth(client)
data = await client.post(url, json={"query": query}) data = await client.post(url, json=query)
except httpx.HTTPError: except httpx.HTTPError:
pass pass
else: else:
@@ -74,7 +75,7 @@ class BOSMinerWebAPI(BaseWebAPI):
async def multicommand( async def multicommand(
self, *commands: Union[dict, str], allow_warning: bool = True self, *commands: Union[dict, str], allow_warning: bool = True
): ) -> dict:
luci_commands = [] luci_commands = []
gql_commands = [] gql_commands = []
for cmd in commands: for cmd in commands:
@@ -86,6 +87,11 @@ class BOSMinerWebAPI(BaseWebAPI):
luci_data = await self.luci_multicommand(*luci_commands) luci_data = await self.luci_multicommand(*luci_commands)
gql_data = await self.gql_multicommand(*gql_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) data = dict(**luci_data, **gql_data)
return data return data
@@ -144,8 +150,6 @@ class BOSMinerWebAPI(BaseWebAPI):
data = await client.get( data = await client.get(
f"http://{self.ip}{path}", headers={"User-Agent": "BTC Tools v0.1"} f"http://{self.ip}{path}", headers={"User-Agent": "BTC Tools v0.1"}
) )
print(data.status_code)
print(data.text)
if data.status_code == 200: if data.status_code == 200:
return data.json() return data.json()
if ignore_errors: if ignore_errors:

View File

@@ -19,7 +19,7 @@ from typing import Union
import httpx import httpx
from pyasic.settings import PyasicSettings from pyasic import settings
from pyasic.web import BaseWebAPI from pyasic.web import BaseWebAPI
@@ -27,7 +27,7 @@ class GoldshellWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.username = "admin" self.username = "admin"
self.pwd = PyasicSettings().global_goldshell_password self.pwd = settings.get("default_goldshell_password", "123456789")
self.jwt = None self.jwt = None
async def auth(self): async def auth(self):
@@ -72,7 +72,7 @@ class GoldshellWebAPI(BaseWebAPI):
if not self.jwt: if not self.jwt:
await self.auth() await self.auth()
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
for i in range(PyasicSettings().miner_get_data_retries): for i in range(settings.get("get_data_retries", 1)):
try: try:
if parameters: if parameters:
response = await client.put( response = await client.put(

View File

@@ -19,8 +19,8 @@ from typing import Union
import httpx import httpx
from pyasic import settings
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.settings import PyasicSettings
from pyasic.web import BaseWebAPI from pyasic.web import BaseWebAPI
@@ -28,7 +28,7 @@ class InnosiliconWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.username = "admin" self.username = "admin"
self.pwd = PyasicSettings().global_innosilicon_password self.pwd = settings.get("default_innosilicon_password", "admin")
self.jwt = None self.jwt = None
async def auth(self): async def auth(self):
@@ -55,7 +55,7 @@ class InnosiliconWebAPI(BaseWebAPI):
if not self.jwt: if not self.jwt:
await self.auth() await self.auth()
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
for i in range(PyasicSettings().miner_get_data_retries): for i in range(settings.get("get_data_retries", 1)):
try: try:
response = await client.post( response = await client.post(
f"http://{self.ip}/api/{command}", f"http://{self.ip}/api/{command}",

View File

@@ -19,7 +19,7 @@ from typing import Union
import httpx import httpx
from pyasic.settings import PyasicSettings from pyasic import settings
from pyasic.web import BaseWebAPI from pyasic.web import BaseWebAPI
@@ -27,7 +27,7 @@ class VNishWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.username = "admin" self.username = "admin"
self.pwd = PyasicSettings().global_vnish_password self.pwd = settings.get("default_vnish_password", "admin")
self.token = None self.token = None
async def auth(self): async def auth(self):
@@ -59,7 +59,7 @@ class VNishWebAPI(BaseWebAPI):
if not self.token: if not self.token:
await self.auth() await self.auth()
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
for i in range(PyasicSettings().miner_get_data_retries): for i in range(settings.get("get_data_retries", 1)):
try: try:
auth = self.token auth = self.token
if command.startswith("system"): if command.startswith("system"):
@@ -116,8 +116,32 @@ class VNishWebAPI(BaseWebAPI):
async def reboot(self) -> dict: async def reboot(self) -> dict:
return await self.send_command("system/reboot", post=True) return await self.send_command("system/reboot", post=True)
async def pause_mining(self) -> dict:
return await self.send_command("mining/pause", post=True)
async def resume_mining(self) -> dict:
return await self.send_command("mining/resume", post=True)
async def stop_mining(self) -> dict:
return await self.send_command("mining/stop", post=True)
async def start_mining(self) -> dict:
return await self.send_command("mining/start", post=True)
async def info(self): async def info(self):
return await self.send_command("info") return await self.send_command("info")
async def summary(self): async def summary(self):
return await self.send_command("summary") return await self.send_command("summary")
async def chips(self):
return await self.send_command("chips")
async def layout(self):
return await self.send_command("layout")
async def status(self):
return await self.send_command("status")
async def settings(self):
return await self.send_command("settings")

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pyasic" name = "pyasic"
version = "0.36.2" version = "0.39.0"
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH." description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
authors = ["UpstreamData <brett@upstreamdata.ca>"] authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic" repository = "https://github.com/UpstreamData/pyasic"
@@ -14,7 +14,6 @@ httpx = "^0.24.0"
passlib = "^1.7.4" passlib = "^1.7.4"
pyaml = "^23.5.9" pyaml = "^23.5.9"
toml = "^0.10.2" toml = "^0.10.2"
aiohttp = "^3.8.4"
[tool.poetry.group.dev] [tool.poetry.group.dev]
optional = true optional = true