Compare commits
348 Commits
cfg_util-u
...
v0.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d265e823b | ||
|
|
5e6bc8c8ef | ||
|
|
871499b77f | ||
|
|
117a161fd5 | ||
|
|
40bacbf41c | ||
|
|
e091863aa7 | ||
|
|
85e8ac63f1 | ||
|
|
a5252e3a84 | ||
|
|
404d6590db | ||
|
|
1d04399daf | ||
|
|
03ebcacca5 | ||
|
|
75934fd7fe | ||
|
|
bbeca15799 | ||
|
|
45befb569b | ||
|
|
61334ed99e | ||
|
|
2bf059df01 | ||
|
|
9c2de26182 | ||
|
|
714983cddc | ||
|
|
191f1d24b9 | ||
|
|
5a0bafb964 | ||
|
|
67aedd319d | ||
|
|
44012c50d6 | ||
|
|
06540efc98 | ||
|
|
9d0d1a24d9 | ||
|
|
8568f91482 | ||
|
|
64918e5552 | ||
|
|
53d5ecd04a | ||
|
|
1b0e80a418 | ||
|
|
9ad506a313 | ||
|
|
18c4bbd09c | ||
|
|
0d123d5dd8 | ||
|
|
b9b91293fe | ||
|
|
47a702c94c | ||
|
|
6d5a288120 | ||
|
|
038aae95ac | ||
|
|
dd84aede25 | ||
|
|
dc8ad271de | ||
|
|
b78c1cdca5 | ||
|
|
0eb7ced932 | ||
|
|
8e58f4492f | ||
|
|
95fb32de19 | ||
|
|
5145dc19f8 | ||
|
|
1808d62bba | ||
|
|
97ef4dfe37 | ||
|
|
174a132e75 | ||
|
|
84d6e58ebe | ||
|
|
e9a1483e5f | ||
|
|
4eb51eed20 | ||
|
|
066fc1a4b3 | ||
|
|
cc24236c0a | ||
|
|
564cd42eae | ||
|
|
8677eff491 | ||
|
|
63a21ea9aa | ||
|
|
1c9d3dc84d | ||
|
|
0dacd3d294 | ||
|
|
6fa74613b4 | ||
|
|
f7fb7a3acb | ||
|
|
666c5bfc64 | ||
|
|
1f8d92f6bb | ||
|
|
ef336a9e23 | ||
|
|
7fe6fd47fb | ||
|
|
91a0298d96 | ||
|
|
ed3d8fc815 | ||
|
|
4f2d630746 | ||
|
|
a8c685a883 | ||
|
|
09660e1934 | ||
|
|
c01908ff9a | ||
|
|
267c388a95 | ||
|
|
8215d33241 | ||
|
|
f4258a304a | ||
|
|
514fafea58 | ||
|
|
e324369fe0 | ||
|
|
3bc9287668 | ||
|
|
d90bf190c5 | ||
|
|
8cc6f66458 | ||
|
|
a2b071af4f | ||
|
|
b7b589802f | ||
|
|
93912a6df6 | ||
|
|
ffce15f653 | ||
|
|
725b14e583 | ||
|
|
26c6e47f1e | ||
|
|
51dae7375f | ||
|
|
801cfc4ff8 | ||
|
|
ac3ff7a63e | ||
|
|
1b22810f4b | ||
|
|
b756c9e4a1 | ||
|
|
64b5e6c032 | ||
|
|
a13f5dd2d1 | ||
|
|
e6ea8d3e16 | ||
|
|
af37850289 | ||
|
|
6ecdfa1cf8 | ||
|
|
c0b21ebc23 | ||
|
|
184ada417f | ||
|
|
b636860ecb | ||
|
|
0107fdacde | ||
|
|
ce5e1cad40 | ||
|
|
d877ba01a0 | ||
|
|
b0ed990d5a | ||
|
|
89c8a16900 | ||
|
|
247cf0ccc2 | ||
|
|
d0aa219a7a | ||
|
|
87291e2a89 | ||
|
|
9c88d21db6 | ||
|
|
8b7415042f | ||
|
|
59ab6e6c8a | ||
|
|
0724a376ea | ||
|
|
f9f26a5587 | ||
|
|
ed4122fb21 | ||
|
|
0739a7f689 | ||
|
|
c7b7a6e7c5 | ||
|
|
2a132c8325 | ||
|
|
154882a668 | ||
|
|
3f64c9dd67 | ||
|
|
d8d66e4244 | ||
|
|
a9cdefcd43 | ||
|
|
029d3ef596 | ||
|
|
0e474402c0 | ||
|
|
b6560cdedb | ||
|
|
767575703e | ||
|
|
4b4d9060ed | ||
|
|
ad75b1d25c | ||
|
|
4b767c5427 | ||
|
|
a6df7a83d6 | ||
|
|
93f2990399 | ||
|
|
e74f67089e | ||
|
|
41a6078790 | ||
|
|
4d93926fee | ||
|
|
03f5cafe76 | ||
|
|
4f6ebff880 | ||
|
|
af27cbbe2c | ||
|
|
3604957c83 | ||
|
|
3670a02aec | ||
|
|
7ebfdb3f33 | ||
|
|
b9b7da8746 | ||
|
|
eaaf137b9b | ||
|
|
a0311e3ce3 | ||
|
|
8864aa7b4b | ||
|
|
4d58129eee | ||
|
|
4468fe9fbb | ||
|
|
3b716a044b | ||
|
|
25e657729c | ||
|
|
cace399ed2 | ||
|
|
045e1ca6ba | ||
|
|
4f86dec560 | ||
|
|
13f033440d | ||
|
|
b5c455ffa4 | ||
|
|
eb5a00b706 | ||
|
|
3a560472e6 | ||
|
|
4776dce038 | ||
|
|
2d6891c6d2 | ||
|
|
f5a41f7b13 | ||
|
|
4a2926df94 | ||
|
|
8736f33a56 | ||
|
|
89eb77588f | ||
|
|
c930510226 | ||
|
|
b7c58e5d34 | ||
|
|
ce48ae020b | ||
|
|
7809bfc0d1 | ||
|
|
d84fcaafdf | ||
|
|
a9f600b797 | ||
|
|
f0a8e7ba9f | ||
|
|
c57a523553 | ||
|
|
d905f6f414 | ||
|
|
22f78ac405 | ||
|
|
7a098b1c7e | ||
|
|
e1383f2002 | ||
|
|
c3b23313ba | ||
|
|
02581e917d | ||
|
|
e267073f76 | ||
|
|
4038dae446 | ||
|
|
134b5fe0ff | ||
|
|
d452ca36b7 | ||
|
|
fdec35cd2e | ||
|
|
d488c8458c | ||
|
|
6d2e40c81d | ||
|
|
594b5d0448 | ||
|
|
1be12e5d4c | ||
|
|
bae2ee4245 | ||
|
|
57bd606f21 | ||
|
|
eb8cefa461 | ||
|
|
9edcd866bb | ||
|
|
07a8b00a93 | ||
|
|
c22be7ded8 | ||
|
|
2380b94db1 | ||
|
|
d8e59afee0 | ||
|
|
05e14baa68 | ||
|
|
ff56148732 | ||
|
|
bfc5668d24 | ||
|
|
b3103ae700 | ||
|
|
43834203a8 | ||
|
|
7ba8044564 | ||
|
|
7e91fe12e7 | ||
|
|
02114aac65 | ||
|
|
244dac76af | ||
|
|
2bd25c3f35 | ||
|
|
23350ea4b6 | ||
|
|
8a6917878e | ||
|
|
7dd00954e4 | ||
|
|
f3710f618e | ||
|
|
8ecdb6f5e8 | ||
|
|
309b4d44fc | ||
|
|
80f941d912 | ||
|
|
4534b09532 | ||
|
|
97a9b59acc | ||
|
|
87b8de9029 | ||
|
|
42f5146632 | ||
|
|
f613cc039f | ||
|
|
e974c77359 | ||
|
|
0f324177cb | ||
|
|
46a4508cd7 | ||
|
|
d4d9b1ad3c | ||
|
|
322ee05fdf | ||
|
|
85569366a2 | ||
|
|
dea6ff2a96 | ||
|
|
3fcd2edf6f | ||
|
|
16b84310ec | ||
|
|
f8899521bc | ||
|
|
3558a1a6b1 | ||
|
|
385943755d | ||
|
|
3002cb4e97 | ||
|
|
6d711520fc | ||
|
|
584de40983 | ||
|
|
81911ba549 | ||
|
|
e37e9e2251 | ||
|
|
92a65c8977 | ||
|
|
ae8b2cbd07 | ||
|
|
cda13edf85 | ||
|
|
610ee57963 | ||
|
|
2ef809db54 | ||
|
|
f315c0c051 | ||
|
|
936c230aa3 | ||
|
|
2c93f1f395 | ||
|
|
727ebd9c42 | ||
|
|
1e4fc897e3 | ||
|
|
3945a86004 | ||
|
|
58cc64d17b | ||
|
|
b66cf6f0ba | ||
|
|
1db15a741e | ||
|
|
5f355c833b | ||
|
|
a76b32e3ff | ||
|
|
f2c01dca25 | ||
|
|
abc542a0ca | ||
|
|
9e598ebd8c | ||
|
|
7801ca5819 | ||
|
|
482edabd27 | ||
|
|
3e5998de6e | ||
|
|
c3d19607f6 | ||
|
|
2c2648cbe7 | ||
|
|
a72c4f7797 | ||
|
|
19ee9eb18f | ||
|
|
3ae29c3883 | ||
|
|
d9f8f53a10 | ||
|
|
6b3e525f45 | ||
|
|
c8824f86af | ||
|
|
cf3163dccf | ||
|
|
da5a784214 | ||
|
|
30b3315084 | ||
|
|
5a7dcc7fcf | ||
|
|
c6305c57cf | ||
|
|
d330e2e978 | ||
|
|
1ec2a2a4a6 | ||
|
|
c97d384cf4 | ||
|
|
ca52e40a6a | ||
|
|
4a10efd7a4 | ||
|
|
128aab1b88 | ||
|
|
bb89be64f4 | ||
|
|
ef0a507306 | ||
|
|
908594970e | ||
|
|
36ff5e96a4 | ||
|
|
9bf9f8342a | ||
|
|
f3660c1f68 | ||
|
|
d58aa871b5 | ||
|
|
4f90eb65ad | ||
|
|
b50da98322 | ||
|
|
ca47f2817f | ||
|
|
c489a4bed9 | ||
|
|
54c7e996db | ||
|
|
0426bb289e | ||
|
|
8e253ffa05 | ||
|
|
102f365003 | ||
|
|
48d2f6ec07 | ||
|
|
58f0ce8e2d | ||
|
|
3178083533 | ||
|
|
516075db6d | ||
|
|
d6c8335162 | ||
|
|
e7a45efe15 | ||
|
|
1c0b5e6441 | ||
|
|
66792e1ab9 | ||
|
|
6fd631df5b | ||
|
|
dcf1a805c5 | ||
|
|
8edfde96dc | ||
|
|
ae911ec775 | ||
|
|
465d0e6f1c | ||
|
|
6d9de87fb8 | ||
|
|
a93027369e | ||
|
|
a1839aae46 | ||
|
|
d5fc7650ef | ||
|
|
cdc6c898ae | ||
|
|
574432ec0d | ||
|
|
a89486d6ad | ||
|
|
56d7234ccb | ||
|
|
c7f1b00e13 | ||
|
|
60f5137115 | ||
|
|
a90239e3c5 | ||
|
|
a105429d99 | ||
|
|
8e73b8f7a1 | ||
|
|
53af55a87d | ||
|
|
0711bcb259 | ||
|
|
282e00f93a | ||
|
|
2e11527416 | ||
|
|
01a64e63c6 | ||
|
|
2610d642fa | ||
|
|
e1e93aea66 | ||
|
|
ab208d0d2f | ||
|
|
e9210eb37d | ||
|
|
93665772c3 | ||
|
|
44bcc30130 | ||
|
|
d8bccbccaa | ||
|
|
2734caa9da | ||
|
|
d9ecdfc9d7 | ||
|
|
fa88bea376 | ||
|
|
25803b856d | ||
|
|
88539650ca | ||
|
|
3cf0162892 | ||
|
|
51e9e19409 | ||
|
|
c93d99b27c | ||
|
|
770b17c86b | ||
|
|
4e8ff9ea74 | ||
|
|
8ec8c57e31 | ||
|
|
48aa7232b1 | ||
|
|
1f3ffe96a1 | ||
|
|
e0505a31ca | ||
|
|
3ecc27b3f9 | ||
|
|
5d66c539d4 | ||
|
|
c751d53398 | ||
|
|
ea1e8abeac | ||
|
|
8d3f6a3c06 | ||
|
|
f35adf0ae4 | ||
|
|
848ac6ef7c | ||
|
|
6db7cd4a1f | ||
|
|
23d465a733 | ||
|
|
1148946a29 | ||
|
|
e77cbc5415 | ||
|
|
5ecb87ec63 | ||
|
|
8ef135dfd7 | ||
|
|
c26a2cc99e | ||
|
|
e0d8078bf1 | ||
|
|
4528060fd0 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
# Ignore VENV
|
||||
venv
|
||||
|
||||
# Ignore builds
|
||||
build
|
||||
|
||||
# Ignore github files
|
||||
.github
|
||||
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Miner Information (If applicable):**
|
||||
- Manufacturer: [e.g. Bitmain, MicroBT]
|
||||
- Type: [e.g. S9, M20]
|
||||
- Firmware Type: [e.g. Stock, BraiinsOS]
|
||||
- Firmware Version:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
115
API/__init__.py
115
API/__init__.py
@@ -1,6 +1,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
import ipaddress
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
|
||||
class APIError(Exception):
|
||||
@@ -17,6 +19,20 @@ class APIError(Exception):
|
||||
return "Incorrect API parameters."
|
||||
|
||||
|
||||
class APIWarning(Warning):
|
||||
def __init__(self, *args):
|
||||
if args:
|
||||
self.message = args[0]
|
||||
else:
|
||||
self.message = None
|
||||
|
||||
def __str__(self):
|
||||
if self.message:
|
||||
return f"{self.message}"
|
||||
else:
|
||||
return "Incorrect API parameters."
|
||||
|
||||
|
||||
class BaseMinerAPI:
|
||||
def __init__(self, ip: str, port: int = 4028) -> None:
|
||||
# api port, should be 4028
|
||||
@@ -26,37 +42,44 @@ class BaseMinerAPI:
|
||||
|
||||
def get_commands(self) -> list:
|
||||
"""Get a list of command accessible to a specific type of API on the miner."""
|
||||
return [func for func in
|
||||
# each function in self
|
||||
dir(self) if callable(getattr(self, func)) and
|
||||
# no __ methods
|
||||
not func.startswith("__") and
|
||||
# remove all functions that are in this base class
|
||||
func not in
|
||||
[func for func in
|
||||
dir(BaseMinerAPI) if callable(getattr(BaseMinerAPI, func))
|
||||
]
|
||||
]
|
||||
return [
|
||||
func
|
||||
for func in
|
||||
# each function in self
|
||||
dir(self)
|
||||
if callable(getattr(self, func)) and
|
||||
# no __ methods
|
||||
not func.startswith("__") and
|
||||
# remove all functions that are in this base class
|
||||
func
|
||||
not in [
|
||||
func
|
||||
for func in dir(BaseMinerAPI)
|
||||
if callable(getattr(BaseMinerAPI, func))
|
||||
]
|
||||
]
|
||||
|
||||
async def multicommand(self, *commands: str) -> dict:
|
||||
"""Creates and sends multiple commands as one command to the miner."""
|
||||
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
|
||||
# split the commands into a proper list
|
||||
commands = [*commands]
|
||||
|
||||
for item in commands:
|
||||
# make sure we can actually run the command, otherwise it will fail
|
||||
if item not in self.get_commands():
|
||||
# if the command isnt allowed, remove it
|
||||
print(f"Removing incorrect command: {item}")
|
||||
commands.remove(item)
|
||||
|
||||
user_commands = [*commands]
|
||||
allowed_commands = self.get_commands()
|
||||
# make sure we can actually run the command, otherwise it will fail
|
||||
commands = [command for command in user_commands if command in allowed_commands]
|
||||
for item in list(set(user_commands) - set(commands)):
|
||||
warnings.warn(
|
||||
f"""Removing incorrect command: {item}
|
||||
If you are sure you want to use this command please use API.send_command("{item}", ignore_errors=True) instead.""",
|
||||
APIWarning,
|
||||
)
|
||||
# standard multicommand format is "command1+command2"
|
||||
# doesnt work for S19 which is dealt with in the send command function
|
||||
command = "+".join(commands)
|
||||
data = None
|
||||
try:
|
||||
data = await self.send_command(command)
|
||||
except APIError:
|
||||
except APIError as e:
|
||||
try:
|
||||
data = {}
|
||||
# S19 handler, try again
|
||||
@@ -66,11 +89,17 @@ class BaseMinerAPI:
|
||||
except APIError as e:
|
||||
raise APIError(e)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
|
||||
if data:
|
||||
logging.debug(f"{self.ip}: Received multicommand data.")
|
||||
return data
|
||||
|
||||
async def send_command(self, command: str, parameters: str or int or bool = None) -> dict:
|
||||
async def send_command(
|
||||
self,
|
||||
command: str,
|
||||
parameters: str or int or bool = None,
|
||||
ignore_errors: bool = False,
|
||||
) -> dict:
|
||||
"""Send an API command to the miner and return the result."""
|
||||
try:
|
||||
# get reader and writer streams
|
||||
@@ -78,7 +107,7 @@ class BaseMinerAPI:
|
||||
# handle OSError 121
|
||||
except OSError as e:
|
||||
if e.winerror == "121":
|
||||
print("Semaphore Timeout has Expired.")
|
||||
logging.warning("Semaphore Timeout has Expired.")
|
||||
return {}
|
||||
|
||||
# create the command
|
||||
@@ -87,7 +116,7 @@ class BaseMinerAPI:
|
||||
cmd["parameter"] = parameters
|
||||
|
||||
# send the command
|
||||
writer.write(json.dumps(cmd).encode('utf-8'))
|
||||
writer.write(json.dumps(cmd).encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
# instantiate data
|
||||
@@ -101,7 +130,7 @@ class BaseMinerAPI:
|
||||
break
|
||||
data += d
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.warning(f"{self.ip}: API Command Error: {e}")
|
||||
|
||||
data = self.load_api_data(data)
|
||||
|
||||
@@ -109,14 +138,18 @@ class BaseMinerAPI:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
# validate the command suceeded
|
||||
if not self.validate_command_output(data):
|
||||
raise APIError(data["STATUS"][0]["Msg"])
|
||||
# check for if the user wants to allow errors to return
|
||||
if not ignore_errors:
|
||||
# validate the command succeeded
|
||||
validation = self.validate_command_output(data)
|
||||
if not validation[0]:
|
||||
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
|
||||
raise APIError(validation[1])
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def validate_command_output(data: dict) -> bool:
|
||||
def validate_command_output(data: dict) -> tuple:
|
||||
"""Check if the returned command output is correctly formatted."""
|
||||
# check if the data returned is correct or an error
|
||||
# if status isn't a key, it is a multicommand
|
||||
@@ -125,42 +158,46 @@ class BaseMinerAPI:
|
||||
# make sure not to try to turn id into a dict
|
||||
if not key == "id":
|
||||
# make sure they succeeded
|
||||
if "STATUS" in data.keys():
|
||||
if "STATUS" in data[key][0].keys():
|
||||
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
|
||||
# this is an error
|
||||
return False
|
||||
return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"]
|
||||
elif "id" not in data.keys():
|
||||
if data["STATUS"] not in ["S", "I"]:
|
||||
return False
|
||||
return False, data["Msg"]
|
||||
else:
|
||||
# make sure the command succeeded
|
||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||
# this is an error
|
||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||
return False
|
||||
return True
|
||||
return False, data["STATUS"][0]["Msg"]
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def load_api_data(data: bytes) -> dict:
|
||||
"""Convert API data from JSON to dict"""
|
||||
str_data = None
|
||||
try:
|
||||
# some json from the API returns with a null byte (\x00) on the end
|
||||
if data.endswith(b"\x00"):
|
||||
# handle the null byte
|
||||
str_data = data.decode('utf-8')[:-1]
|
||||
str_data = data.decode("utf-8")[:-1]
|
||||
else:
|
||||
# no null byte
|
||||
str_data = data.decode('utf-8')
|
||||
str_data = data.decode("utf-8")
|
||||
# fix an error with a btminer return having an extra comma that breaks json.loads()
|
||||
str_data = str_data.replace(",}", "}")
|
||||
# fix an error with a btminer return having a newline that breaks json.loads()
|
||||
str_data = str_data.replace("\n", "")
|
||||
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
|
||||
str_data = str_data.replace("}{", "},{")
|
||||
# fix an error with a bmminer return having a specific comma that breaks json.loads()
|
||||
str_data = str_data.replace("[,{", "[{")
|
||||
# fix an error with a btminer return having a specific comma that breaks json.loads()
|
||||
str_data = str_data.replace("inf", "0")
|
||||
# parse the json
|
||||
parsed_data = json.loads(str_data)
|
||||
# handle bad json
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
print(e)
|
||||
raise APIError(f"Decode Error: {data}")
|
||||
raise APIError(f"Decode Error {e}: {str_data}")
|
||||
return parsed_data
|
||||
|
||||
383
API/bmminer.py
383
API/bmminer.py
@@ -2,34 +2,37 @@ from API import BaseMinerAPI
|
||||
|
||||
|
||||
class BMMinerAPI(BaseMinerAPI):
|
||||
"""
|
||||
A class that abstracts the BMMiner API in the miners.
|
||||
"""An abstraction of the BMMiner API.
|
||||
|
||||
Each method corresponds to an API command in BMMiner.
|
||||
|
||||
BMMiner API documentation:
|
||||
https://github.com/jameshilliard/bmminer/blob/master/API-README
|
||||
|
||||
Parameters:
|
||||
ip: the IP address of the miner.
|
||||
port (optional): the port of the API on the miner (standard is 4028)
|
||||
This class abstracts use of the BMMiner API, as well as the
|
||||
methods for sending commands to it. The self.send_command()
|
||||
function handles sending a command to the miner asynchronously, and
|
||||
as such is the base for many of the functions in this class, which
|
||||
rely on it to send the command for them.
|
||||
|
||||
:param ip: The IP of the miner to reference the API on.
|
||||
:param port: The port to reference the API on. Default is 4028.
|
||||
"""
|
||||
|
||||
def __init__(self, ip: str, port: int = 4028) -> None:
|
||||
super().__init__(ip, port)
|
||||
|
||||
async def version(self) -> dict:
|
||||
"""
|
||||
API 'version' command.
|
||||
"""Get miner version info.
|
||||
|
||||
Returns a dict containing version information.
|
||||
:return: Miner version information.
|
||||
"""
|
||||
return await self.send_command("version")
|
||||
|
||||
async def config(self) -> dict:
|
||||
"""
|
||||
API 'config' command.
|
||||
"""Get some basic configuration info.
|
||||
|
||||
Returns a dict containing some miner configuration information:
|
||||
:return: Some miner configuration information:
|
||||
ASC Count <- the number of ASCs
|
||||
PGA Count <- the number of PGAs
|
||||
Pool Count <- the number of Pools
|
||||
@@ -45,151 +48,131 @@ class BMMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command("config")
|
||||
|
||||
async def summary(self) -> dict:
|
||||
"""
|
||||
API 'summary' command.
|
||||
"""Get the status summary of the miner.
|
||||
|
||||
Returns a dict containing the status summary of the miner.
|
||||
:return: The status summary of the miner.
|
||||
"""
|
||||
return await self.send_command("summary")
|
||||
|
||||
async def pools(self) -> dict:
|
||||
"""
|
||||
API 'pools' command.
|
||||
"""Get pool information.
|
||||
|
||||
Returns a dict containing the status of each pool.
|
||||
:return: Miner pool information.
|
||||
"""
|
||||
return await self.send_command("pools")
|
||||
|
||||
async def devs(self) -> dict:
|
||||
"""
|
||||
API 'devs' command.
|
||||
"""Get data on each PGA/ASC with their details.
|
||||
|
||||
Returns a dict containing each PGA/ASC with their details.
|
||||
:return: Data on each PGA/ASC with their details.
|
||||
"""
|
||||
return await self.send_command("devs")
|
||||
|
||||
async def edevs(self, old: bool = False) -> dict:
|
||||
"""
|
||||
API 'edevs' command.
|
||||
"""Get data on each PGA/ASC with their details, ignoring
|
||||
blacklisted and zombie devices.
|
||||
|
||||
Returns a dict containing each PGA/ASC with their details,
|
||||
ignoring blacklisted devices and zombie devices.
|
||||
:param old: Include zombie devices that became zombies less
|
||||
than 'old' seconds ago
|
||||
|
||||
Parameters:
|
||||
old (optional): include zombie devices that became zombies less than 'old' seconds ago
|
||||
:return: Data on each PGA/ASC with their details.
|
||||
"""
|
||||
if old:
|
||||
return await self.send_command("edevs", parameters="old")
|
||||
return await self.send_command("edevs", parameters=old)
|
||||
else:
|
||||
return await self.send_command("edevs")
|
||||
|
||||
async def pga(self, n: int) -> dict:
|
||||
"""
|
||||
API 'pga' command.
|
||||
"""Get data from PGA n.
|
||||
|
||||
Returns a dict containing the details of a single PGA of number N.
|
||||
:param n: The PGA number to get data from.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to get details of.
|
||||
:return: Data on the PGA n.
|
||||
"""
|
||||
return await self.send_command("pga", parameters=n)
|
||||
|
||||
async def pgacount(self) -> dict:
|
||||
"""
|
||||
API 'pgacount' command.
|
||||
"""Get data fon all PGAs.
|
||||
|
||||
Returns a dict containing the number of PGA devices.
|
||||
:return: Data on the PGAs connected.
|
||||
"""
|
||||
return await self.send_command("pgacount")
|
||||
|
||||
async def switchpool(self, n: int) -> dict:
|
||||
"""
|
||||
API 'switchpool' command.
|
||||
"""Switch pools to pool n.
|
||||
|
||||
Returns the STATUS section with the results of switching pools.
|
||||
:param n: The pool to switch to.
|
||||
|
||||
Parameters:
|
||||
n: the number of the pool to switch to.
|
||||
:return: A confirmation of switching to pool n.
|
||||
"""
|
||||
return await self.send_command("switchpool", parameters=n)
|
||||
|
||||
async def enablepool(self, n: int) -> dict:
|
||||
"""
|
||||
API 'enablepool' command.
|
||||
"""Enable pool n.
|
||||
|
||||
Returns the STATUS section with the results of enabling the pool.
|
||||
:param n: The pool to enable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the pool to enable.
|
||||
:return: A confirmation of enabling pool n.
|
||||
"""
|
||||
return await self.send_command("enablepool", parameters=n)
|
||||
|
||||
async def addpool(self, url: str, username: str, password: str) -> dict:
|
||||
"""
|
||||
API 'addpool' command.
|
||||
"""Add a pool to the miner.
|
||||
|
||||
Returns the STATUS section with the results of adding the pool.
|
||||
:param url: The URL of the new pool to add.
|
||||
:param username: The users username on the new pool.
|
||||
:param password: The worker password on the new pool.
|
||||
|
||||
Parameters:
|
||||
url: the URL of the new pool to add.
|
||||
username: the users username on the new pool.
|
||||
password: the worker password on the new pool.
|
||||
:return: A confirmation of adding the pool.
|
||||
"""
|
||||
return await self.send_command("addpool", parameters=f"{url}, {username}, {password}")
|
||||
return await self.send_command(
|
||||
"addpool", parameters=f"{url}, " f"{username}, " f"{password}"
|
||||
)
|
||||
|
||||
async def poolpriority(self, *n: int) -> dict:
|
||||
"""
|
||||
API 'poolpriority' command.
|
||||
"""Set pool priority.
|
||||
|
||||
Returns the STATUS section with the results of setting pool priority.
|
||||
:param n: Pools in order of priority.
|
||||
|
||||
Parameters:
|
||||
n: pool numbers in order of priority.
|
||||
:return: A confirmation of setting pool priority.
|
||||
"""
|
||||
return await self.send_command("poolpriority", parameters=f"{','.join([str(item) for item in n])}")
|
||||
pools = f"{','.join([str(item) for item in n])}"
|
||||
return await self.send_command("poolpriority", parameters=pools)
|
||||
|
||||
async def poolquota(self, n: int, q: int) -> dict:
|
||||
"""
|
||||
API 'poolquota' command.
|
||||
"""Set pool quota.
|
||||
|
||||
Returns the STATUS section with the results of setting pool quota.
|
||||
:param n: Pool number to set quota on.
|
||||
:param q: Quota to set the pool to.
|
||||
|
||||
Parameters:
|
||||
n: pool number to set quota on.
|
||||
q: quota to set the pool to.
|
||||
:return: A confirmation of setting pool quota.
|
||||
"""
|
||||
return await self.send_command("poolquota", parameters=f"{n}, {q}")
|
||||
return await self.send_command("poolquota", parameters=f"{n}, " f"{q}")
|
||||
|
||||
async def disablepool(self, n: int) -> dict:
|
||||
"""
|
||||
API 'disablepool' command.
|
||||
"""Disable a pool.
|
||||
|
||||
Returns the STATUS section with the results of disabling the pool.
|
||||
:param n: Pool to disable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the pool to disable.
|
||||
:return: A confirmation of diabling the pool.
|
||||
"""
|
||||
return await self.send_command("disablepool", parameters=n)
|
||||
|
||||
async def removepool(self, n: int) -> dict:
|
||||
"""
|
||||
API 'removepool' command.
|
||||
"""Remove a pool.
|
||||
|
||||
Returns the STATUS section with the results of removing the pool.
|
||||
:param n: Pool to remove.
|
||||
|
||||
Parameters:
|
||||
n: the number of the pool to remove.
|
||||
:return: A confirmation of removing the pool.
|
||||
"""
|
||||
return await self.send_command("removepool", parameters=n)
|
||||
|
||||
async def save(self, filename: str = None) -> dict:
|
||||
"""
|
||||
API 'save' command.
|
||||
"""Save the config.
|
||||
|
||||
Returns the STATUS section with the results of saving the config file..
|
||||
:param filename: Filename to save the config as.
|
||||
|
||||
Parameters:
|
||||
filename (optional): the filename to save the config as.
|
||||
:return: A confirmation of saving the config.
|
||||
"""
|
||||
if filename:
|
||||
return await self.send_command("save", parameters=filename)
|
||||
@@ -197,146 +180,128 @@ class BMMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command("save")
|
||||
|
||||
async def quit(self) -> dict:
|
||||
"""
|
||||
API 'quit' command.
|
||||
"""Quit BMMiner.
|
||||
|
||||
Returns a single "BYE" before BMMiner quits.
|
||||
:return: A single "BYE" before BMMiner quits.
|
||||
"""
|
||||
return await self.send_command("quit")
|
||||
|
||||
async def notify(self) -> dict:
|
||||
"""
|
||||
API 'notify' command.
|
||||
"""Notify the user of past errors.
|
||||
|
||||
Returns a dict containing the last status and count of each devices problem(s).
|
||||
:return: The last status and count of each devices problem(s).
|
||||
"""
|
||||
return await self.send_command("notify")
|
||||
|
||||
async def privileged(self) -> dict:
|
||||
"""
|
||||
API 'privileged' command.
|
||||
"""Check if you have privileged access.
|
||||
|
||||
Returns the STATUS section with an error if you have no privileged access.
|
||||
:return: The STATUS section with an error if you have no
|
||||
privileged access, or success if you have privileged access.
|
||||
"""
|
||||
return await self.send_command("privileged")
|
||||
|
||||
async def pgaenable(self, n: int) -> dict:
|
||||
"""
|
||||
API 'pgaenable' command.
|
||||
"""Enable PGA n.
|
||||
|
||||
Returns the STATUS section with the results of enabling the PGA device N.
|
||||
:param n: The PGA to enable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to enable.
|
||||
:return: A confirmation of enabling PGA n.
|
||||
"""
|
||||
return await self.send_command("pgaenable", parameters=n)
|
||||
|
||||
async def pgadisable(self, n: int) -> dict:
|
||||
"""
|
||||
API 'pgadisable' command.
|
||||
"""Disable PGA n.
|
||||
|
||||
Returns the STATUS section with the results of disabling the PGA device N.
|
||||
:param n: The PGA to disable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to disable.
|
||||
:return: A confirmation of disabling PGA n.
|
||||
"""
|
||||
return await self.send_command("pgadisable", parameters=n)
|
||||
|
||||
async def pgaidentify(self, n: int) -> dict:
|
||||
"""
|
||||
API 'pgaidentify' command.
|
||||
"""Identify PGA n.
|
||||
|
||||
Returns the STATUS section with the results of identifying the PGA device N.
|
||||
:param n: The PGA to identify.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to identify.
|
||||
:return: A confirmation of identifying PGA n.
|
||||
"""
|
||||
return await self.send_command("pgaidentify", parameters=n)
|
||||
|
||||
async def devdetails(self) -> dict:
|
||||
"""
|
||||
API 'devdetails' command.
|
||||
"""Get data on all devices with their static details.
|
||||
|
||||
Returns a dict containing all devices with their static details.
|
||||
:return: Data on all devices with their static details.
|
||||
"""
|
||||
return await self.send_command("devdetails")
|
||||
|
||||
async def restart(self) -> dict:
|
||||
"""
|
||||
API 'restart' command.
|
||||
"""Restart BMMiner using the API.
|
||||
|
||||
Returns a single "RESTART" before BMMiner restarts.
|
||||
:return: A reply informing of the restart.
|
||||
"""
|
||||
return await self.send_command("restart")
|
||||
|
||||
async def stats(self) -> dict:
|
||||
"""
|
||||
API 'stats' command.
|
||||
"""Get stats of each device/pool with more than 1 getwork.
|
||||
|
||||
Returns a dict containing stats for all device/pool with more than 1 getwork.
|
||||
:return: Stats of each device/pool with more than 1 getwork.
|
||||
"""
|
||||
return await self.send_command("stats")
|
||||
|
||||
async def estats(self, old: bool = False) -> dict:
|
||||
"""
|
||||
API 'estats' command.
|
||||
|
||||
Returns a dict containing stats for all device/pool with more than 1 getwork,
|
||||
"""Get stats of each device/pool with more than 1 getwork,
|
||||
ignoring zombie devices.
|
||||
|
||||
Parameters:
|
||||
old (optional): include zombie devices that became zombies less than 'old' seconds ago.
|
||||
:param old: Include zombie devices that became zombies less
|
||||
than 'old' seconds ago.
|
||||
|
||||
:return: Stats of each device/pool with more than 1 getwork,
|
||||
ignoring zombie devices.
|
||||
"""
|
||||
if old:
|
||||
return await self.send_command("estats", parameters="old")
|
||||
return await self.send_command("estats", parameters=old)
|
||||
else:
|
||||
return await self.send_command("estats")
|
||||
|
||||
async def check(self, command: str) -> dict:
|
||||
"""
|
||||
API 'check' command.
|
||||
"""Check if the command command exists in BMMiner.
|
||||
|
||||
Returns information about a command:
|
||||
:param command: The command to check.
|
||||
|
||||
:return: Information about a command:
|
||||
Exists (Y/N) <- the command exists in this version
|
||||
Access (Y/N) <- you have access to use the command
|
||||
|
||||
Parameters:
|
||||
command: the command to get information about.
|
||||
"""
|
||||
return await self.send_command("check", parameters=command)
|
||||
|
||||
async def failover_only(self, failover: bool) -> dict:
|
||||
"""
|
||||
API 'failover-only' command.
|
||||
"""Set failover-only.
|
||||
|
||||
Returns the STATUS section with what failover-only was set to.
|
||||
|
||||
Parameters:
|
||||
failover: what to set failover-only to.
|
||||
:param failover: What to set failover-only to.
|
||||
|
||||
:return: Confirmation of setting failover-only.
|
||||
"""
|
||||
return await self.send_command("failover-only", parameters=failover)
|
||||
|
||||
async def coin(self) -> dict:
|
||||
"""
|
||||
API 'coin' command.
|
||||
"""Get information on the current coin.
|
||||
|
||||
Returns information about the current coin being mined:
|
||||
:return: Information about the current coin being mined:
|
||||
Hash Method <- the hashing algorithm
|
||||
Current Block Time <- blocktime as a float, 0 means none
|
||||
Current Block Hash <- the hash of the current block, blank means none
|
||||
Current Block Hash <- the hash of the current block, blank
|
||||
means none
|
||||
LP <- whether LP is in use on at least 1 pool
|
||||
Network Difficulty: the current network difficulty
|
||||
"""
|
||||
return await self.send_command("coin")
|
||||
|
||||
async def debug(self, setting: str) -> dict:
|
||||
"""
|
||||
API 'debug' command.
|
||||
"""Set a debug setting.
|
||||
|
||||
Returns which debug setting was enabled or disabled.
|
||||
|
||||
Parameters:
|
||||
setting: which setting to switch to. Options are:
|
||||
:param setting: Which setting to switch to. Options are:
|
||||
Silent,
|
||||
Quiet,
|
||||
Verbose,
|
||||
@@ -345,42 +310,33 @@ class BMMinerAPI(BaseMinerAPI):
|
||||
PerDevice,
|
||||
WorkTime,
|
||||
Normal.
|
||||
|
||||
:return: Data on which debug setting was enabled or disabled.
|
||||
"""
|
||||
return await self.send_command("debug", parameters=setting)
|
||||
|
||||
async def setconfig(self, name: str, n: int) -> dict:
|
||||
"""
|
||||
API 'setconfig' command.
|
||||
"""Set config of name to value n.
|
||||
|
||||
Returns the STATUS section with the results of setting 'name' to N.
|
||||
|
||||
Parameters:
|
||||
name: name of the config setting to set. Options are:
|
||||
:param name: The name of the config setting to set. Options are:
|
||||
queue,
|
||||
scantime,
|
||||
expiry.
|
||||
n: the value to set the 'name' setting to.
|
||||
:param n: The value to set the 'name' setting to.
|
||||
|
||||
:return: The results of setting config of name to n.
|
||||
"""
|
||||
return await self.send_command("setconfig", parameters=f"{name}, {n}")
|
||||
return await self.send_command("setconfig", parameters=f"{name}, " f"{n}")
|
||||
|
||||
async def usbstats(self) -> dict:
|
||||
"""
|
||||
API 'usbstats' command.
|
||||
"""Get stats of all USB devices except ztex.
|
||||
|
||||
Returns a dict containing the stats of all USB devices except ztex.
|
||||
:return: The stats of all USB devices except ztex.
|
||||
"""
|
||||
return await self.send_command("usbstats")
|
||||
|
||||
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
|
||||
"""
|
||||
API 'pgaset' command.
|
||||
|
||||
Returns the STATUS section with the results of setting PGA N with opt[,val].
|
||||
|
||||
Parameters:
|
||||
n: the PGA to set the options on.
|
||||
opt: the option to set. Setting this to 'help' returns a help message.
|
||||
val: the value to set the option to.
|
||||
"""Set PGA option opt to val on PGA n.
|
||||
|
||||
Options:
|
||||
MMQ -
|
||||
@@ -389,97 +345,92 @@ class BMMinerAPI(BaseMinerAPI):
|
||||
CMR -
|
||||
opt: clock
|
||||
val: 100 - 220
|
||||
|
||||
:param n: The PGA to set the options on.
|
||||
:param opt: The option to set. Setting this to 'help'
|
||||
returns a help message.
|
||||
:param val: The value to set the option to.
|
||||
|
||||
:return: Confirmation of setting PGA n with opt[,val].
|
||||
"""
|
||||
if val:
|
||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}, {val}")
|
||||
return await self.send_command(
|
||||
"pgaset", parameters=f"{n}, " f"{opt}, " f"{val}"
|
||||
)
|
||||
else:
|
||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}")
|
||||
return await self.send_command("pgaset", parameters=f"{n}, " f"{opt}")
|
||||
|
||||
async def zero(self, which: str, summary: bool) -> dict:
|
||||
"""
|
||||
API 'zero' command.
|
||||
"""Zero a device.
|
||||
|
||||
Returns the STATUS section with info on the zero and optional summary.
|
||||
:param which: Which device to zero.
|
||||
Setting this to 'all' zeros all devices.
|
||||
Setting this to 'bestshare' zeros only the bestshare values
|
||||
for each pool and global.
|
||||
:param summary: Whether or not to show a full summary.
|
||||
|
||||
Parameters:
|
||||
which: which device to zero.
|
||||
Setting this to 'all' zeros all devices.
|
||||
Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
|
||||
summary: whether or not to show a full summary.
|
||||
|
||||
:return: the STATUS section with info on the zero and optional
|
||||
summary.
|
||||
"""
|
||||
return await self.send_command("zero", parameters=f"{which}, {summary}")
|
||||
|
||||
async def hotplug(self, n: int) -> dict:
|
||||
"""
|
||||
API 'hotplug' command.
|
||||
"""Enable hotplug.
|
||||
|
||||
Returns the STATUS section with whether or not hotplug was enabled.
|
||||
:param n: The device number to set hotplug on.
|
||||
|
||||
:return: Information on hotplug status.
|
||||
"""
|
||||
return await self.send_command("hotplug", parameters=n)
|
||||
|
||||
async def asc(self, n: int) -> dict:
|
||||
"""
|
||||
API 'asc' command.
|
||||
"""Get data for ASC device n.
|
||||
|
||||
Returns a dict containing the details of a single ASC of number N.
|
||||
:param n: The device to get data for.
|
||||
|
||||
n: the ASC device to get details of.
|
||||
:return: The data for ASC device n.
|
||||
"""
|
||||
return await self.send_command("asc", parameters=n)
|
||||
|
||||
async def ascenable(self, n: int) -> dict:
|
||||
"""
|
||||
API 'ascenable' command.
|
||||
"""Enable ASC device n.
|
||||
|
||||
Returns the STATUS section with the results of enabling the ASC device N.
|
||||
:param n: The device to enable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the ASC to enable.
|
||||
:return: Confirmation of enabling ASC device n.
|
||||
"""
|
||||
return await self.send_command("ascenable", parameters=n)
|
||||
|
||||
async def ascdisable(self, n: int) -> dict:
|
||||
"""
|
||||
API 'ascdisable' command.
|
||||
"""Disable ASC device n.
|
||||
|
||||
Returns the STATUS section with the results of disabling the ASC device N.
|
||||
:param n: The device to disable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the ASC to disable.
|
||||
:return: Confirmation of disabling ASC device n.
|
||||
"""
|
||||
return await self.send_command("ascdisable", parameters=n)
|
||||
|
||||
async def ascidentify(self, n: int) -> dict:
|
||||
"""
|
||||
API 'ascidentify' command.
|
||||
"""Identify ASC device n.
|
||||
|
||||
Returns the STATUS section with the results of identifying the ASC device N.
|
||||
:param n: The device to identify.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to identify.
|
||||
:return: Confirmation of identifying ASC device n.
|
||||
"""
|
||||
return await self.send_command("ascidentify", parameters=n)
|
||||
|
||||
async def asccount(self) -> dict:
|
||||
"""
|
||||
API 'asccount' command.
|
||||
"""Get data on the number of ASC devices and their info.
|
||||
|
||||
Returns a dict containing the number of ASC devices.
|
||||
:return: Data on all ASC devices.
|
||||
"""
|
||||
return await self.send_command("asccount")
|
||||
|
||||
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
|
||||
"""
|
||||
API 'ascset' command.
|
||||
"""Set ASC n option opt to value val.
|
||||
|
||||
Returns the STATUS section with the results of setting ASC N with opt[,val].
|
||||
|
||||
Parameters:
|
||||
n: the ASC to set the options on.
|
||||
opt: the option to set. Setting this to 'help' returns a help message.
|
||||
val: the value to set the option to.
|
||||
|
||||
Options:
|
||||
Sets an option on the ASC n to a value. Allowed options are:
|
||||
AVA+BTB -
|
||||
opt: freq
|
||||
val: 256 - 1024 (chip frequency)
|
||||
@@ -513,6 +464,14 @@ class BMMinerAPI(BaseMinerAPI):
|
||||
|
||||
opt: clock
|
||||
val: 0 - 15
|
||||
|
||||
|
||||
:param n: The ASC to set the options on.
|
||||
:param opt: The option to set. Setting this to 'help' returns a
|
||||
help message.
|
||||
:param val: The value to set the option to.
|
||||
|
||||
:return: Confirmation of setting option opt to value val.
|
||||
"""
|
||||
if val:
|
||||
return await self.send_command("ascset", parameters=f"{n}, {opt}, {val}")
|
||||
@@ -520,17 +479,15 @@ class BMMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command("ascset", parameters=f"{n}, {opt}")
|
||||
|
||||
async def lcd(self) -> dict:
|
||||
"""
|
||||
API 'lcd' command.
|
||||
"""Get a general all-in-one status summary of the miner.
|
||||
|
||||
Returns a dict containing an all in one status summary of the miner.
|
||||
:return: An all-in-one status summary of the miner.
|
||||
"""
|
||||
return await self.send_command("lcd")
|
||||
|
||||
async def lockstats(self) -> dict:
|
||||
"""
|
||||
API 'lockstats' command.
|
||||
"""Write lockstats to STDERR.
|
||||
|
||||
Returns the STATUS section with the result of writing the lock stats to STDERR.
|
||||
:return: The result of writing the lock stats to STDERR.
|
||||
"""
|
||||
return await self.send_command("lockstats")
|
||||
|
||||
144
API/bosminer.py
144
API/bosminer.py
@@ -2,64 +2,64 @@ from API import BaseMinerAPI
|
||||
|
||||
|
||||
class BOSMinerAPI(BaseMinerAPI):
|
||||
"""
|
||||
A class that abstracts the BOSMiner API in the miners.
|
||||
"""An abstraction of the BOSMiner API.
|
||||
|
||||
Each method corresponds to an API command in BOSMiner.
|
||||
|
||||
BOSMiner API documentation:
|
||||
https://docs.braiins.com/os/plus-en/Development/1_api.html
|
||||
|
||||
Parameters:
|
||||
ip: the IP address of the miner.
|
||||
port (optional): the port of the API on the miner (standard is 4028)
|
||||
This class abstracts use of the BOSMiner API, as well as the
|
||||
methods for sending commands to it. The self.send_command()
|
||||
function handles sending a command to the miner asynchronously, and
|
||||
as such is the base for many of the functions in this class, which
|
||||
rely on it to send the command for them.
|
||||
|
||||
:param ip: The IP of the miner to reference the API on.
|
||||
:param port: The port to reference the API on. Default is 4028.
|
||||
"""
|
||||
|
||||
def __init__(self, ip, port=4028):
|
||||
super().__init__(ip, port)
|
||||
|
||||
async def asccount(self) -> dict:
|
||||
"""
|
||||
API 'asccount' command.
|
||||
"""Get data on the number of ASC devices and their info.
|
||||
|
||||
Returns a dict containing the number of ASC devices.
|
||||
:return: Data on all ASC devices.
|
||||
"""
|
||||
return await self.send_command("asccount")
|
||||
|
||||
async def asc(self, n: int) -> dict:
|
||||
"""
|
||||
API 'asc' command.
|
||||
"""Get data for ASC device n.
|
||||
|
||||
Returns a dict containing the details of a single ASC of number N.
|
||||
:param n: The device to get data for.
|
||||
|
||||
n: the ASC device to get details of.
|
||||
:return: The data for ASC device n.
|
||||
"""
|
||||
return await self.send_command("asc", parameters=n)
|
||||
|
||||
async def devdetails(self) -> dict:
|
||||
"""
|
||||
API 'devdetails' command.
|
||||
"""Get data on all devices with their static details.
|
||||
|
||||
Returns a dict containing all devices with their static details.
|
||||
:return: Data on all devices with their static details.
|
||||
"""
|
||||
return await self.send_command("devdetails")
|
||||
|
||||
async def devs(self) -> dict:
|
||||
"""
|
||||
API 'devs' command.
|
||||
"""Get data on each PGA/ASC with their details.
|
||||
|
||||
Returns a dict containing each PGA/ASC with their details.
|
||||
:return: Data on each PGA/ASC with their details.
|
||||
"""
|
||||
return await self.send_command("devs")
|
||||
|
||||
async def edevs(self, old: bool = False) -> dict:
|
||||
"""
|
||||
API 'edevs' command.
|
||||
"""Get data on each PGA/ASC with their details, ignoring
|
||||
blacklisted and zombie devices.
|
||||
|
||||
Returns a dict containing each PGA/ASC with their details,
|
||||
ignoring blacklisted devices and zombie devices.
|
||||
:param old: Include zombie devices that became zombies less
|
||||
than 'old' seconds ago
|
||||
|
||||
Parameters:
|
||||
old (optional): include zombie devices that became zombies less than 'old' seconds ago
|
||||
:return: Data on each PGA/ASC with their details.
|
||||
"""
|
||||
if old:
|
||||
return await self.send_command("edevs", parameters="old")
|
||||
@@ -67,80 +67,76 @@ class BOSMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command("edevs")
|
||||
|
||||
async def pools(self) -> dict:
|
||||
"""
|
||||
API 'pools' command.
|
||||
"""Get pool information.
|
||||
|
||||
Returns a dict containing the status of each pool.
|
||||
:return: Miner pool information.
|
||||
"""
|
||||
return await self.send_command("pools")
|
||||
|
||||
async def summary(self) -> dict:
|
||||
"""
|
||||
API 'summary' command.
|
||||
"""Get the status summary of the miner.
|
||||
|
||||
Returns a dict containing the status summary of the miner.
|
||||
:return: The status summary of the miner.
|
||||
"""
|
||||
return await self.send_command("summary")
|
||||
|
||||
async def stats(self) -> dict:
|
||||
"""
|
||||
API 'stats' command.
|
||||
"""Get stats of each device/pool with more than 1 getwork.
|
||||
|
||||
Returns a dict containing stats for all device/pool with more than 1 getwork.
|
||||
:return: Stats of each device/pool with more than 1 getwork.
|
||||
"""
|
||||
return await self.send_command("stats")
|
||||
|
||||
async def version(self) -> dict:
|
||||
"""
|
||||
API 'version' command.
|
||||
"""Get miner version info.
|
||||
|
||||
Returns a dict containing version information.
|
||||
:return: Miner version information.
|
||||
"""
|
||||
return await self.send_command("version")
|
||||
|
||||
async def estats(self) -> dict:
|
||||
"""
|
||||
API 'estats' command.
|
||||
|
||||
Returns a dict containing stats for all device/pool with more than 1 getwork,
|
||||
async def estats(self, old: bool = False) -> dict:
|
||||
"""Get stats of each device/pool with more than 1 getwork,
|
||||
ignoring zombie devices.
|
||||
|
||||
Parameters:
|
||||
old (optional): include zombie devices that became zombies less than 'old' seconds ago.
|
||||
:param old: Include zombie devices that became zombies less
|
||||
than 'old' seconds ago.
|
||||
|
||||
:return: Stats of each device/pool with more than 1 getwork,
|
||||
ignoring zombie devices.
|
||||
"""
|
||||
return await self.send_command("estats")
|
||||
if old:
|
||||
return await self.send_command("estats", parameters=old)
|
||||
else:
|
||||
return await self.send_command("estats")
|
||||
|
||||
async def check(self, command: str) -> dict:
|
||||
"""
|
||||
API 'check' command.
|
||||
"""Check if the command command exists in BOSMiner.
|
||||
|
||||
Returns information about a command:
|
||||
:param command: The command to check.
|
||||
|
||||
:return: Information about a command:
|
||||
Exists (Y/N) <- the command exists in this version
|
||||
Access (Y/N) <- you have access to use the command
|
||||
|
||||
Parameters:
|
||||
command: the command to get information about.
|
||||
"""
|
||||
return await self.send_command("check", parameters=command)
|
||||
|
||||
async def coin(self) -> dict:
|
||||
"""
|
||||
API 'coin' command.
|
||||
"""Get information on the current coin.
|
||||
|
||||
Returns information about the current coin being mined:
|
||||
:return: Information about the current coin being mined:
|
||||
Hash Method <- the hashing algorithm
|
||||
Current Block Time <- blocktime as a float, 0 means none
|
||||
Current Block Hash <- the hash of the current block, blank means none
|
||||
Current Block Hash <- the hash of the current block, blank
|
||||
means none
|
||||
LP <- whether LP is in use on at least 1 pool
|
||||
Network Difficulty: the current network difficulty
|
||||
"""
|
||||
return await self.send_command("coin")
|
||||
|
||||
async def lcd(self) -> dict:
|
||||
"""
|
||||
API 'lcd' command.
|
||||
"""Get a general all-in-one status summary of the miner.
|
||||
|
||||
Returns a dict containing an all in one status summary of the miner.
|
||||
:return: An all-in-one status summary of the miner.
|
||||
"""
|
||||
return await self.send_command("lcd")
|
||||
|
||||
@@ -170,53 +166,43 @@ class BOSMinerAPI(BaseMinerAPI):
|
||||
# return await self.send_command("removepool", parameters=n)
|
||||
|
||||
async def fans(self) -> dict:
|
||||
"""
|
||||
API 'fans' command.
|
||||
"""Get fan data.
|
||||
|
||||
Returns a dict containing information on fans and fan speeds.
|
||||
:return: Data on the fans of the miner.
|
||||
"""
|
||||
return await self.send_command("fans")
|
||||
|
||||
async def tempctrl(self) -> dict:
|
||||
"""
|
||||
API 'tempctrl' command.
|
||||
"""Get temperature control data.
|
||||
|
||||
Returns a dict containing temp control configuration.
|
||||
:return: Data about the temp control settings of the miner.
|
||||
"""
|
||||
return await self.send_command("tempctrl")
|
||||
|
||||
async def temps(self) -> dict:
|
||||
"""
|
||||
API 'temps' command.
|
||||
"""Get temperature data.
|
||||
|
||||
Returns a dict containing temperature information.
|
||||
:return: Data on the temps of the miner.
|
||||
"""
|
||||
return await self.send_command("temps")
|
||||
|
||||
async def tunerstatus(self) -> dict:
|
||||
"""
|
||||
API 'tunerstatus' command.
|
||||
"""Get tuner status data
|
||||
|
||||
Returns a dict containing tuning stats.
|
||||
:return: Data on the status of autotuning.
|
||||
"""
|
||||
return await self.send_command("tunerstatus")
|
||||
|
||||
async def pause(self) -> dict:
|
||||
"""
|
||||
API 'pause' command.
|
||||
"""Pause mining.
|
||||
|
||||
Pauses mining and stops power consumption and waits for resume command.
|
||||
|
||||
Returns a dict stating that the miner paused mining.
|
||||
:return: Confirmation of pausing mining.
|
||||
"""
|
||||
return await self.send_command("pause")
|
||||
|
||||
async def resume(self) -> dict:
|
||||
"""
|
||||
API 'pause' command.
|
||||
"""Resume mining.
|
||||
|
||||
Resumes mining on the miner.
|
||||
|
||||
Returns a dict stating that the miner resumed mining.
|
||||
:return: Confirmation of resuming mining.
|
||||
"""
|
||||
return await self.send_command("resume")
|
||||
|
||||
564
API/btminer.py
564
API/btminer.py
@@ -1,32 +1,45 @@
|
||||
from API import BaseMinerAPI, APIError
|
||||
|
||||
from passlib.handlers import md5_crypt
|
||||
import asyncio
|
||||
import re
|
||||
import json
|
||||
import hashlib
|
||||
import binascii
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from passlib.handlers.md5_crypt import md5_crypt
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
from API import BaseMinerAPI, APIError
|
||||
from settings import WHATSMINER_PWD
|
||||
|
||||
|
||||
### IMPORTANT ###
|
||||
# you need to change the password of the miners using
|
||||
# the whatsminer tool, then you can set them back to
|
||||
# admin with this tool, but they must be changed to
|
||||
# something else and set back to admin with this or
|
||||
# the privileged API will not work using admin as
|
||||
# the password.
|
||||
# you need to change the password of the miners using the Whatsminer
|
||||
# tool, then you can set them back to admin with this tool, but they
|
||||
# must be changed to something else and set back to admin with this
|
||||
# or the privileged API will not work using admin as the password. If
|
||||
# you change the password, you can pass that to the this class as pwd,
|
||||
# or add it as the Whatsminer_pwd in the settings.toml file.
|
||||
|
||||
|
||||
def _crypt(word: str, salt: str) -> str:
|
||||
"""Encrypts a word with a salt, using a standard salt format.
|
||||
|
||||
Encrypts a word using a salt with the format
|
||||
'\s*\$(\d+)\$([\w\./]*)\$'. If this format is not used, a
|
||||
ValueError is raised.
|
||||
|
||||
:param word: The word to be encrypted.
|
||||
:param salt: The salt to encrypt the word.
|
||||
:return: An MD5 hash of the word with the salt.
|
||||
"""
|
||||
# compile a standard format for the salt
|
||||
standard_salt = re.compile('\s*\$(\d+)\$([\w\./]*)\$')
|
||||
standard_salt = re.compile("\s*\$(\d+)\$([\w\./]*)\$")
|
||||
# check if the salt matches
|
||||
match = standard_salt.match(salt)
|
||||
# if the matching fails, the salt is incorrect
|
||||
if not match:
|
||||
raise ValueError("salt format is not correct")
|
||||
raise ValueError("Salt format is not correct.")
|
||||
# save the matched salt in a new variable
|
||||
new_salt = match.group(2)
|
||||
# encrypt the word with the salt using md5
|
||||
@@ -34,35 +47,65 @@ def _crypt(word: str, salt: str) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def _add_to_16(s: str) -> bytes:
|
||||
"""Add null bytes to a string until the length is 16"""
|
||||
while len(s) % 16 != 0:
|
||||
s += '\0'
|
||||
return str.encode(s) # return bytes
|
||||
def _add_to_16(string: str) -> bytes:
|
||||
"""Add null bytes to a string until the length is a multiple 16
|
||||
|
||||
:param string: The string to lengthen to a multiple of 16 and
|
||||
encode.
|
||||
|
||||
:return: The input string as bytes with a multiple of 16 as the
|
||||
length.
|
||||
"""
|
||||
while len(string) % 16 != 0:
|
||||
string += "\0"
|
||||
return str.encode(string) # return bytes
|
||||
|
||||
|
||||
def parse_btminer_priviledge_data(token_data, data):
|
||||
def parse_btminer_priviledge_data(token_data: dict, data: dict):
|
||||
"""Parses data returned from the BTMiner privileged API.
|
||||
|
||||
Parses data from the BTMiner privileged API using the the token
|
||||
from the API in an AES format.
|
||||
|
||||
:param token_data: The token information from self.get_token().
|
||||
:param data: The data to parse, returned from the API.
|
||||
|
||||
:return: A decoded dict version of the privileged command output.
|
||||
"""
|
||||
# get the encoded data from the dict
|
||||
enc_data = data['enc']
|
||||
enc_data = data["enc"]
|
||||
# get the aes key from the token data
|
||||
aeskey = hashlib.sha256(token_data['host_passwd_md5'].encode()).hexdigest()
|
||||
aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest()
|
||||
# unhexlify the aes key
|
||||
aeskey = binascii.unhexlify(aeskey.encode())
|
||||
# create the required decryptor
|
||||
aes = Cipher(algorithms.AES(aeskey), modes.ECB())
|
||||
decryptor = aes.decryptor()
|
||||
# decode the message with the decryptor
|
||||
ret_msg = json.loads(decryptor.update(
|
||||
base64.decodebytes(bytes(enc_data, encoding='utf8'))
|
||||
).rstrip(b'\0').decode("utf8"))
|
||||
ret_msg = json.loads(
|
||||
decryptor.update(base64.decodebytes(bytes(enc_data, encoding="utf8")))
|
||||
.rstrip(b"\0")
|
||||
.decode("utf8")
|
||||
)
|
||||
return ret_msg
|
||||
|
||||
|
||||
def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
|
||||
"""Create a privileged command to send to the BTMiner API.
|
||||
|
||||
Creates a privileged command using the token from the API and the
|
||||
command as a dict of {'command': cmd}, with cmd being any command
|
||||
that the miner API accepts.
|
||||
|
||||
:param token_data: The token information from self.get_token().
|
||||
:param command: The command to turn into a privileged command.
|
||||
|
||||
:return: The encrypted privileged command to be sent to the miner.
|
||||
"""
|
||||
# add token to command
|
||||
command['token'] = token_data['host_sign']
|
||||
command["token"] = token_data["host_sign"]
|
||||
# encode host_passwd data and get hexdigest
|
||||
aeskey = hashlib.sha256(token_data['host_passwd_md5'].encode()).hexdigest()
|
||||
aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest()
|
||||
# unhexlify the encoded host_passwd
|
||||
aeskey = binascii.unhexlify(aeskey.encode())
|
||||
# create a new AES key
|
||||
@@ -71,23 +114,71 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
|
||||
# dump the command to json
|
||||
api_json_str = json.dumps(command)
|
||||
# encode the json command with the aes key
|
||||
api_json_str_enc = base64.encodebytes(encryptor.update(_add_to_16(api_json_str))).decode("utf-8").replace("\n", "")
|
||||
api_json_str_enc = (
|
||||
base64.encodebytes(encryptor.update(_add_to_16(api_json_str)))
|
||||
.decode("utf-8")
|
||||
.replace("\n", "")
|
||||
)
|
||||
# label the data as being encoded
|
||||
data_enc = {'enc': 1, 'data': api_json_str_enc}
|
||||
data_enc = {"enc": 1, "data": api_json_str_enc}
|
||||
# dump the labeled data to json
|
||||
api_packet_str = json.dumps(data_enc)
|
||||
return api_packet_str.encode('utf-8')
|
||||
return api_packet_str.encode("utf-8")
|
||||
|
||||
|
||||
class BTMinerAPI(BaseMinerAPI):
|
||||
def __init__(self, ip, port=4028, pwd: str = "admin"):
|
||||
"""An abstraction of the API for MicroBT Whatsminers, BTMiner.
|
||||
|
||||
Each method corresponds to an API command in BMMiner.
|
||||
|
||||
This class abstracts use of the BTMiner API, as well as the
|
||||
methods for sending commands to it. The self.send_command()
|
||||
function handles sending a command to the miner asynchronously, and
|
||||
as such is the base for many of the functions in this class, which
|
||||
rely on it to send the command for them.
|
||||
|
||||
All privileged commands for BTMiner's API require that you change
|
||||
the password of the miners using the Whatsminer tool, and it can be
|
||||
changed back to admin with this tool after. Set the new password
|
||||
either by passing it to the __init__ method, or changing it in
|
||||
settings.toml.
|
||||
|
||||
Additionally, the API commands for the privileged API must be
|
||||
encoded using a token from the miner, all privileged commands do
|
||||
this automatically for you and will decode the output to look like
|
||||
a normal output from a miner API.
|
||||
|
||||
:param ip: The IP of the miner to reference the API on.
|
||||
:param port: The port to reference the API on. Default is 4028.
|
||||
:param pwd: The admin password of the miner. Default is admin.
|
||||
"""
|
||||
|
||||
def __init__(self, ip, port=4028, pwd: str = WHATSMINER_PWD):
|
||||
super().__init__(ip, port)
|
||||
self.admin_pwd = pwd
|
||||
self.current_token = None
|
||||
|
||||
async def send_command(self, command: str | bytes, **kwargs) -> dict:
|
||||
"""Send an API command to the miner and return the result."""
|
||||
# check if command is a string, if its bytes its encoded and needs to be send raw
|
||||
async def send_command(
|
||||
self,
|
||||
command: str or bytes,
|
||||
parameters: str or int or bool = None,
|
||||
ignore_errors: bool = False,
|
||||
) -> dict:
|
||||
"""Send a command to the miner API.
|
||||
|
||||
Send a command using an asynchronous connection, load the data,
|
||||
parse encoded data if needed, and return the result.
|
||||
|
||||
:param command: The command to send to the miner.
|
||||
:param parameters: Parameters to pass to the command.
|
||||
:param ignore_errors: Ignore the E (Error) status code from the
|
||||
API.
|
||||
|
||||
:return: The data received from the API after sending the
|
||||
command.
|
||||
"""
|
||||
# check if command is a string
|
||||
# if its bytes its encoded and needs to be sent raw
|
||||
if isinstance(command, str):
|
||||
# if it is a string, put it into the standard command format
|
||||
command = json.dumps({"command": command}).encode("utf-8")
|
||||
@@ -115,7 +206,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
break
|
||||
data += d
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.info(f"{str(self.ip)}: {e}")
|
||||
|
||||
data = self.load_api_data(data)
|
||||
|
||||
@@ -123,46 +214,86 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
# check if th returned data is encoded
|
||||
if 'enc' in data.keys():
|
||||
# check if the returned data is encoded
|
||||
if "enc" in data.keys():
|
||||
# try to parse the encoded data
|
||||
try:
|
||||
data = parse_btminer_priviledge_data(self.current_token, data)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.info(f"{str(self.ip)}: {e}")
|
||||
|
||||
# if it fails to validate, it is likely an error
|
||||
if not self.validate_command_output(data):
|
||||
raise APIError(data["Msg"])
|
||||
if not ignore_errors:
|
||||
# if it fails to validate, it is likely an error
|
||||
validation = self.validate_command_output(data)
|
||||
if not validation[0]:
|
||||
raise APIError(validation[1])
|
||||
|
||||
# return the parsed json as a dict
|
||||
return data
|
||||
|
||||
async def get_token(self):
|
||||
"""
|
||||
API 'get_token' command.
|
||||
"""Gets token information from the API.
|
||||
|
||||
Returns an encoded token and md5 password, which are used for the privileged API.
|
||||
:return: An encoded token and md5 password, which are used
|
||||
for the privileged API.
|
||||
"""
|
||||
# get the token
|
||||
data = await self.send_command("get_token")
|
||||
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + '$')
|
||||
pwd = pwd.split('$')
|
||||
|
||||
# encrypt the admin password with the salt
|
||||
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + "$")
|
||||
pwd = pwd.split("$")
|
||||
|
||||
# take the 4th item from the pwd split
|
||||
host_passwd_md5 = pwd[3]
|
||||
tmp = _crypt(pwd[3] + data["Msg"]["time"], "$1$" + data["Msg"]["newsalt"] + '$')
|
||||
tmp = tmp.split('$')
|
||||
|
||||
# encrypt the pwd with the time and new salt
|
||||
tmp = _crypt(pwd[3] + data["Msg"]["time"], "$1$" + data["Msg"]["newsalt"] + "$")
|
||||
tmp = tmp.split("$")
|
||||
|
||||
# take the 4th item from the encrypted pwd split
|
||||
host_sign = tmp[3]
|
||||
self.current_token = {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
|
||||
return {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
|
||||
|
||||
# set the current token
|
||||
self.current_token = {
|
||||
"host_sign": host_sign,
|
||||
"host_passwd_md5": host_passwd_md5,
|
||||
}
|
||||
return self.current_token
|
||||
|
||||
#### PRIVILEGED COMMANDS ####
|
||||
# Please read the top of this file to learn
|
||||
# how to configure the whatsminer API to
|
||||
# how to configure the Whatsminer API to
|
||||
# use these commands.
|
||||
|
||||
async def update_pools(self,
|
||||
pool_1: str, worker_1: str, passwd_1: str,
|
||||
pool_2: str = None, worker_2: str = None, passwd_2: str = None,
|
||||
pool_3: str = None, worker_3: str = None, passwd_3: str = None):
|
||||
async def update_pools(
|
||||
self,
|
||||
pool_1: str,
|
||||
worker_1: str,
|
||||
passwd_1: str,
|
||||
pool_2: str = None,
|
||||
worker_2: str = None,
|
||||
passwd_2: str = None,
|
||||
pool_3: str = None,
|
||||
worker_3: str = None,
|
||||
passwd_3: str = None,
|
||||
):
|
||||
"""Update the pools of the miner using the API.
|
||||
|
||||
Update the pools of the miner using the API, only works after
|
||||
changing the password of the miner using the Whatsminer tool.
|
||||
|
||||
:param pool_1: The URL to update pool 1 to.
|
||||
:param worker_1: The worker name for pool 1 to update to.
|
||||
:param passwd_1: The password for pool 1 to update to.
|
||||
:param pool_2: The URL to update pool 2 to.
|
||||
:param worker_2: The worker name for pool 2 to update to.
|
||||
:param passwd_2: The password for pool 2 to update to.
|
||||
:param pool_3: The URL to update pool 3 to.
|
||||
:param worker_3: The worker name for pool 3 to update to.
|
||||
:param passwd_3: The password for pool 3 to update to.
|
||||
:return: A dict from the API to confirm the pools were updated.
|
||||
"""
|
||||
# get the token and password from the miner
|
||||
token_data = await self.get_token()
|
||||
|
||||
@@ -172,20 +303,32 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
elif pool_2 and pool_3:
|
||||
command = {
|
||||
"cmd": "update_pools",
|
||||
"pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1,
|
||||
"pool2": pool_2, "worker2": worker_2, "passwd2": passwd_2,
|
||||
"pool3": pool_3, "worker3": worker_3, "passwd3": passwd_3,
|
||||
"pool1": pool_1,
|
||||
"worker1": worker_1,
|
||||
"passwd1": passwd_1,
|
||||
"pool2": pool_2,
|
||||
"worker2": worker_2,
|
||||
"passwd2": passwd_2,
|
||||
"pool3": pool_3,
|
||||
"worker3": worker_3,
|
||||
"passwd3": passwd_3,
|
||||
}
|
||||
elif pool_2:
|
||||
command = {
|
||||
"cmd": "update_pools",
|
||||
"pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1,
|
||||
"pool2": pool_2, "worker2": worker_2, "passwd2": passwd_2
|
||||
"pool1": pool_1,
|
||||
"worker1": worker_1,
|
||||
"passwd1": passwd_1,
|
||||
"pool2": pool_2,
|
||||
"worker2": worker_2,
|
||||
"passwd2": passwd_2,
|
||||
}
|
||||
else:
|
||||
command = {
|
||||
"cmd": "update_pools",
|
||||
"pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1,
|
||||
"pool1": pool_1,
|
||||
"worker1": worker_1,
|
||||
"passwd1": passwd_1,
|
||||
}
|
||||
# encode the command with the token data
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
@@ -193,10 +336,12 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def restart(self):
|
||||
"""
|
||||
API 'restart_btminer' command
|
||||
"""Restart BTMiner using the API.
|
||||
|
||||
Returns a reply informing of the restart and restarts BTMiner.
|
||||
Restart BTMiner using the API, only works after changing
|
||||
the password of the miner using the Whatsminer tool.
|
||||
|
||||
:return: A reply informing of the restart.
|
||||
"""
|
||||
command = {"cmd": "restart_btminer"}
|
||||
token_data = await self.get_token()
|
||||
@@ -204,15 +349,13 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def power_off(self, respbefore: bool = True):
|
||||
"""
|
||||
API 'power_off' command.
|
||||
"""Power off the miner using the API.
|
||||
|
||||
Powers off the mining of the miner.
|
||||
Power off the miner using the API, only works after changing
|
||||
the password of the miner using the Whatsminer tool.
|
||||
|
||||
Returns info on the power off.
|
||||
|
||||
Parameters:
|
||||
respbefore (optional): respond before powering off.
|
||||
:param respbefore: Whether to respond before powering off.
|
||||
:return: A reply informing of the status of powering off.
|
||||
"""
|
||||
if respbefore:
|
||||
command = {"cmd": "power_off", "respbefore": "true"}
|
||||
@@ -223,12 +366,12 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def power_on(self):
|
||||
"""
|
||||
API 'power_on' command.
|
||||
"""Power on the miner using the API.
|
||||
|
||||
Powers on the mining of the miner.
|
||||
Power on the miner using the API, only works after changing
|
||||
the password of the miner using the Whatsminer tool.
|
||||
|
||||
Returns info on the power on.
|
||||
:return: A reply informing of the status of powering on.
|
||||
"""
|
||||
command = {"cmd": "power_on"}
|
||||
token_data = await self.get_token()
|
||||
@@ -236,44 +379,55 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def reset_led(self):
|
||||
"""
|
||||
API 'reset_led' command.
|
||||
"""Reset the LED on the miner using the API.
|
||||
|
||||
Resets the LED flashing to normal.
|
||||
Reset the LED on the miner using the API, only works after
|
||||
changing the password of the miner using the Whatsminer tool.
|
||||
|
||||
Returns a confirmation of resetting the LED.
|
||||
:return: A reply informing of the status of resetting the LED.
|
||||
"""
|
||||
command = {"cmd": "set_led", "param": "auto"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def set_led(self, color: str = "red", period: int = 2000, duration: int = 1000, start: int = 0):
|
||||
"""
|
||||
API 'set_led' command.
|
||||
async def set_led(
|
||||
self,
|
||||
color: str = "red",
|
||||
period: int = 2000,
|
||||
duration: int = 1000,
|
||||
start: int = 0,
|
||||
):
|
||||
"""Set the LED on the miner using the API.
|
||||
|
||||
Sets the LED to do some pattern set with parameters.
|
||||
Set the LED on the miner using the API, only works after
|
||||
changing the password of the miner using the Whatsminer tool.
|
||||
|
||||
Returns a confirmation of setting the LED.
|
||||
|
||||
Parameters:
|
||||
color: 'red' or 'green'
|
||||
period: flash cycle in ms
|
||||
duration: led on time in the cycle in ms
|
||||
start: led on time offset in the cycle in ms
|
||||
:param color: The LED color to set, either 'red' or 'green'.
|
||||
:param period: The flash cycle in ms.
|
||||
:param duration: LED on time in the cycle in ms.
|
||||
:param start: LED on time offset in the cycle in ms.
|
||||
:return: A reply informing of the status of setting the LED.
|
||||
"""
|
||||
command = {"cmd": "set_led", "color": color, "period": period, "duration": duration, "start": start}
|
||||
command = {
|
||||
"cmd": "set_led",
|
||||
"color": color,
|
||||
"period": period,
|
||||
"duration": duration,
|
||||
"start": start,
|
||||
}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def set_low_power(self):
|
||||
"""
|
||||
API 'set_low_power' command.
|
||||
|
||||
Sets the miner to low power mode.
|
||||
|
||||
Returns the status of setting the miner to low power mode.
|
||||
"""Set low power mode on the miner using the API.
|
||||
|
||||
Set low power mode on the miner using the API, only works after
|
||||
changing the password of the miner using the Whatsminer tool.
|
||||
|
||||
:return: A reply informing of the status of setting low power
|
||||
mode.
|
||||
"""
|
||||
command = {"cmd": "set_low_power"}
|
||||
token_data = await self.get_token()
|
||||
@@ -286,12 +440,9 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return NotImplementedError
|
||||
|
||||
async def reboot(self):
|
||||
"""
|
||||
API 'reboot' command.
|
||||
|
||||
Reboots the miner.
|
||||
|
||||
Returns the status of the command then reboots.
|
||||
"""Reboot the miner using the API.
|
||||
|
||||
:return: A reply informing of the status of the reboot.
|
||||
"""
|
||||
command = {"cmd": "reboot"}
|
||||
token_data = await self.get_token()
|
||||
@@ -299,12 +450,9 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def factory_reset(self):
|
||||
"""
|
||||
API 'factory_reset' command.
|
||||
"""Reset the miner to factory defaults.
|
||||
|
||||
Resets the miner to factory defaults.
|
||||
|
||||
Returns the status of the command then resets.
|
||||
:return: A reply informing of the status of the reset.
|
||||
"""
|
||||
command = {"cmd": "factory_reset"}
|
||||
token_data = await self.get_token()
|
||||
@@ -312,48 +460,58 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def update_pwd(self, old_pwd: str, new_pwd: str):
|
||||
"""
|
||||
API 'update_pwd' command.
|
||||
"""Update the admin user's password.
|
||||
|
||||
Updates the admin user's password.
|
||||
|
||||
Returns the status of setting the password to the new password.
|
||||
|
||||
Parameters:
|
||||
old_pwd: the old admin password.
|
||||
new_pwd: the new password to set. Max length of 8 bytes, using letters, numbers, and underscores.
|
||||
Update the admin user's password, only works after changing the
|
||||
password of the miner using the Whatsminer tool. New password
|
||||
has a max length of 8 bytes, using letters, numbers, and
|
||||
underscores.
|
||||
|
||||
:param old_pwd: The old admin password.
|
||||
:param new_pwd: The new password to set.
|
||||
:return: A reply informing of the status of setting the
|
||||
password.
|
||||
"""
|
||||
# check if password length is greater than 8 bytes
|
||||
if len(new_pwd.encode('utf-8')) > 8:
|
||||
if len(new_pwd.encode("utf-8")) > 8:
|
||||
return APIError(
|
||||
f"New password too long, the max length is 8. Password size: {len(new_pwd.encode('utf-8'))}")
|
||||
f"New password too long, the max length is 8. "
|
||||
f"Password size: {len(new_pwd.encode('utf-8'))}"
|
||||
)
|
||||
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def set_target_freq(self, percent: int):
|
||||
"""
|
||||
API 'set_target_freq' command.
|
||||
"""Update the target frequency.
|
||||
|
||||
Sets the frequency for the miner ot use.
|
||||
Update the target frequency, only works after changing the
|
||||
password of the miner using the Whatsminer tool. The new
|
||||
frequency must be between -10% and 100%.
|
||||
|
||||
Returns the status of setting the frequency.
|
||||
:param percent: The frequency % to set.
|
||||
:return: A reply informing of the status of setting the
|
||||
frequency.
|
||||
"""
|
||||
if not -10 < percent < 100:
|
||||
return APIError(f"Frequency % is outside of the allowed range. Please set a % between -10 and 100")
|
||||
return APIError(
|
||||
f"Frequency % is outside of the allowed "
|
||||
f"range. Please set a % between -10 and "
|
||||
f"100"
|
||||
)
|
||||
command = {"cmd": "set_target_freq", "percent": str(percent)}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def enable_fast_boot(self):
|
||||
"""
|
||||
API 'enable_fast_boot' command.
|
||||
"""Turn on fast boot.
|
||||
|
||||
Turns on the fast boot feature on the miner.
|
||||
Turn on fast boot, only works after changing the password of
|
||||
the miner using the Whatsminer tool.
|
||||
|
||||
Returns the status of setting the fast boot to on.
|
||||
:return: A reply informing of the status of enabling fast boot.
|
||||
"""
|
||||
command = {"cmd": "enable_btminer_fast_boot"}
|
||||
token_data = await self.get_token()
|
||||
@@ -361,12 +519,12 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def disable_fast_boot(self):
|
||||
"""
|
||||
API 'disable'_fast_boot' command.
|
||||
"""Turn off fast boot.
|
||||
|
||||
Turns off the fast boot feature on the miner.
|
||||
Turn off fast boot, only works after changing the password of
|
||||
the miner using the Whatsminer tool.
|
||||
|
||||
Returns the status of setting the fast boot to off.
|
||||
:return: A reply informing of the status of disabling fast boot.
|
||||
"""
|
||||
command = {"cmd": "disable_btminer_fast_boot"}
|
||||
token_data = await self.get_token()
|
||||
@@ -374,12 +532,12 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def enable_web_pools(self):
|
||||
"""
|
||||
API 'enable_web_pools' command.
|
||||
"""Turn on web pool updates.
|
||||
|
||||
Turns on the ability to change the pools through the web interface.
|
||||
Turn on web pool updates, only works after changing the
|
||||
password of the miner using the Whatsminer tool.
|
||||
|
||||
Returns the status of setting the web pools to enabled.
|
||||
:return: A reply informing of the status of enabling web pools.
|
||||
"""
|
||||
command = {"cmd": "enable_web_pools"}
|
||||
token_data = await self.get_token()
|
||||
@@ -387,12 +545,13 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def disable_web_pools(self):
|
||||
"""
|
||||
API 'disable_web_pools' command.
|
||||
"""Turn off web pool updates.
|
||||
|
||||
Turns off the ability to change the pools through the web interface.
|
||||
Turn off web pool updates, only works after changing the
|
||||
password of the miner using the Whatsminer tool.
|
||||
|
||||
Returns the status of setting the web pools to disabled.
|
||||
:return: A reply informing of the status of disabling web
|
||||
pools.
|
||||
"""
|
||||
command = {"cmd": "disable_web_pools"}
|
||||
token_data = await self.get_token()
|
||||
@@ -400,12 +559,15 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def set_hostname(self, hostname: str):
|
||||
"""
|
||||
API 'set_hostname' command.
|
||||
"""Set the hostname of the miner.
|
||||
|
||||
Sets the hostname of the miner.
|
||||
Set the hostname of the miner, only works after changing the
|
||||
password of the miner using the Whatsminer tool.
|
||||
|
||||
Returns the status of setting the hostname.
|
||||
|
||||
:param hostname: The new hostname to use.
|
||||
:return: A reply informing of the status of setting the
|
||||
hostname.
|
||||
"""
|
||||
command = {"cmd": "set_hostname", "hostname": hostname}
|
||||
token_data = await self.get_token()
|
||||
@@ -413,41 +575,47 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def set_power_pct(self, percent: int):
|
||||
"""
|
||||
API 'set_power_pct' command.
|
||||
"""Set the power percentage of the miner.
|
||||
|
||||
Sets the percent of power the miner should use.
|
||||
Set the power percentage of the miner, only works after changing
|
||||
the password of the miner using the Whatsminer tool.
|
||||
|
||||
Returns the status of setting the power usage to this percent.
|
||||
|
||||
Parameters:
|
||||
percent: the percent to set the power usage to, between 0 and 100.
|
||||
:param percent: The power percentage to set.
|
||||
:return: A reply informing of the status of setting the
|
||||
power percentage.
|
||||
"""
|
||||
|
||||
if not 0 < percent < 100:
|
||||
return APIError(f"Power PCT % is outside of the allowed range. Please set a % between 0 and 100")
|
||||
return APIError(
|
||||
f"Power PCT % is outside of the allowed "
|
||||
f"range. Please set a % between 0 and "
|
||||
f"100"
|
||||
)
|
||||
command = {"cmd": "set_power_pct", "percent": str(percent)}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
|
||||
async def pre_power_on(self, complete: bool, msg: str):
|
||||
"""
|
||||
API 'pre_power_on' command.
|
||||
"""Configure or check status of pre power on.
|
||||
|
||||
Preheats the miner for the 'power_on' command. Can also be used to query the status of pre powering on.
|
||||
Configure or check status of pre power on, only works after
|
||||
changing the password of the miner using the Whatsminer tool.
|
||||
|
||||
Returns status of pre powering on.
|
||||
|
||||
Parameters:
|
||||
complete: check whether or not it is complete.
|
||||
msg: the message to check. "wait for adjust temp" or "adjust complete" or "adjust continue"
|
||||
:param complete: check whether pre power on is complete.
|
||||
:param msg: the message to check.
|
||||
"wait for adjust temp" or
|
||||
"adjust complete" or
|
||||
"adjust continue"
|
||||
:return: A reply informing of the status of pre power on.
|
||||
"""
|
||||
|
||||
if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue":
|
||||
return APIError(
|
||||
'Message is incorrect, please choose one of '
|
||||
'["wait for adjust temp", "adjust complete", "adjust continue"]'
|
||||
"Message is incorrect, please choose one of "
|
||||
'["wait for adjust temp", '
|
||||
'"adjust complete", '
|
||||
'"adjust continue"]'
|
||||
)
|
||||
if complete:
|
||||
complete = "true"
|
||||
@@ -461,88 +629,76 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
#### END privileged COMMANDS ####
|
||||
|
||||
async def summary(self):
|
||||
"""
|
||||
API 'summary' command.
|
||||
"""Get the summary status from the miner.
|
||||
|
||||
Returns a dict containing the status summary of the miner.
|
||||
:return: Summary status of the miner.
|
||||
"""
|
||||
return await self.send_command("summary")
|
||||
|
||||
async def pools(self):
|
||||
"""
|
||||
API 'pools' command.
|
||||
"""Get the pool status from the miner.
|
||||
|
||||
Returns a dict containing the status of each pool.
|
||||
:return: Pool status of the miner.
|
||||
"""
|
||||
return await self.send_command("pools")
|
||||
|
||||
async def devs(self):
|
||||
"""
|
||||
API 'devs' command.
|
||||
"""Get data on each PGA/ASC with their details.
|
||||
|
||||
Returns a dict containing each PGA/ASC with their details.
|
||||
:return: Data on each PGA/ASC with their details.
|
||||
"""
|
||||
return await self.send_command("devs")
|
||||
|
||||
async def edevs(self):
|
||||
"""
|
||||
API 'edevs' command.
|
||||
"""Get data on each PGA/ASC with their details, ignoring
|
||||
blacklisted and zombie devices.
|
||||
|
||||
Returns a dict containing each PGA/ASC with their details,
|
||||
ignoring blacklisted devices and zombie devices.
|
||||
:return: Data on each PGA/ASC with their details.
|
||||
"""
|
||||
return await self.send_command("edevs")
|
||||
|
||||
async def devdetails(self):
|
||||
"""
|
||||
API 'devdetails' command.
|
||||
"""Get data on all devices with their static details.
|
||||
|
||||
Returns a dict containing all devices with their static details.
|
||||
:return: Data on all devices with their static details.
|
||||
"""
|
||||
return await self.send_command("devdetails")
|
||||
|
||||
async def get_psu(self):
|
||||
"""
|
||||
API 'get_psu' command.
|
||||
"""Get data on the PSU and power information.
|
||||
|
||||
Returns a dict containing PSU and power information.
|
||||
:return: Data on the PSU and power information.
|
||||
"""
|
||||
return await self.send_command("get_psu")
|
||||
|
||||
async def version(self):
|
||||
"""
|
||||
API 'get_version' command.
|
||||
"""Get version data for the miner.
|
||||
|
||||
Returns a dict containing version information.
|
||||
Get version data for the miner. This calls another function,
|
||||
self.get_version(), but is named version to stay consistent
|
||||
with the other miner APIs.
|
||||
|
||||
:return: Version data for the miner.
|
||||
"""
|
||||
return await self.get_version()
|
||||
|
||||
async def get_version(self):
|
||||
"""Get version data for the miner.
|
||||
|
||||
:return: Version data for the miner.
|
||||
"""
|
||||
return await self.send_command("get_version")
|
||||
|
||||
async def status(self):
|
||||
"""
|
||||
API 'status' command.
|
||||
"""Get BTMiner status and firmware version.
|
||||
|
||||
Returns a dict containing BTMiner status and firmware version.
|
||||
:return: BTMiner status and firmware version.
|
||||
"""
|
||||
return await self.send_command("status")
|
||||
|
||||
async def get_miner_info(self, info: str | list):
|
||||
"""
|
||||
API 'get_miner_info' command.
|
||||
async def get_miner_info(self):
|
||||
"""Get general miner info.
|
||||
|
||||
Returns a dict containing requested information.
|
||||
|
||||
Parameters:
|
||||
info: the info that you want to get.
|
||||
"ip",
|
||||
"proto",
|
||||
"netmask",
|
||||
"gateway",
|
||||
"dns",
|
||||
"hostname",
|
||||
"mac",
|
||||
"ledstat".
|
||||
:return: General miner info.
|
||||
"""
|
||||
if isinstance(info, str):
|
||||
return await self.send_command("get_miner_info", parameters=info)
|
||||
else:
|
||||
return await self.send_command("get_miner_info", parameters=f"{','.join([str(item) for item in info])}")
|
||||
return await self.send_command("get_miner_info")
|
||||
|
||||
393
API/cgminer.py
393
API/cgminer.py
@@ -2,34 +2,37 @@ from API import BaseMinerAPI
|
||||
|
||||
|
||||
class CGMinerAPI(BaseMinerAPI):
|
||||
"""
|
||||
A class that abstracts the CGMiner API in the miners.
|
||||
"""An abstraction of the CGMiner API.
|
||||
|
||||
Each method corresponds to an API command in CGMiner.
|
||||
Each method corresponds to an API command in GGMiner.
|
||||
|
||||
CGMiner API documentation:
|
||||
https://github.com/ckolivas/cgminer/blob/master/API-README
|
||||
|
||||
Parameters:
|
||||
ip: the IP address of the miner.
|
||||
port (optional): the port of the API on the miner (standard is 4028)
|
||||
This class abstracts use of the CGMiner API, as well as the
|
||||
methods for sending commands to it. The self.send_command()
|
||||
function handles sending a command to the miner asynchronously, and
|
||||
as such is the base for many of the functions in this class, which
|
||||
rely on it to send the command for them.
|
||||
|
||||
:param ip: The IP of the miner to reference the API on.
|
||||
:param port: The port to reference the API on. Default is 4028.
|
||||
"""
|
||||
|
||||
def __init__(self, ip, port=4028):
|
||||
super().__init__(ip, port)
|
||||
|
||||
async def version(self) -> dict:
|
||||
"""
|
||||
API 'version' command.
|
||||
"""Get miner version info.
|
||||
|
||||
Returns a dict containing version information.
|
||||
:return: Miner version information.
|
||||
"""
|
||||
return await self.send_command("version")
|
||||
|
||||
async def config(self) -> dict:
|
||||
"""
|
||||
API 'config' command.
|
||||
"""Get some basic configuration info.
|
||||
|
||||
Returns a dict containing some miner configuration information:
|
||||
:return: Some miner configuration information:
|
||||
ASC Count <- the number of ASCs
|
||||
PGA Count <- the number of PGAs
|
||||
Pool Count <- the number of Pools
|
||||
@@ -41,38 +44,34 @@ class CGMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command("config")
|
||||
|
||||
async def summary(self) -> dict:
|
||||
"""
|
||||
API 'summary' command.
|
||||
"""Get the status summary of the miner.
|
||||
|
||||
Returns a dict containing the status summary of the miner.
|
||||
:return: The status summary of the miner.
|
||||
"""
|
||||
return await self.send_command("summary")
|
||||
|
||||
async def pools(self) -> dict:
|
||||
"""
|
||||
API 'pools' command.
|
||||
"""Get pool information.
|
||||
|
||||
Returns a dict containing the status of each pool.
|
||||
:return: Miner pool information.
|
||||
"""
|
||||
return await self.send_command("pools")
|
||||
|
||||
async def devs(self) -> dict:
|
||||
"""
|
||||
API 'devs' command.
|
||||
"""Get data on each PGA/ASC with their details.
|
||||
|
||||
Returns a dict containing each PGA/ASC with their details.
|
||||
:return: Data on each PGA/ASC with their details.
|
||||
"""
|
||||
return await self.send_command("devs")
|
||||
|
||||
async def edevs(self, old: bool = False) -> dict:
|
||||
"""
|
||||
API 'edevs' command.
|
||||
"""Get data on each PGA/ASC with their details, ignoring
|
||||
blacklisted and zombie devices.
|
||||
|
||||
Returns a dict containing each PGA/ASC with their details,
|
||||
ignoring blacklisted devices and zombie devices.
|
||||
:param old: Include zombie devices that became zombies less
|
||||
than 'old' seconds ago
|
||||
|
||||
Parameters:
|
||||
old (optional): include zombie devices that became zombies less than 'old' seconds ago
|
||||
:return: Data on each PGA/ASC with their details.
|
||||
"""
|
||||
if old:
|
||||
return await self.send_command("edevs", parameters="old")
|
||||
@@ -80,112 +79,96 @@ class CGMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command("edevs")
|
||||
|
||||
async def pga(self, n: int) -> dict:
|
||||
"""
|
||||
API 'pga' command.
|
||||
"""Get data from PGA n.
|
||||
|
||||
Returns a dict containing the details of a single PGA of number N.
|
||||
:param n: The PGA number to get data from.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to get details of.
|
||||
:return: Data on the PGA n.
|
||||
"""
|
||||
return await self.send_command("pga", parameters=n)
|
||||
|
||||
async def pgacount(self) -> dict:
|
||||
"""
|
||||
API 'pgacount' command.
|
||||
"""Get data fon all PGAs.
|
||||
|
||||
Returns a dict containing the number of PGA devices.
|
||||
:return: Data on the PGAs connected.
|
||||
"""
|
||||
return await self.send_command("pgacount")
|
||||
|
||||
async def switchpool(self, n: int) -> dict:
|
||||
"""
|
||||
API 'switchpool' command.
|
||||
"""Switch pools to pool n.
|
||||
|
||||
Returns the STATUS section with the results of switching pools.
|
||||
:param n: The pool to switch to.
|
||||
|
||||
Parameters:
|
||||
n: the number of the pool to switch to.
|
||||
:return: A confirmation of switching to pool n.
|
||||
"""
|
||||
return await self.send_command("switchpool", parameters=n)
|
||||
|
||||
async def enablepool(self, n: int) -> dict:
|
||||
"""
|
||||
API 'enablepool' command.
|
||||
"""Enable pool n.
|
||||
|
||||
Returns the STATUS section with the results of enabling the pool.
|
||||
:param n: The pool to enable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the pool to enable.
|
||||
:return: A confirmation of enabling pool n.
|
||||
"""
|
||||
return await self.send_command("enablepool", parameters=n)
|
||||
|
||||
async def addpool(self, url: str, username: str, password: str) -> dict:
|
||||
"""
|
||||
API 'addpool' command.
|
||||
"""Add a pool to the miner.
|
||||
|
||||
Returns the STATUS section with the results of adding the pool.
|
||||
:param url: The URL of the new pool to add.
|
||||
:param username: The users username on the new pool.
|
||||
:param password: The worker password on the new pool.
|
||||
|
||||
Parameters:
|
||||
url: the URL of the new pool to add.
|
||||
username: the users username on the new pool.
|
||||
password: the worker password on the new pool.
|
||||
:return: A confirmation of adding the pool.
|
||||
"""
|
||||
return await self.send_command("addpool", parameters=f"{url}, {username}, {password}")
|
||||
return await self.send_command(
|
||||
"addpool", parameters=f"{url}, " f"{username}, " f"{password}"
|
||||
)
|
||||
|
||||
async def poolpriority(self, *n: int) -> dict:
|
||||
"""
|
||||
API 'poolpriority' command.
|
||||
"""Set pool priority.
|
||||
|
||||
Returns the STATUS section with the results of setting pool priority.
|
||||
:param n: Pools in order of priority.
|
||||
|
||||
Parameters:
|
||||
n: pool numbers in order of priority.
|
||||
:return: A confirmation of setting pool priority.
|
||||
"""
|
||||
return await self.send_command("poolpriority", parameters=f"{','.join([str(item) for item in n])}")
|
||||
pools = f"{','.join([str(item) for item in n])}"
|
||||
return await self.send_command("poolpriority", parameters=pools)
|
||||
|
||||
async def poolquota(self, n: int, q: int) -> dict:
|
||||
"""
|
||||
API 'poolquota' command.
|
||||
"""Set pool quota.
|
||||
|
||||
Returns the STATUS section with the results of setting pool quota.
|
||||
:param n: Pool number to set quota on.
|
||||
:param q: Quota to set the pool to.
|
||||
|
||||
Parameters:
|
||||
n: pool number to set quota on.
|
||||
q: quota to set the pool to.
|
||||
:return: A confirmation of setting pool quota.
|
||||
"""
|
||||
return await self.send_command("poolquota", parameters=f"{n}, {q}")
|
||||
return await self.send_command("poolquota", parameters=f"{n}, " f"{q}")
|
||||
|
||||
async def disablepool(self, n: int) -> dict:
|
||||
"""
|
||||
API 'disablepool' command.
|
||||
"""Disable a pool.
|
||||
|
||||
Returns the STATUS section with the results of disabling the pool.
|
||||
:param n: Pool to disable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the pool to disable.
|
||||
:return: A confirmation of diabling the pool.
|
||||
"""
|
||||
return await self.send_command("disablepool", parameters=n)
|
||||
|
||||
async def removepool(self, n: int) -> dict:
|
||||
"""
|
||||
API 'removepool' command.
|
||||
"""Remove a pool.
|
||||
|
||||
Returns the STATUS section with the results of removing the pool.
|
||||
:param n: Pool to remove.
|
||||
|
||||
Parameters:
|
||||
n: the number of the pool to remove.
|
||||
:return: A confirmation of removing the pool.
|
||||
"""
|
||||
return await self.send_command("removepool", parameters=n)
|
||||
|
||||
async def save(self, filename: str = None) -> dict:
|
||||
"""
|
||||
API 'save' command.
|
||||
"""Save the config.
|
||||
|
||||
Returns the STATUS section with the results of saving the config file..
|
||||
:param filename: Filename to save the config as.
|
||||
|
||||
Parameters:
|
||||
filename (optional): the filename to save the config as.
|
||||
:return: A confirmation of saving the config.
|
||||
"""
|
||||
if filename:
|
||||
return await self.send_command("save", parameters=filename)
|
||||
@@ -193,146 +176,128 @@ class CGMinerAPI(BaseMinerAPI):
|
||||
return await self.send_command("save")
|
||||
|
||||
async def quit(self) -> dict:
|
||||
"""
|
||||
API 'quit' command.
|
||||
"""Quit CGMiner.
|
||||
|
||||
Returns a single "BYE" before CGMiner quits.
|
||||
:return: A single "BYE" before CGMiner quits.
|
||||
"""
|
||||
return await self.send_command("quit")
|
||||
|
||||
async def notify(self) -> dict:
|
||||
"""
|
||||
API 'notify' command.
|
||||
"""Notify the user of past errors.
|
||||
|
||||
Returns a dict containing the last status and count of each devices problem(s).
|
||||
:return: The last status and count of each devices problem(s).
|
||||
"""
|
||||
return await self.send_command("notify")
|
||||
|
||||
async def privileged(self) -> dict:
|
||||
"""
|
||||
API 'privileged' command.
|
||||
"""Check if you have privileged access.
|
||||
|
||||
Returns the STATUS section with an error if you have no privileged access.
|
||||
:return: The STATUS section with an error if you have no
|
||||
privileged access, or success if you have privileged access.
|
||||
"""
|
||||
return await self.send_command("privileged")
|
||||
|
||||
async def pgaenable(self, n: int) -> dict:
|
||||
"""
|
||||
API 'pgaenable' command.
|
||||
"""Enable PGA n.
|
||||
|
||||
Returns the STATUS section with the results of enabling the PGA device N.
|
||||
:param n: The PGA to enable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to enable.
|
||||
:return: A confirmation of enabling PGA n.
|
||||
"""
|
||||
return await self.send_command("pgaenable", parameters=n)
|
||||
|
||||
async def pgadisable(self, n: int) -> dict:
|
||||
"""
|
||||
API 'pgadisable' command.
|
||||
"""Disable PGA n.
|
||||
|
||||
Returns the STATUS section with the results of disabling the PGA device N.
|
||||
:param n: The PGA to disable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to disable.
|
||||
:return: A confirmation of disabling PGA n.
|
||||
"""
|
||||
return await self.send_command("pgadisable", parameters=n)
|
||||
|
||||
async def pgaidentify(self, n: int) -> dict:
|
||||
"""
|
||||
API 'pgaidentify' command.
|
||||
"""Identify PGA n.
|
||||
|
||||
Returns the STATUS section with the results of identifying the PGA device N.
|
||||
:param n: The PGA to identify.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to identify.
|
||||
:return: A confirmation of identifying PGA n.
|
||||
"""
|
||||
return await self.send_command("pgaidentify", parameters=n)
|
||||
|
||||
async def devdetails(self) -> dict:
|
||||
"""
|
||||
API 'devdetails' command.
|
||||
"""Get data on all devices with their static details.
|
||||
|
||||
Returns a dict containing all devices with their static details.
|
||||
:return: Data on all devices with their static details.
|
||||
"""
|
||||
return await self.send_command("devdetails")
|
||||
|
||||
async def restart(self) -> dict:
|
||||
"""
|
||||
API 'restart' command.
|
||||
"""Restart CGMiner using the API.
|
||||
|
||||
Returns a single "RESTART" before CGMiner restarts.
|
||||
:return: A reply informing of the restart.
|
||||
"""
|
||||
return await self.send_command("restart")
|
||||
|
||||
async def stats(self) -> dict:
|
||||
"""
|
||||
API 'stats' command.
|
||||
"""Get stats of each device/pool with more than 1 getwork.
|
||||
|
||||
Returns a dict containing stats for all device/pool with more than 1 getwork.
|
||||
:return: Stats of each device/pool with more than 1 getwork.
|
||||
"""
|
||||
return await self.send_command("stats")
|
||||
|
||||
async def estats(self, old: bool = False) -> dict:
|
||||
"""
|
||||
API 'estats' command.
|
||||
|
||||
Returns a dict containing stats for all device/pool with more than 1 getwork,
|
||||
"""Get stats of each device/pool with more than 1 getwork,
|
||||
ignoring zombie devices.
|
||||
|
||||
Parameters:
|
||||
old (optional): include zombie devices that became zombies less than 'old' seconds ago.
|
||||
:param old: Include zombie devices that became zombies less
|
||||
than 'old' seconds ago.
|
||||
|
||||
:return: Stats of each device/pool with more than 1 getwork,
|
||||
ignoring zombie devices.
|
||||
"""
|
||||
if old:
|
||||
return await self.send_command("estats", parameters="old")
|
||||
return await self.send_command("estats", parameters=old)
|
||||
else:
|
||||
return await self.send_command("estats")
|
||||
|
||||
async def check(self, command) -> dict:
|
||||
"""
|
||||
API 'check' command.
|
||||
async def check(self, command: str) -> dict:
|
||||
"""Check if the command command exists in CGMiner.
|
||||
|
||||
Returns information about a command:
|
||||
:param command: The command to check.
|
||||
|
||||
:return: Information about a command:
|
||||
Exists (Y/N) <- the command exists in this version
|
||||
Access (Y/N) <- you have access to use the command
|
||||
|
||||
Parameters:
|
||||
command: the command to get information about.
|
||||
"""
|
||||
return await self.send_command("check", parameters=command)
|
||||
|
||||
async def failover_only(self, failover: bool) -> dict:
|
||||
"""
|
||||
API 'failover-only' command.
|
||||
"""Set failover-only.
|
||||
|
||||
Returns the STATUS section with what failover-only was set to.
|
||||
|
||||
Parameters:
|
||||
failover: what to set failover-only to.
|
||||
:param failover: What to set failover-only to.
|
||||
|
||||
:return: Confirmation of setting failover-only.
|
||||
"""
|
||||
return await self.send_command("failover-only", parameters=failover)
|
||||
|
||||
async def coin(self) -> dict:
|
||||
"""
|
||||
API 'coin' command.
|
||||
"""Get information on the current coin.
|
||||
|
||||
Returns information about the current coin being mined:
|
||||
:return: Information about the current coin being mined:
|
||||
Hash Method <- the hashing algorithm
|
||||
Current Block Time <- blocktime as a float, 0 means none
|
||||
Current Block Hash <- the hash of the current block, blank means none
|
||||
Current Block Hash <- the hash of the current block, blank
|
||||
means none
|
||||
LP <- whether LP is in use on at least 1 pool
|
||||
Network Difficulty: the current network difficulty
|
||||
"""
|
||||
return await self.send_command("coin")
|
||||
|
||||
async def debug(self, setting: str) -> dict:
|
||||
"""
|
||||
API 'debug' command.
|
||||
"""Set a debug setting.
|
||||
|
||||
Returns which debug setting was enabled or disabled.
|
||||
|
||||
Parameters:
|
||||
setting: which setting to switch to. Options are:
|
||||
:param setting: Which setting to switch to. Options are:
|
||||
Silent,
|
||||
Quiet,
|
||||
Verbose,
|
||||
@@ -341,42 +306,33 @@ class CGMinerAPI(BaseMinerAPI):
|
||||
PerDevice,
|
||||
WorkTime,
|
||||
Normal.
|
||||
|
||||
:return: Data on which debug setting was enabled or disabled.
|
||||
"""
|
||||
return await self.send_command("debug", parameters=setting)
|
||||
|
||||
async def setconfig(self, name: str, n: int) -> dict:
|
||||
"""
|
||||
API 'setconfig' command.
|
||||
"""Set config of name to value n.
|
||||
|
||||
Returns the STATUS section with the results of setting 'name' to N.
|
||||
|
||||
Parameters:
|
||||
name: name of the config setting to set. Options are:
|
||||
:param name: The name of the config setting to set. Options are:
|
||||
queue,
|
||||
scantime,
|
||||
expiry.
|
||||
n: the value to set the 'name' setting to.
|
||||
:param n: The value to set the 'name' setting to.
|
||||
|
||||
:return: The results of setting config of name to n.
|
||||
"""
|
||||
return await self.send_command("setconfig", parameters=f"{name}, {n}")
|
||||
return await self.send_command("setconfig", parameters=f"{name}, " f"{n}")
|
||||
|
||||
async def usbstats(self) -> dict:
|
||||
"""
|
||||
API 'usbstats' command.
|
||||
"""Get stats of all USB devices except ztex.
|
||||
|
||||
Returns a dict containing the stats of all USB devices except ztex.
|
||||
:return: The stats of all USB devices except ztex.
|
||||
"""
|
||||
return await self.send_command("usbstats")
|
||||
|
||||
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
|
||||
"""
|
||||
API 'pgaset' command.
|
||||
|
||||
Returns the STATUS section with the results of setting PGA N with opt[,val].
|
||||
|
||||
Parameters:
|
||||
n: the PGA to set the options on.
|
||||
opt: the option to set. Setting this to 'help' returns a help message.
|
||||
val: the value to set the option to.
|
||||
"""Set PGA option opt to val on PGA n.
|
||||
|
||||
Options:
|
||||
MMQ -
|
||||
@@ -385,97 +341,92 @@ class CGMinerAPI(BaseMinerAPI):
|
||||
CMR -
|
||||
opt: clock
|
||||
val: 100 - 220
|
||||
|
||||
:param n: The PGA to set the options on.
|
||||
:param opt: The option to set. Setting this to 'help'
|
||||
returns a help message.
|
||||
:param val: The value to set the option to.
|
||||
|
||||
:return: Confirmation of setting PGA n with opt[,val].
|
||||
"""
|
||||
if val:
|
||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}, {val}")
|
||||
return await self.send_command(
|
||||
"pgaset", parameters=f"{n}, " f"{opt}, " f"{val}"
|
||||
)
|
||||
else:
|
||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}")
|
||||
return await self.send_command("pgaset", parameters=f"{n}, " f"{opt}")
|
||||
|
||||
async def zero(self, which: str, summary: bool) -> dict:
|
||||
"""
|
||||
API 'zero' command.
|
||||
"""Zero a device.
|
||||
|
||||
Returns the STATUS section with info on the zero and optional summary.
|
||||
:param which: Which device to zero.
|
||||
Setting this to 'all' zeros all devices.
|
||||
Setting this to 'bestshare' zeros only the bestshare values
|
||||
for each pool and global.
|
||||
:param summary: Whether or not to show a full summary.
|
||||
|
||||
Parameters:
|
||||
which: which device to zero.
|
||||
Setting this to 'all' zeros all devices.
|
||||
Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
|
||||
summary: whether or not to show a full summary.
|
||||
|
||||
:return: the STATUS section with info on the zero and optional
|
||||
summary.
|
||||
"""
|
||||
return await self.send_command("zero", parameters=f"{which}, {summary}")
|
||||
return await self.send_command("zero", parameters=f"{which}, " f"{summary}")
|
||||
|
||||
async def hotplug(self, n: int) -> dict:
|
||||
"""
|
||||
API 'hotplug' command.
|
||||
"""Enable hotplug.
|
||||
|
||||
Returns the STATUS section with whether or not hotplug was enabled.
|
||||
:param n: The device number to set hotplug on.
|
||||
|
||||
:return: Information on hotplug status.
|
||||
"""
|
||||
return await self.send_command("hotplug", parameters=n)
|
||||
|
||||
async def asc(self, n: int) -> dict:
|
||||
"""
|
||||
API 'asc' command.
|
||||
"""Get data for ASC device n.
|
||||
|
||||
Returns a dict containing the details of a single ASC of number N.
|
||||
:param n: The device to get data for.
|
||||
|
||||
n: the ASC device to get details of.
|
||||
:return: The data for ASC device n.
|
||||
"""
|
||||
return await self.send_command("asc", parameters=n)
|
||||
|
||||
async def ascenable(self, n: int) -> dict:
|
||||
"""
|
||||
API 'ascenable' command.
|
||||
"""Enable ASC device n.
|
||||
|
||||
Returns the STATUS section with the results of enabling the ASC device N.
|
||||
:param n: The device to enable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the ASC to enable.
|
||||
:return: Confirmation of enabling ASC device n.
|
||||
"""
|
||||
return await self.send_command("ascenable", parameters=n)
|
||||
|
||||
async def ascdisable(self, n: int) -> dict:
|
||||
"""
|
||||
API 'ascdisable' command.
|
||||
"""Disable ASC device n.
|
||||
|
||||
Returns the STATUS section with the results of disabling the ASC device N.
|
||||
:param n: The device to disable.
|
||||
|
||||
Parameters:
|
||||
n: the number of the ASC to disable.
|
||||
:return: Confirmation of disabling ASC device n.
|
||||
"""
|
||||
return await self.send_command("ascdisable", parameters=n)
|
||||
|
||||
async def ascidentify(self, n: int) -> dict:
|
||||
"""
|
||||
API 'ascidentify' command.
|
||||
"""Identify ASC device n.
|
||||
|
||||
Returns the STATUS section with the results of identifying the ASC device N.
|
||||
:param n: The device to identify.
|
||||
|
||||
Parameters:
|
||||
n: the number of the PGA to identify.
|
||||
:return: Confirmation of identifying ASC device n.
|
||||
"""
|
||||
return await self.send_command("ascidentify", parameters=n)
|
||||
|
||||
async def asccount(self) -> dict:
|
||||
"""
|
||||
API 'asccount' command.
|
||||
"""Get data on the number of ASC devices and their info.
|
||||
|
||||
Returns a dict containing the number of ASC devices.
|
||||
:return: Data on all ASC devices.
|
||||
"""
|
||||
return await self.send_command("asccount")
|
||||
|
||||
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
|
||||
"""
|
||||
API 'ascset' command.
|
||||
"""Set ASC n option opt to value val.
|
||||
|
||||
Returns the STATUS section with the results of setting ASC N with opt[,val].
|
||||
|
||||
Parameters:
|
||||
n: the ASC to set the options on.
|
||||
opt: the option to set. Setting this to 'help' returns a help message.
|
||||
val: the value to set the option to.
|
||||
|
||||
Options:
|
||||
Sets an option on the ASC n to a value. Allowed options are:
|
||||
AVA+BTB -
|
||||
opt: freq
|
||||
val: 256 - 1024 (chip frequency)
|
||||
@@ -509,24 +460,32 @@ class CGMinerAPI(BaseMinerAPI):
|
||||
|
||||
opt: clock
|
||||
val: 0 - 15
|
||||
|
||||
|
||||
:param n: The ASC to set the options on.
|
||||
:param opt: The option to set. Setting this to 'help' returns a
|
||||
help message.
|
||||
:param val: The value to set the option to.
|
||||
|
||||
:return: Confirmation of setting option opt to value val.
|
||||
"""
|
||||
if val:
|
||||
return await self.send_command("ascset", parameters=f"{n}, {opt}, {val}")
|
||||
return await self.send_command(
|
||||
"ascset", parameters=f"{n}, " f"{opt}, " f"{val}"
|
||||
)
|
||||
else:
|
||||
return await self.send_command("ascset", parameters=f"{n}, {opt}")
|
||||
return await self.send_command("ascset", parameters=f"{n}, " f"{opt}")
|
||||
|
||||
async def lcd(self) -> dict:
|
||||
"""
|
||||
API 'lcd' command.
|
||||
"""Get a general all-in-one status summary of the miner.
|
||||
|
||||
Returns a dict containing an all in one status summary of the miner.
|
||||
:return: An all-in-one status summary of the miner.
|
||||
"""
|
||||
return await self.send_command("lcd")
|
||||
|
||||
async def lockstats(self) -> dict:
|
||||
"""
|
||||
API 'lockstats' command.
|
||||
"""Write lockstats to STDERR.
|
||||
|
||||
Returns the STATUS section with the result of writing the lock stats to STDERR.
|
||||
:return: The result of writing the lock stats to STDERR.
|
||||
"""
|
||||
return await self.send_command("lockstats")
|
||||
|
||||
@@ -2,6 +2,13 @@ from API import BaseMinerAPI
|
||||
|
||||
|
||||
class UnknownAPI(BaseMinerAPI):
|
||||
"""An abstraction of an API for a miner which is unknown.
|
||||
|
||||
This class is designed to try to be a intersection of as many miner APIs
|
||||
and API commands as possible (API ⋂ API), to ensure that it can be used
|
||||
with as many APIs as possible.
|
||||
"""
|
||||
|
||||
def __init__(self, ip, port=4028):
|
||||
super().__init__(ip, port)
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# CFG-Util
|
||||
|
||||
## Interact with bitcoin mining ASICs using a simple GUI.
|
||||
|
||||
---
|
||||
## Input Fields
|
||||
### Network IP:
|
||||
* Defaults to 192.168.1.0/24 (192.168.1.0 - 192.168.1.255)
|
||||
* Enter any IP on your local network and it will automatically load your entire network with a /24 subnet (255 IP addresses)
|
||||
* You can also add a subnet mask by adding a / after the IP and entering the subnet mask
|
||||
* Press Scan to scan the selected network for miners
|
||||
|
||||
### IP List File:
|
||||
* Use the Browse button to select a file
|
||||
* Use the Import button to import all IP addresses from a file, regardless of where they are located in the file
|
||||
* Use the Export button to export all IP addresses (or all selected IP addresses if you select some) to a file, with each seperated by a new line
|
||||
|
||||
### Config File:
|
||||
* Use the Browse button to select a file
|
||||
* Use the Import button to import the config file (only toml format is implemented right now)
|
||||
* Use the Export button to export the config file in toml format
|
||||
|
||||
|
||||
---
|
||||
## Data Fields
|
||||
### IP List:
|
||||
* This field contains all the IP addresses of miners that were either imported from a file or scanned
|
||||
* Select one by clicking, mutiple by holding CTRL and clicking, and select all between 2 chosen miners by holding SHIFT as you select them
|
||||
* Use the ALL button to select all IP addresses in the field, or unselect all if they are selected
|
||||
|
||||
### Data:
|
||||
* This field contains all data that is collected by selecting IP addresses and hitting GET
|
||||
* The GET button gets data on all selected IP addresses
|
||||
* The SORT IP button sorts the data list by IP address, as well as the IP List
|
||||
* The SORT HR button sorts the data list by hashrate, as well as the IP List
|
||||
* The SORT USER button sorts the data list by pool username, as well as the IP List
|
||||
* The SORT W button sorts the data list by wattage, as well as the IP List
|
||||
|
||||
### Config:
|
||||
* This field contains the configuration file either imported from a miner or from a file
|
||||
* The IMPORT button imports the configuration file from any 1 selected miner to the config textbox
|
||||
* The CONFIG button configures all selected miners with the config in the config textbox
|
||||
* The LIGHT button turns on the fault light/locator light on miners that support it (Only BraiinsOS for now)
|
||||
* The GENERATE button generates a new basic config in the config textbox
|
||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM python:3.10-slim-buster
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
WORKDIR /minerInterface-web_monitor
|
||||
|
||||
COPY tools/web_monitor/requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["uvicorn", "tools.web_monitor.app:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
198
README.md
198
README.md
@@ -3,6 +3,8 @@
|
||||
## Usage
|
||||
To use this repo, first download it, create a virtual environment, enter the virtual environment, and install relevant packages by navigating to this directory and running ```pip install -r requirements.txt``` on Windows or ```pip3 install -r requirements.txt``` on Mac or UNIX if the first command fails.
|
||||
|
||||
For those of you who aren't comfortable with code and developer tools, there are windows builds of the GUI applications here -> (https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing)
|
||||
|
||||
### CFG Util
|
||||
*CFG Util is a GUI for interfacing with the miners easily, it is mostly self-explanatory.*
|
||||
|
||||
@@ -10,13 +12,13 @@ To use CFG Util you have 2 options -
|
||||
1. Run it directly with the file ```config_tool.py``` or import it with ```from cfg_util import main```, then run the ```main()``` function in an asyncio event loop like -
|
||||
|
||||
```python
|
||||
from cfg_util import main
|
||||
from tools.cfg_util import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
2. Make a build of the CFG Util for your system using cx_freeze and ```make_cfg_tool_exe.py```
|
||||
(Alternatively, you can get a build made by me here -> https://drive.google.com/drive/folders/1nzojuGRu0IszIGpwx7SvG5RlJ2_KXIOv)
|
||||
(Alternatively, you can get a build made by me here -> https://drive.google.com/drive/folders/147vBXbuaX85inataXeSAiKk8IKf-7xtR)
|
||||
1. Open either Command Prompt on Windows or Terminal on Mac or UNIX.
|
||||
2. Navigate to this directory, and run ```make_cfg_tool_exe.py build``` on Windows or ```python3 make_cfg_tool_exe.py``` on Mac or UNIX.
|
||||
|
||||
@@ -46,13 +48,14 @@ A basic script to find all miners on the network and get the hashrate from them
|
||||
```python
|
||||
import asyncio
|
||||
from network import MinerNetwork
|
||||
from cfg_util.func import safe_parse_api_data
|
||||
from tools.cfg_util_old.func.parse_data import safe_parse_api_data
|
||||
|
||||
|
||||
async def get_hashrate():
|
||||
# Miner Network class allows for easy scanning of a network
|
||||
# Give it any IP on a network and it will find the whole subnet
|
||||
# It can also be passed a subnet mask:
|
||||
# miner_network = MinerNetwork('192.168.1.55', mask=23)
|
||||
# miner_network = MinerNetwork('192.168.1.55', mask=23)
|
||||
miner_network = MinerNetwork('192.168.1.1')
|
||||
# Miner Network scan function returns Miner classes for all miners found
|
||||
miners = await miner_network.scan_network_for_miners()
|
||||
@@ -70,6 +73,7 @@ async def get_hashrate():
|
||||
# Print a list of all the hashrates
|
||||
print(data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.new_event_loop().run_until_complete(get_hashrate())
|
||||
```
|
||||
@@ -80,7 +84,8 @@ You can also create your own miner without scanning if you know the IP:
|
||||
import asyncio
|
||||
import ipaddress
|
||||
from miners.miner_factory import MinerFactory
|
||||
from cfg_util.func import safe_parse_api_data
|
||||
from tools.cfg_util_old.func.parse_data import safe_parse_api_data
|
||||
|
||||
|
||||
async def get_miner_hashrate(ip: str):
|
||||
# Instantiate a Miner Factory to generate miners from their IP
|
||||
@@ -96,51 +101,180 @@ async def get_miner_hashrate(ip: str):
|
||||
data = await safe_parse_api_data(summary, 'SUMMARY', 0, 'MHS 5s')
|
||||
print(data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.new_event_loop().run_until_complete(get_miner_hashrate(str("192.168.1.69")))
|
||||
asyncio.new_event_loop().run_until_complete(
|
||||
get_miner_hashrate(str("192.168.1.69")))
|
||||
```
|
||||
|
||||
<br>
|
||||
Or generate a miner directly without the factory:
|
||||
|
||||
Now that you know that, lets move on to some common API functions that you might want to use.
|
||||
|
||||
### Common commands:
|
||||
* Get the data used by the config utility, this includes pool data, wattage use, temperature, hashrate, etc:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from miners.bosminer import BOSminer
|
||||
from cfg_util.func import safe_parse_api_data
|
||||
import ipaddress
|
||||
from miners.miner_factory import MinerFactory
|
||||
|
||||
async def get_miner_hashrate(ip: str):
|
||||
# Create a BOSminer miner object
|
||||
miner = BOSminer(ip)
|
||||
|
||||
async def get_miner_pool_data(ip: str):
|
||||
# Instantiate a Miner Factory to generate miners from their IP
|
||||
miner_factory = MinerFactory()
|
||||
# Make the string IP into an IP address
|
||||
miner_ip = ipaddress.ip_address(ip)
|
||||
# Wait for the factory to return the miner
|
||||
miner = await miner_factory.get_miner(miner_ip)
|
||||
# Get the data
|
||||
data = await miner.get_data()
|
||||
|
||||
print(data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.new_event_loop().run_until_complete(
|
||||
get_miner_pool_data(str("192.168.1.69")))
|
||||
```
|
||||
|
||||
|
||||
|
||||
* Getting pool data:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import ipaddress
|
||||
from miners.miner_factory import MinerFactory
|
||||
|
||||
|
||||
async def get_miner_pool_data(ip: str):
|
||||
# Instantiate a Miner Factory to generate miners from their IP
|
||||
miner_factory = MinerFactory()
|
||||
# Make the string IP into an IP address
|
||||
miner_ip = ipaddress.ip_address(ip)
|
||||
# Wait for the factory to return the miner
|
||||
miner = await miner_factory.get_miner(miner_ip)
|
||||
# Get the API data
|
||||
pools = await miner.api.pools()
|
||||
# safe_parse_api_data parses the data from a miner API
|
||||
# It will raise an APIError (from API import APIError) if there is a problem
|
||||
data = pools["POOLS"]
|
||||
# parse further from here to get all the pool info you want.
|
||||
# each pool is on a different index eg:
|
||||
# data[0] is pool 1
|
||||
# data[1] is pool 2
|
||||
# etc
|
||||
print(data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.new_event_loop().run_until_complete(
|
||||
get_miner_pool_data(str("192.168.1.69")))
|
||||
```
|
||||
|
||||
* Getting temperature data:
|
||||
|
||||
This one is a bit tougher, lots of miners do this a different way, you might need to experiment a bit to find what works for you.
|
||||
BraiinsOS uses the "temps" command, Whatsminers has it in "devs", Avalonminers put it in "stats" as well as some other miners,
|
||||
but the spot I like to try first is in "summary".
|
||||
|
||||
A pretty good example of really trying to make this robust is in ```cfg_util.func.miners``` in the ```get_formatted_data()``` function.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import ipaddress
|
||||
from miners.miner_factory import MinerFactory
|
||||
|
||||
|
||||
async def get_miner_temperature_data(ip: str):
|
||||
# Instantiate a Miner Factory to generate miners from their IP
|
||||
miner_factory = MinerFactory()
|
||||
# Make the string IP into an IP address
|
||||
miner_ip = ipaddress.ip_address(ip)
|
||||
# Wait for the factory to return the miner
|
||||
miner = await miner_factory.get_miner(miner_ip)
|
||||
# Get the API data
|
||||
summary = await miner.api.summary()
|
||||
# safe_parse_api_data parses the data from a miner API
|
||||
# It will raise an APIError (from API import APIError) if there is a problem
|
||||
data = await safe_parse_api_data(summary, 'SUMMARY', 0, 'MHS 5s')
|
||||
|
||||
data = summary['SUMMARY'][0]["Temperature"]
|
||||
print(data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.new_event_loop().run_until_complete(get_miner_hashrate(str("192.168.1.69")))
|
||||
asyncio.new_event_loop().run_until_complete(
|
||||
get_miner_temperature_data(str("192.168.1.69")))
|
||||
```
|
||||
|
||||
<br>
|
||||
Or finally, just get the API directly:
|
||||
* Getting power data:
|
||||
|
||||
How about data on the power usage of the miner? This one only works for Whatsminers and BraiinsOS for now, and the Braiins one just uses the tuning setting, but its good enough for basic uses.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from API.bosminer import BOSMinerAPI
|
||||
from cfg_util.func import safe_parse_api_data
|
||||
import ipaddress
|
||||
from miners.miner_factory import MinerFactory
|
||||
|
||||
|
||||
async def get_miner_power_data(ip: str):
|
||||
data = None
|
||||
# Instantiate a Miner Factory to generate miners from their IP
|
||||
miner_factory = MinerFactory()
|
||||
# Make the string IP into an IP address
|
||||
miner_ip = ipaddress.ip_address(ip)
|
||||
# Wait for the factory to return the miner
|
||||
miner = await miner_factory.get_miner(miner_ip)
|
||||
# check if this can be sent the "tunerstatus" command, BraiinsOS only
|
||||
if "tunerstatus" in miner.api.get_commands():
|
||||
# send the command
|
||||
tunerstatus = await miner.api.tunerstatus()
|
||||
# parse the return
|
||||
data = tunerstatus['TUNERSTATUS'][0]["PowerLimit"]
|
||||
else:
|
||||
# send the command
|
||||
# whatsminers have the power info in summary
|
||||
summary = await miner.api.summary()
|
||||
# parse the return
|
||||
data = summary['SUMMARY'][0]["Power"]
|
||||
|
||||
if data:
|
||||
print(data)
|
||||
|
||||
async def get_miner_hashrate(ip: str):
|
||||
# Create a BOSminerAPI object
|
||||
# Port can be declared manually, if not it defaults to 4028
|
||||
api = BOSMinerAPI(ip, port=4028)
|
||||
# Get the API data
|
||||
summary = await api.summary()
|
||||
# safe_parse_api_data parses the data from a miner API
|
||||
# It will raise an APIError (from API import APIError) if there is a problem
|
||||
data = await safe_parse_api_data(summary, 'SUMMARY', 0, 'MHS 5s')
|
||||
print(data)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.new_event_loop().run_until_complete(get_miner_hashrate(str("192.168.1.69")))
|
||||
asyncio.new_event_loop().run_until_complete(
|
||||
get_miner_power_data(str("192.168.1.69")))
|
||||
```
|
||||
|
||||
* Multicommands:
|
||||
|
||||
Multicommands make it much easier to get many types of data all at once. The multicommand function will also remove any commands that your API can't handle automatically.
|
||||
How about we get the current pool user and hashrate in 1 command?
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import ipaddress
|
||||
from miners.miner_factory import MinerFactory
|
||||
from tools.cfg_util_old.func.parse_data import safe_parse_api_data
|
||||
|
||||
|
||||
async def get_miner_hashrate_and_pool(ip: str):
|
||||
# Instantiate a Miner Factory to generate miners from their IP
|
||||
miner_factory = MinerFactory()
|
||||
# Make the string IP into an IP address
|
||||
miner_ip = ipaddress.ip_address(ip)
|
||||
# Wait for the factory to return the miner
|
||||
miner = await miner_factory.get_miner(miner_ip)
|
||||
# Get the API data
|
||||
api_data = await miner.api.multicommand("pools", "summary")
|
||||
if "pools" in api_data.keys():
|
||||
user = api_data["pools"][0]["POOLS"][0]["User"]
|
||||
print(user)
|
||||
if "summary" in api_data.keys():
|
||||
hashrate = api_data["summary"][0]["SUMMARY"][0]["MHS av"]
|
||||
print(hashrate)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.new_event_loop().run_until_complete(
|
||||
get_miner_hashrate_and_pool(str("192.168.1.9")))
|
||||
```
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
from cfg_util.miner_factory import miner_factory
|
||||
from cfg_util.layout import window
|
||||
from cfg_util.ui import ui
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
# Fix bug with some whatsminers and asyncio because of a socket not being shut down:
|
||||
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
|
||||
def main():
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(ui())
|
||||
@@ -1,324 +0,0 @@
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import aiofiles
|
||||
import toml
|
||||
|
||||
from API import APIError
|
||||
from cfg_util.func.data import safe_parse_api_data
|
||||
from cfg_util.layout import window
|
||||
from cfg_util.miner_factory import miner_factory
|
||||
from config.bos import bos_config_convert, general_config_convert_bos
|
||||
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
|
||||
|
||||
|
||||
async def update_ui_with_data(key, message, append=False):
|
||||
if append:
|
||||
message = window[key].get_text() + message
|
||||
window[key].update(message)
|
||||
|
||||
|
||||
async def update_prog_bar(amount):
|
||||
window["progress"].Update(amount)
|
||||
percent_done = 100 * (amount / window['progress'].maxlen)
|
||||
window["progress_percent"].Update(f"{round(percent_done, 2)} %")
|
||||
if percent_done == 100:
|
||||
window["progress_percent"].Update("")
|
||||
|
||||
|
||||
async def set_progress_bar_len(amount):
|
||||
window["progress"].Update(0, max=amount)
|
||||
window["progress"].maxlen = amount
|
||||
window["progress_percent"].Update("0.0 %")
|
||||
|
||||
|
||||
async def scan_network(network):
|
||||
await update_ui_with_data("status", "Scanning")
|
||||
await update_ui_with_data("hr_total", "")
|
||||
network_size = len(network)
|
||||
miner_generator = network.scan_network_generator()
|
||||
await set_progress_bar_len(2 * network_size)
|
||||
progress_bar_len = 0
|
||||
miners = []
|
||||
async for miner in miner_generator:
|
||||
if miner:
|
||||
miners.append(miner)
|
||||
# can output "Identifying" for each found item, but it gets a bit cluttered
|
||||
# and could possibly be confusing for the end user because of timing on
|
||||
# adding the IPs
|
||||
# window["ip_table"].update([["Identifying...", "", "", "", ""] for miner in miners])
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
progress_bar_len += network_size - len(miners)
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
get_miner_genenerator = miner_factory.get_miner_generator(miners)
|
||||
all_miners = []
|
||||
async for found_miner in get_miner_genenerator:
|
||||
all_miners.append(found_miner)
|
||||
all_miners.sort(key=lambda x: x.ip)
|
||||
window["ip_table"].update([[str(miner.ip), "", "", "", ""] for miner in all_miners])
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
await update_ui_with_data("ip_count", str(len(all_miners)))
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def miner_light(ips: list):
|
||||
await asyncio.gather(*[flip_light(ip) for ip in ips])
|
||||
|
||||
|
||||
async def flip_light(ip):
|
||||
ip_list = window['ip_table'].Widget
|
||||
miner = await miner_factory.get_miner(ip)
|
||||
index = [item[0] for item in window["ip_table"].Values].index(ip)
|
||||
index_tags = ip_list.item(index)['tags']
|
||||
if "light" not in index_tags:
|
||||
ip_list.item(index, tags=([*index_tags, "light"]))
|
||||
window['ip_table'].update(row_colors=[(index, "white", "red")])
|
||||
await miner.fault_light_on()
|
||||
else:
|
||||
index_tags.remove("light")
|
||||
ip_list.item(index, tags=index_tags)
|
||||
window['ip_table'].update(row_colors=[(index, "black", "white")])
|
||||
await miner.fault_light_off()
|
||||
|
||||
|
||||
async def import_config(idx):
|
||||
await update_ui_with_data("status", "Importing")
|
||||
miner = await miner_factory.get_miner(ipaddress.ip_address(window["ip_table"].Values[idx[0]][0]))
|
||||
await miner.get_config()
|
||||
config = miner.config
|
||||
await update_ui_with_data("config", str(config))
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def import_iplist(file_location):
|
||||
await update_ui_with_data("status", "Importing")
|
||||
if not os.path.exists(file_location):
|
||||
return
|
||||
else:
|
||||
ip_list = []
|
||||
async with aiofiles.open(file_location, mode='r') as file:
|
||||
async for line in file:
|
||||
ips = [x.group() for x in re.finditer(
|
||||
"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", line)]
|
||||
for ip in ips:
|
||||
if ip not in ip_list:
|
||||
ip_list.append(ipaddress.ip_address(ip))
|
||||
ip_list.sort()
|
||||
window["ip_table"].update([[str(ip), "", "", "", ""] for ip in ip_list])
|
||||
await update_ui_with_data("ip_count", str(len(ip_list)))
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def export_iplist(file_location, ip_list_selected):
|
||||
await update_ui_with_data("status", "Exporting")
|
||||
if not os.path.exists(file_location):
|
||||
return
|
||||
else:
|
||||
if ip_list_selected is not None and not ip_list_selected == []:
|
||||
async with aiofiles.open(file_location, mode='w') as file:
|
||||
for item in ip_list_selected:
|
||||
await file.write(str(item) + "\n")
|
||||
else:
|
||||
async with aiofiles.open(file_location, mode='w') as file:
|
||||
for item in window['ip_table'].Values:
|
||||
await file.write(str(item[0]) + "\n")
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def send_config_generator(miners: list, config):
|
||||
loop = asyncio.get_event_loop()
|
||||
config_tasks = []
|
||||
for miner in miners:
|
||||
if len(config_tasks) >= CONFIG_THREADS:
|
||||
configured = asyncio.as_completed(config_tasks)
|
||||
config_tasks = []
|
||||
for sent_config in configured:
|
||||
yield await sent_config
|
||||
config_tasks.append(loop.create_task(miner.send_config(config)))
|
||||
configured = asyncio.as_completed(config_tasks)
|
||||
for sent_config in configured:
|
||||
yield await sent_config
|
||||
|
||||
|
||||
async def send_config(ips: list, config):
|
||||
await update_ui_with_data("status", "Configuring")
|
||||
await set_progress_bar_len(2 * len(ips))
|
||||
progress_bar_len = 0
|
||||
get_miner_genenerator = miner_factory.get_miner_generator(ips)
|
||||
all_miners = []
|
||||
async for miner in get_miner_genenerator:
|
||||
all_miners.append(miner)
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
|
||||
config_sender_generator = send_config_generator(all_miners, config)
|
||||
async for _config_sender in config_sender_generator:
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def import_config_file(file_location):
|
||||
await update_ui_with_data("status", "Importing")
|
||||
if not os.path.exists(file_location):
|
||||
return
|
||||
else:
|
||||
async with aiofiles.open(file_location, mode='r') as file:
|
||||
config = await file.read()
|
||||
await update_ui_with_data("config", await bos_config_convert(toml.loads(config)))
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def export_config_file(file_location, config):
|
||||
await update_ui_with_data("status", "Exporting")
|
||||
config = toml.loads(config)
|
||||
config['format']['generator'] = 'upstream_config_util'
|
||||
config['format']['timestamp'] = int(time.time())
|
||||
config = toml.dumps(config)
|
||||
async with aiofiles.open(file_location, mode='w+') as file:
|
||||
await file.write(await general_config_convert_bos(config))
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def get_data(ip_list: list):
|
||||
await update_ui_with_data("status", "Getting Data")
|
||||
ips = [ipaddress.ip_address(ip) for ip in ip_list]
|
||||
if len(ips) == 0:
|
||||
ips = [ipaddress.ip_address(ip) for ip in [item[0] for item in window["ip_table"].Values]]
|
||||
await set_progress_bar_len(len(ips))
|
||||
progress_bar_len = 0
|
||||
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in ips])
|
||||
ip_table_data = window["ip_table"].Values
|
||||
ordered_all_ips = [item[0] for item in ip_table_data]
|
||||
for all_data in data_gen:
|
||||
data_point = await all_data
|
||||
if data_point["IP"] in ordered_all_ips:
|
||||
ip_table_index = ordered_all_ips.index(data_point["IP"])
|
||||
ip_table_data[ip_table_index] = [
|
||||
data_point["IP"], data_point["host"], str(data_point['TH/s']) + " TH/s", data_point['user'], str(data_point['wattage']) + " W"
|
||||
]
|
||||
window["ip_table"].update(ip_table_data)
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
|
||||
hashrate_list = [float(item[2].replace(" TH/s", "")) for item in window["ip_table"].Values]
|
||||
total_hr = round(sum(hashrate_list), 2)
|
||||
window["hr_total"].update(f"{total_hr} TH/s")
|
||||
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def get_formatted_data(ip: ipaddress.ip_address):
|
||||
miner = await miner_factory.get_miner(ip)
|
||||
try:
|
||||
miner_data = await miner.api.multicommand("summary", "pools", "tunerstatus")
|
||||
except APIError:
|
||||
return {'TH/s': "Unknown", 'IP': str(miner.ip), 'host': "Unknown", 'user': "Unknown", 'wattage': 0}
|
||||
host = await miner.get_hostname()
|
||||
if "tunerstatus" in miner_data.keys():
|
||||
wattage = await safe_parse_api_data(miner_data, "tunerstatus", 0, 'TUNERSTATUS', 0, "PowerLimit")
|
||||
# data['tunerstatus'][0]['TUNERSTATUS'][0]['PowerLimit']
|
||||
else:
|
||||
wattage = 0
|
||||
if "summary" in miner_data.keys():
|
||||
if 'MHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
||||
th5s = round(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'MHS 5s') / 1000000, 2)
|
||||
elif 'GHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
||||
if not miner_data['summary'][0]['SUMMARY'][0]['GHS 5s'] == "":
|
||||
th5s = round(float(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'GHS 5s')) / 1000,
|
||||
2)
|
||||
else:
|
||||
th5s = 0
|
||||
else:
|
||||
th5s = 0
|
||||
else:
|
||||
th5s = 0
|
||||
if "pools" not in miner_data.keys():
|
||||
user = "?"
|
||||
elif not miner_data['pools'][0]['POOLS'] == []:
|
||||
user = await safe_parse_api_data(miner_data, 'pools', 0, 'POOLS', 0, 'User')
|
||||
else:
|
||||
user = "Blank"
|
||||
return {'TH/s': th5s, 'IP': str(miner.ip), 'host': host, 'user': user, 'wattage': wattage}
|
||||
|
||||
|
||||
async def generate_config(username, workername, v2_allowed):
|
||||
if username and workername:
|
||||
user = f"{username}.{workername}"
|
||||
elif username and not workername:
|
||||
user = username
|
||||
else:
|
||||
return
|
||||
|
||||
if v2_allowed:
|
||||
url_1 = 'stratum2+tcp://v2.us-east.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt'
|
||||
url_2 = 'stratum2+tcp://v2.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt'
|
||||
url_3 = 'stratum+tcp://stratum.slushpool.com:3333'
|
||||
else:
|
||||
url_1 = 'stratum+tcp://ca.stratum.slushpool.com:3333'
|
||||
url_2 = 'stratum+tcp://us-east.stratum.slushpool.com:3333'
|
||||
url_3 = 'stratum+tcp://stratum.slushpool.com:3333'
|
||||
|
||||
config = {'group': [{
|
||||
'name': 'group',
|
||||
'quota': 1,
|
||||
'pool': [{
|
||||
'url': url_1,
|
||||
'user': user,
|
||||
'password': '123'
|
||||
}, {
|
||||
'url': url_2,
|
||||
'user': user,
|
||||
'password': '123'
|
||||
}, {
|
||||
'url': url_3,
|
||||
'user': user,
|
||||
'password': '123'
|
||||
}]
|
||||
}],
|
||||
'format': {
|
||||
'version': '1.2+',
|
||||
'model': 'Antminer S9',
|
||||
'generator': 'upstream_config_util',
|
||||
'timestamp': int(time.time())
|
||||
},
|
||||
'temp_control': {
|
||||
'target_temp': 80.0,
|
||||
'hot_temp': 90.0,
|
||||
'dangerous_temp': 120.0
|
||||
},
|
||||
'autotuning': {
|
||||
'enabled': True,
|
||||
'psu_power_limit': 900
|
||||
}
|
||||
}
|
||||
window['config'].update(await bos_config_convert(config))
|
||||
|
||||
|
||||
async def sort_data(index: int or str):
|
||||
await update_ui_with_data("status", "Sorting Data")
|
||||
data_list = window['ip_table'].Values
|
||||
|
||||
# wattage
|
||||
if re.match("[0-9]* W", data_list[0][index]):
|
||||
new_list = sorted(data_list, key=lambda x: int(x[index].replace(" W", "")))
|
||||
|
||||
# hashrate
|
||||
elif re.match("[0-9]*\.?[0-9]* TH\/s", data_list[0][index]):
|
||||
new_list = sorted(data_list, key=lambda x: float(x[index].replace(" TH/s", "")))
|
||||
|
||||
# ip addresses
|
||||
elif re.match("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",
|
||||
data_list[0][index]):
|
||||
new_list = sorted(data_list, key=lambda x: ipaddress.ip_address(x[index]))
|
||||
|
||||
# everything else, hostname and user
|
||||
else:
|
||||
new_list = sorted(data_list, key=lambda x: x[index])
|
||||
await update_ui_with_data("ip_table", new_list)
|
||||
await update_ui_with_data("status", "")
|
||||
@@ -1,47 +0,0 @@
|
||||
from API import APIError
|
||||
|
||||
|
||||
async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int = 0):
|
||||
path = [*path]
|
||||
if len(path) == idx+1:
|
||||
if isinstance(path[idx], str):
|
||||
if isinstance(data, dict):
|
||||
if path[idx] in data.keys():
|
||||
return data[path[idx]]
|
||||
elif isinstance(path[idx], int):
|
||||
if isinstance(data, list):
|
||||
if len(data) > path[idx]:
|
||||
return data[path[idx]]
|
||||
else:
|
||||
if isinstance(path[idx], str):
|
||||
if isinstance(data, dict):
|
||||
if path[idx] in data.keys():
|
||||
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
|
||||
# has to be == None, or else it fails on 0.0 hashrates
|
||||
if parsed_data == None:
|
||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||
return parsed_data
|
||||
else:
|
||||
if idx == 0:
|
||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||
return False
|
||||
else:
|
||||
if idx == 0:
|
||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||
return False
|
||||
elif isinstance(path[idx], int):
|
||||
if isinstance(data, list):
|
||||
if len(data) > path[idx]:
|
||||
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
|
||||
# has to be == None, or else it fails on 0.0 hashrates
|
||||
if parsed_data == None:
|
||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||
return parsed_data
|
||||
else:
|
||||
if idx == 0:
|
||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||
return False
|
||||
else:
|
||||
if idx == 0:
|
||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||
return False
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
This file stores the MinerFactory instance used by the ConfigUtility for use in other files.
|
||||
"""
|
||||
|
||||
from miners.miner_factory import MinerFactory
|
||||
|
||||
miner_factory = MinerFactory()
|
||||
@@ -1,72 +0,0 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import PySimpleGUI as sg
|
||||
|
||||
from cfg_util.layout import window, generate_config_layout
|
||||
from cfg_util.func import scan_network, sort_data, send_config, miner_light, get_data, export_config_file, \
|
||||
generate_config, import_config, import_iplist, import_config_file, export_iplist
|
||||
|
||||
from network import MinerNetwork
|
||||
|
||||
|
||||
async def ui():
|
||||
while True:
|
||||
event, value = window.read(timeout=10)
|
||||
if event in (None, 'Close', sg.WIN_CLOSED):
|
||||
sys.exit()
|
||||
if event == 'scan':
|
||||
if len(value['miner_network'].split("/")) > 1:
|
||||
network = value['miner_network'].split("/")
|
||||
miner_network = MinerNetwork(ip_addr=network[0], mask=network[1])
|
||||
else:
|
||||
miner_network = MinerNetwork(value['miner_network'])
|
||||
asyncio.create_task(scan_network(miner_network))
|
||||
if event == 'select_all_ips':
|
||||
if len(value["ip_table"]) == len(window["ip_table"].Values):
|
||||
window["ip_table"].update(select_rows=())
|
||||
else:
|
||||
window["ip_table"].update(select_rows=([row for row in range(len(window["ip_table"].Values))]))
|
||||
if event == 'import_config':
|
||||
if 2 > len(value['ip_table']) > 0:
|
||||
asyncio.create_task(import_config(value['ip_table']))
|
||||
if event == 'light':
|
||||
asyncio.create_task(miner_light([window['ip_table'].Values[item][0] for item in value['ip_table']]))
|
||||
if event == "import_iplist":
|
||||
asyncio.create_task(import_iplist(value["file_iplist"]))
|
||||
if event == "export_iplist":
|
||||
asyncio.create_task(export_iplist(value["file_iplist"], [window['ip_table'].Values[item][0] for item in value['ip_table']]))
|
||||
if event == "send_config":
|
||||
asyncio.create_task(send_config([window['ip_table'].Values[item][0] for item in value['ip_table']], value['config']))
|
||||
if event == "import_file_config":
|
||||
asyncio.create_task(import_config_file(value['file_config']))
|
||||
if event == "export_file_config":
|
||||
asyncio.create_task(export_config_file(value['file_config'], value["config"]))
|
||||
if event == "get_data":
|
||||
asyncio.create_task(get_data([window["ip_table"].Values[item][0] for item in value["ip_table"]]))
|
||||
if event == "generate_config":
|
||||
await generate_config_ui()
|
||||
if event == "sort_data_ip":
|
||||
asyncio.create_task(sort_data(0)) # ip index in table
|
||||
if event == "sort_data_hr":
|
||||
asyncio.create_task(sort_data(2)) # HR index in table
|
||||
if event == "sort_data_user":
|
||||
asyncio.create_task(sort_data(3)) # user index in table
|
||||
if event == "sort_data_w":
|
||||
asyncio.create_task(sort_data(4)) # wattage index in table
|
||||
if event == "__TIMEOUT__":
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
async def generate_config_ui():
|
||||
generate_config_window = sg.Window("Generate Config", generate_config_layout(), modal=True)
|
||||
while True:
|
||||
event, values = generate_config_window.read()
|
||||
if event in (None, 'Close', sg.WIN_CLOSED):
|
||||
break
|
||||
if event == "generate_config_window_generate":
|
||||
if values['generate_config_window_username']:
|
||||
await generate_config(values['generate_config_window_username'],
|
||||
values['generate_config_window_workername'],
|
||||
values['generate_config_window_allow_v2'])
|
||||
generate_config_window.close()
|
||||
break
|
||||
@@ -75,4 +75,4 @@ SAMPLE CONFIG
|
||||
"shutdown_duration": 3.0, # -> (default = 3.0, float, (bos: power_scaling.shutdown_duration))
|
||||
}
|
||||
}
|
||||
"""
|
||||
"""
|
||||
|
||||
@@ -3,12 +3,12 @@ import yaml
|
||||
import toml
|
||||
|
||||
|
||||
async def bos_config_convert(config: dict):
|
||||
def bos_config_convert(config: dict):
|
||||
out_config = {}
|
||||
for opt in config:
|
||||
if opt == "format":
|
||||
out_config["format"] = config[opt]
|
||||
out_config["format"]["generator"] = 'upstream_config_util'
|
||||
out_config["format"]["generator"] = "upstream_config_util"
|
||||
out_config["format"]["timestamp"] = int(time.time())
|
||||
elif opt == "temp_control":
|
||||
out_config["temperature"] = {}
|
||||
@@ -47,20 +47,28 @@ async def bos_config_convert(config: dict):
|
||||
out_config["pool_groups"][idx]["pools"] = []
|
||||
out_config["pool_groups"][idx] = {}
|
||||
if "name" in config[opt][idx].keys():
|
||||
out_config["pool_groups"][idx]["group_name"] = config[opt][idx]["name"]
|
||||
out_config["pool_groups"][idx]["group_name"] = config[opt][idx][
|
||||
"name"
|
||||
]
|
||||
else:
|
||||
out_config["pool_groups"][idx]["group_name"] = f"group_{idx}"
|
||||
if "quota" in config[opt][idx].keys():
|
||||
out_config["pool_groups"][idx]["quota"] = config[opt][idx]["quota"]
|
||||
else:
|
||||
out_config["pool_groups"][idx]["quota"] = 1
|
||||
out_config["pool_groups"][idx]["pools"] = [{} for _item in range(len(config[opt][idx]["pool"]))]
|
||||
out_config["pool_groups"][idx]["pools"] = [
|
||||
{} for _item in range(len(config[opt][idx]["pool"]))
|
||||
]
|
||||
for pool_idx in range(len(config[opt][idx]["pool"])):
|
||||
out_config["pool_groups"][idx]["pools"][pool_idx]["url"] = config[opt][idx]["pool"][pool_idx]["url"]
|
||||
out_config["pool_groups"][idx]["pools"][pool_idx]["username"] = config[opt][idx]["pool"][pool_idx][
|
||||
"user"]
|
||||
out_config["pool_groups"][idx]["pools"][pool_idx]["password"] = config[opt][idx]["pool"][pool_idx][
|
||||
"password"]
|
||||
out_config["pool_groups"][idx]["pools"][pool_idx]["url"] = config[
|
||||
opt
|
||||
][idx]["pool"][pool_idx]["url"]
|
||||
out_config["pool_groups"][idx]["pools"][pool_idx][
|
||||
"username"
|
||||
] = config[opt][idx]["pool"][pool_idx]["user"]
|
||||
out_config["pool_groups"][idx]["pools"][pool_idx][
|
||||
"password"
|
||||
] = config[opt][idx]["pool"][pool_idx]["password"]
|
||||
elif opt == "autotuning":
|
||||
out_config["autotuning"] = {}
|
||||
if "enabled" in config[opt].keys():
|
||||
@@ -82,27 +90,33 @@ async def bos_config_convert(config: dict):
|
||||
else:
|
||||
out_config["power_scaling"]["power_step"] = 100
|
||||
if "min_psu_power_limit" in config[opt].keys():
|
||||
out_config["power_scaling"]["min_psu_power_limit"] = config[opt]["min_psu_power_limit"]
|
||||
out_config["power_scaling"]["min_psu_power_limit"] = config[opt][
|
||||
"min_psu_power_limit"
|
||||
]
|
||||
else:
|
||||
out_config["power_scaling"]["min_psu_power_limit"] = 800
|
||||
if "shutdown_enabled" in config[opt].keys():
|
||||
out_config["power_scaling"]["shutdown_enabled"] = config[opt]["shutdown_enabled"]
|
||||
out_config["power_scaling"]["shutdown_enabled"] = config[opt][
|
||||
"shutdown_enabled"
|
||||
]
|
||||
else:
|
||||
out_config["power_scaling"]["shutdown_enabled"] = False
|
||||
if "shutdown_duration" in config[opt].keys():
|
||||
out_config["power_scaling"]["shutdown_duration"] = config[opt]["shutdown_duration"]
|
||||
out_config["power_scaling"]["shutdown_duration"] = config[opt][
|
||||
"shutdown_duration"
|
||||
]
|
||||
else:
|
||||
out_config["power_scaling"]["shutdown_duration"] = 3.0
|
||||
return yaml.dump(out_config, sort_keys=False)
|
||||
|
||||
|
||||
async def general_config_convert_bos(yaml_config):
|
||||
def general_config_convert_bos(yaml_config, user_suffix: str = None):
|
||||
config = yaml.load(yaml_config, Loader=yaml.SafeLoader)
|
||||
out_config = {}
|
||||
for opt in config:
|
||||
if opt == "format":
|
||||
out_config["format"] = config[opt]
|
||||
out_config["format"]["generator"] = 'upstream_config_util'
|
||||
out_config["format"]["generator"] = "upstream_config_util"
|
||||
out_config["format"]["timestamp"] = int(time.time())
|
||||
elif opt == "temperature":
|
||||
out_config["temp_control"] = {}
|
||||
@@ -148,11 +162,24 @@ async def general_config_convert_bos(yaml_config):
|
||||
out_config["group"][idx]["quota"] = config[opt][idx]["quota"]
|
||||
else:
|
||||
out_config["group"][idx]["quota"] = 1
|
||||
out_config["group"][idx]["pool"] = [{} for _item in range(len(config[opt][idx]["pools"]))]
|
||||
out_config["group"][idx]["pool"] = [
|
||||
{} for _item in range(len(config[opt][idx]["pools"]))
|
||||
]
|
||||
for pool_idx in range(len(config[opt][idx]["pools"])):
|
||||
out_config["group"][idx]["pool"][pool_idx]["url"] = config[opt][idx]["pools"][pool_idx]["url"]
|
||||
out_config["group"][idx]["pool"][pool_idx]["user"] = config[opt][idx]["pools"][pool_idx]["username"]
|
||||
out_config["group"][idx]["pool"][pool_idx]["password"] = config[opt][idx]["pools"][pool_idx]["password"]
|
||||
out_config["group"][idx]["pool"][pool_idx]["url"] = config[opt][
|
||||
idx
|
||||
]["pools"][pool_idx]["url"]
|
||||
username = config[opt][idx]["pools"][pool_idx]["username"]
|
||||
if user_suffix:
|
||||
if "." in username:
|
||||
username = f"{username}x{user_suffix}"
|
||||
else:
|
||||
username = f"{username}.{user_suffix}"
|
||||
out_config["group"][idx]["pool"][pool_idx]["user"] = username
|
||||
|
||||
out_config["group"][idx]["pool"][pool_idx]["password"] = config[
|
||||
opt
|
||||
][idx]["pools"][pool_idx]["password"]
|
||||
elif opt == "autotuning":
|
||||
out_config["autotuning"] = {}
|
||||
if "enabled" in config[opt].keys():
|
||||
@@ -174,15 +201,21 @@ async def general_config_convert_bos(yaml_config):
|
||||
else:
|
||||
out_config["power_scaling"]["power_step"] = 100
|
||||
if "min_psu_power_limit" in config[opt].keys():
|
||||
out_config["power_scaling"]["min_psu_power_limit"] = config[opt]["min_psu_power_limit"]
|
||||
out_config["power_scaling"]["min_psu_power_limit"] = config[opt][
|
||||
"min_psu_power_limit"
|
||||
]
|
||||
else:
|
||||
out_config["power_scaling"]["min_psu_power_limit"] = 800
|
||||
if "shutdown_enabled" in config[opt].keys():
|
||||
out_config["power_scaling"]["shutdown_enabled"] = config[opt]["shutdown_enabled"]
|
||||
out_config["power_scaling"]["shutdown_enabled"] = config[opt][
|
||||
"shutdown_enabled"
|
||||
]
|
||||
else:
|
||||
out_config["power_scaling"]["shutdown_enabled"] = False
|
||||
if "shutdown_duration" in config[opt].keys():
|
||||
out_config["power_scaling"]["shutdown_duration"] = config[opt]["shutdown_duration"]
|
||||
out_config["power_scaling"]["shutdown_duration"] = config[opt][
|
||||
"shutdown_duration"
|
||||
]
|
||||
else:
|
||||
out_config["power_scaling"]["shutdown_duration"] = 3.0
|
||||
return toml.dumps(out_config)
|
||||
return out_config
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from cfg_util import main
|
||||
from tools.cfg_util import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
18
logger/__init__.py
Normal file
18
logger/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import logging
|
||||
from settings import DEBUG
|
||||
|
||||
|
||||
logging.basicConfig(
|
||||
# filename="logfile.txt",
|
||||
# filemode="a",
|
||||
format="[%(levelname)s][%(asctime)s](%(name)s) - %(message)s",
|
||||
datefmt="%x %X",
|
||||
)
|
||||
logger = logging.getLogger()
|
||||
|
||||
if DEBUG:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logging.getLogger("asyncssh").setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.INFO)
|
||||
logging.getLogger("asyncssh").setLevel(logging.WARNING)
|
||||
@@ -19,13 +19,25 @@ version = version.strftime("%y.%m.%d")
|
||||
print(version)
|
||||
|
||||
|
||||
setup(name="UpstreamCFGUtil.exe",
|
||||
version=version,
|
||||
description="Upstream Data Config Utility Build",
|
||||
options={"build_exe": {"build_exe": f"{os.getcwd()}\\build\\UpstreamCFGUtil-{version}-{sys.platform}\\",
|
||||
"include_files": [os.path.join(os.getcwd(), "settings.toml"),
|
||||
os.path.join(os.getcwd(), "CFG-Util-README.md")],
|
||||
},
|
||||
},
|
||||
executables=[Executable("config_tool.py", base=base, icon="icon.ico", target_name="UpstreamCFGUtil.exe")]
|
||||
)
|
||||
setup(
|
||||
name="UpstreamCFGUtil.exe",
|
||||
version=version,
|
||||
description="Upstream Data Config Utility Build",
|
||||
options={
|
||||
"build_exe": {
|
||||
"build_exe": f"{os.getcwd()}\\build\\UpstreamCFGUtil-{version}-{sys.platform}\\",
|
||||
"include_files": [
|
||||
os.path.join(os.getcwd(), "settings/settings.toml"),
|
||||
os.path.join(os.getcwd(), "static/CFG-Util-README.md"),
|
||||
],
|
||||
},
|
||||
},
|
||||
executables=[
|
||||
Executable(
|
||||
"config_tool.py",
|
||||
base=base,
|
||||
icon="icon.ico",
|
||||
target_name="UpstreamCFGUtil.exe",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,12 +1,104 @@
|
||||
from API.bmminer import BMMinerAPI
|
||||
from API.bosminer import BOSMinerAPI
|
||||
from API.cgminer import CGMinerAPI
|
||||
from API.btminer import BTMinerAPI
|
||||
from API.unknown import UnknownAPI
|
||||
import ipaddress
|
||||
import asyncssh
|
||||
import logging
|
||||
|
||||
|
||||
class BaseMiner:
|
||||
def __init__(self, ip: str, api: BMMinerAPI | BOSMinerAPI | CGMinerAPI | BTMinerAPI | UnknownAPI) -> None:
|
||||
self.ip = ipaddress.ip_address(ip)
|
||||
self.api = api
|
||||
def __init__(self, *args) -> None:
|
||||
self.ip = None
|
||||
self.uname = "root"
|
||||
self.pwd = "admin"
|
||||
self.api = None
|
||||
self.api_type = None
|
||||
self.model = None
|
||||
self.light = None
|
||||
self.hostname = None
|
||||
self.nominal_chips = 1
|
||||
self.version = None
|
||||
|
||||
def __repr__(self):
|
||||
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
|
||||
|
||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
||||
"""Create a new asyncssh connection"""
|
||||
try:
|
||||
conn = await asyncssh.connect(
|
||||
str(self.ip),
|
||||
known_hosts=None,
|
||||
username=self.uname,
|
||||
password=self.pwd,
|
||||
server_host_key_algs=["ssh-rsa"],
|
||||
)
|
||||
return conn
|
||||
except asyncssh.misc.PermissionDenied:
|
||||
try:
|
||||
conn = await asyncssh.connect(
|
||||
str(self.ip),
|
||||
known_hosts=None,
|
||||
username="root",
|
||||
password="admin",
|
||||
server_host_key_algs=["ssh-rsa"],
|
||||
)
|
||||
return conn
|
||||
except Exception as e:
|
||||
# logging.warning(f"{self} raised an exception: {e}")
|
||||
raise e
|
||||
except OSError as e:
|
||||
logging.warning(f"Connection refused: {self}")
|
||||
raise e
|
||||
except Exception as e:
|
||||
# logging.warning(f"{self} raised an exception: {e}")
|
||||
raise e
|
||||
|
||||
async def fault_light_on(self) -> bool:
|
||||
return False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
return False
|
||||
|
||||
async def send_file(self, src, dest):
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
await asyncssh.scp(src, (conn, dest))
|
||||
|
||||
async def check_light(self):
|
||||
return self.light
|
||||
|
||||
async def get_board_info(self):
|
||||
return None
|
||||
|
||||
async def get_config(self):
|
||||
return None
|
||||
|
||||
async def get_hostname(self):
|
||||
return None
|
||||
|
||||
async def get_model(self):
|
||||
return None
|
||||
|
||||
async def reboot(self):
|
||||
return False
|
||||
|
||||
async def restart_backend(self):
|
||||
return False
|
||||
|
||||
async def send_config(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
async def get_mac(self):
|
||||
return None
|
||||
|
||||
async def get_data(self):
|
||||
data = {
|
||||
"IP": str(self.ip),
|
||||
"Model": "Unknown",
|
||||
"Hostname": "Unknown",
|
||||
"Hashrate": 0,
|
||||
"Temperature": 0,
|
||||
"Pool User": "Unknown",
|
||||
"Wattage": 0,
|
||||
"Split": "0",
|
||||
"Pool 1": "Unknown",
|
||||
"Pool 1 User": "Unknown",
|
||||
"Pool 2": "",
|
||||
"Pool 2 User": "",
|
||||
}
|
||||
return data
|
||||
|
||||
5
miners/_backends/__init__.py
Normal file
5
miners/_backends/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .bmminer import BMMiner
|
||||
from .bosminer import BOSMiner
|
||||
from .btminer import BTMiner
|
||||
from .cgminer import CGMiner
|
||||
from .hiveon import Hiveon
|
||||
246
miners/_backends/bmminer.py
Normal file
246
miners/_backends/bmminer.py
Normal file
@@ -0,0 +1,246 @@
|
||||
from API.bmminer import BMMinerAPI
|
||||
from miners import BaseMiner
|
||||
import logging
|
||||
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||
|
||||
|
||||
class BMMiner(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
self.api = BMMinerAPI(ip)
|
||||
self.api_type = "BMMiner"
|
||||
self.uname = "root"
|
||||
self.pwd = "admin"
|
||||
|
||||
async def get_model(self) -> str or None:
|
||||
"""Get miner model.
|
||||
|
||||
:return: Miner model or None.
|
||||
"""
|
||||
# check if model is cached
|
||||
if self.model:
|
||||
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||
return self.model
|
||||
|
||||
# get devdetails data
|
||||
version_data = await self.api.devdetails()
|
||||
|
||||
# if we get data back, parse it for model
|
||||
if version_data:
|
||||
# handle Antminer BMMiner as a base
|
||||
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
|
||||
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||
return self.model
|
||||
|
||||
# if we don't get devdetails, log a failed attempt
|
||||
logging.warning(f"Failed to get model for miner: {self}")
|
||||
return None
|
||||
|
||||
async def get_hostname(self) -> str:
|
||||
"""Get miner hostname.
|
||||
|
||||
:return: The hostname of the miner as a string or "?"
|
||||
"""
|
||||
if self.hostname:
|
||||
return self.hostname
|
||||
try:
|
||||
# open an ssh connection
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
# if we get the connection, check hostname
|
||||
if conn is not None:
|
||||
# get output of the hostname file
|
||||
data = await conn.run("cat /proc/sys/kernel/hostname")
|
||||
host = data.stdout.strip()
|
||||
|
||||
# return hostname data
|
||||
logging.debug(f"Found hostname for {self.ip}: {host}")
|
||||
self.hostname = host
|
||||
return self.hostname
|
||||
else:
|
||||
# return ? if we fail to get hostname with no ssh connection
|
||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||
return "?"
|
||||
except Exception as e:
|
||||
# return ? if we fail to get hostname with an exception
|
||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||
return "?"
|
||||
|
||||
async def send_ssh_command(self, cmd: str) -> str or None:
|
||||
"""Send a command to the miner over ssh.
|
||||
|
||||
:param cmd: The command to run.
|
||||
|
||||
:return: Result of the command or None.
|
||||
"""
|
||||
result = None
|
||||
|
||||
# open an ssh connection
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
# 3 retries
|
||||
for i in range(3):
|
||||
try:
|
||||
# run the command and get the result
|
||||
result = await conn.run(cmd)
|
||||
result = result.stdout
|
||||
|
||||
except Exception as e:
|
||||
# if the command fails, log it
|
||||
logging.warning(f"{self} command {cmd} error: {e}")
|
||||
|
||||
# on the 3rd retry, return None
|
||||
if i == 3:
|
||||
return
|
||||
continue
|
||||
# return the result, either command output or None
|
||||
return result
|
||||
|
||||
async def get_config(self) -> list or None:
|
||||
"""Get the pool configuration of the miner.
|
||||
|
||||
:return: Pool config data or None.
|
||||
"""
|
||||
# get pool data
|
||||
pools = await self.api.pools()
|
||||
pool_data = []
|
||||
|
||||
# ensure we got pool data
|
||||
if not pools:
|
||||
return
|
||||
|
||||
# parse all the pools
|
||||
for pool in pools["POOLS"]:
|
||||
pool_data.append({"url": pool["URL"], "user": pool["User"], "pwd": "123"})
|
||||
return pool_data
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
logging.debug(f"{self}: Sending reboot command.")
|
||||
_ret = await self.send_ssh_command("reboot")
|
||||
logging.debug(f"{self}: Reboot command completed.")
|
||||
if isinstance(_ret, str):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_data(self):
|
||||
data = {
|
||||
"IP": str(self.ip),
|
||||
"Model": "Unknown",
|
||||
"Hostname": "Unknown",
|
||||
"Hashrate": 0,
|
||||
"Temperature": 0,
|
||||
"Pool User": "Unknown",
|
||||
"Wattage": 0,
|
||||
"Total": 0,
|
||||
"Ideal": self.nominal_chips * 3,
|
||||
"Left Board": 0,
|
||||
"Center Board": 0,
|
||||
"Right Board": 0,
|
||||
"Nominal": False,
|
||||
"Split": "0",
|
||||
"Pool 1": "Unknown",
|
||||
"Pool 1 User": "Unknown",
|
||||
"Pool 2": "",
|
||||
"Pool 2 User": "",
|
||||
}
|
||||
|
||||
model = await self.get_model()
|
||||
hostname = await self.get_hostname()
|
||||
|
||||
if model:
|
||||
data["Model"] = model
|
||||
|
||||
if hostname:
|
||||
data["Hostname"] = hostname
|
||||
|
||||
miner_data = None
|
||||
for i in range(DATA_RETRIES):
|
||||
miner_data = await self.api.multicommand("summary", "pools", "stats")
|
||||
if miner_data:
|
||||
break
|
||||
|
||||
if not miner_data:
|
||||
return data
|
||||
|
||||
summary = miner_data.get("summary")[0]
|
||||
pools = miner_data.get("pools")[0]
|
||||
stats = miner_data.get("stats")[0]
|
||||
|
||||
if summary:
|
||||
hr = summary.get("SUMMARY")
|
||||
if hr:
|
||||
if len(hr) > 0:
|
||||
hr = hr[0].get("GHS av")
|
||||
if hr:
|
||||
data["Hashrate"] = round(hr / 1000, 2)
|
||||
|
||||
if stats:
|
||||
boards = stats.get("STATS")
|
||||
if boards:
|
||||
if len(boards) > 0:
|
||||
data["Left Board"] = boards[1].get("chain_acn1")
|
||||
data["Center Board"] = boards[1].get("chain_acn2")
|
||||
data["Right Board"] = boards[1].get("chain_acn3")
|
||||
data["Total"] = boards[1].get("total_acn")
|
||||
|
||||
if data["Total"] == data["Ideal"]:
|
||||
data["Nominal"] = True
|
||||
|
||||
if stats:
|
||||
temp = stats.get("STATS")
|
||||
if temp:
|
||||
if len(temp) > 1:
|
||||
for item in ["temp2", "temp1", "temp3"]:
|
||||
temperature = temp[1].get(item)
|
||||
if temperature and not temperature == 0.0:
|
||||
data["Temperature"] = round(temperature)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
pool_2 = None
|
||||
pool_1_user = None
|
||||
pool_2_user = None
|
||||
pool_1_quota = 1
|
||||
pool_2_quota = 1
|
||||
quota = 0
|
||||
for pool in pools.get("POOLS"):
|
||||
if not pool_1_user:
|
||||
pool_1_user = pool.get("User")
|
||||
pool_1 = pool["URL"]
|
||||
pool_1_quota = pool["Quota"]
|
||||
elif not pool_2_user:
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
pool_2_quota = pool["Quota"]
|
||||
if not pool.get("User") == pool_1_user:
|
||||
if not pool_2_user == pool.get("User"):
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
pool_2_quota = pool["Quota"]
|
||||
if pool_2_user and not pool_2_user == pool_1_user:
|
||||
quota = f"{pool_1_quota}/{pool_2_quota}"
|
||||
|
||||
if pool_1:
|
||||
if pool_1.startswith("stratum+tcp://"):
|
||||
pool_1.replace("stratum+tcp://", "")
|
||||
if pool_1.startswith("stratum2+tcp://"):
|
||||
pool_1.replace("stratum2+tcp://", "")
|
||||
data["Pool 1"] = pool_1
|
||||
|
||||
if pool_1_user:
|
||||
data["Pool 1 User"] = pool_1_user
|
||||
data["Pool User"] = pool_1_user
|
||||
|
||||
if pool_2:
|
||||
if pool_2.startswith("stratum+tcp://"):
|
||||
pool_2.replace("stratum+tcp://", "")
|
||||
if pool_2.startswith("stratum2+tcp://"):
|
||||
pool_2.replace("stratum2+tcp://", "")
|
||||
data["Pool 2"] = pool_2
|
||||
|
||||
if pool_2_user:
|
||||
data["Pool 2 User"] = pool_2_user
|
||||
|
||||
if quota:
|
||||
data["Split"] = str(quota)
|
||||
|
||||
return data
|
||||
375
miners/_backends/bosminer.py
Normal file
375
miners/_backends/bosminer.py
Normal file
@@ -0,0 +1,375 @@
|
||||
from miners import BaseMiner
|
||||
from API.bosminer import BOSMinerAPI
|
||||
import toml
|
||||
from config.bos import bos_config_convert, general_config_convert_bos
|
||||
import logging
|
||||
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||
|
||||
|
||||
class BOSMiner(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
self.api = BOSMinerAPI(ip)
|
||||
self.api_type = "BOSMiner"
|
||||
self.uname = "root"
|
||||
self.pwd = "admin"
|
||||
|
||||
async def send_ssh_command(self, cmd: str) -> str or None:
|
||||
"""Send a command to the miner over ssh.
|
||||
|
||||
:return: Result of the command or None.
|
||||
"""
|
||||
result = None
|
||||
|
||||
# open an ssh connection
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
# 3 retries
|
||||
for i in range(3):
|
||||
try:
|
||||
# run the command and get the result
|
||||
result = await conn.run(cmd)
|
||||
result = result.stdout
|
||||
except Exception as e:
|
||||
# if the command fails, log it
|
||||
logging.warning(f"{self} command {cmd} error: {e}")
|
||||
|
||||
# on the 3rd retry, return None
|
||||
if i == 3:
|
||||
return
|
||||
continue
|
||||
# return the result, either command output or None
|
||||
return str(result)
|
||||
|
||||
async def fault_light_on(self) -> bool:
|
||||
"""Sends command to turn on fault light on the miner."""
|
||||
logging.debug(f"{self}: Sending fault_light on command.")
|
||||
self.light = True
|
||||
_ret = await self.send_ssh_command("miner fault_light on")
|
||||
logging.debug(f"{self}: fault_light on command completed.")
|
||||
if isinstance(_ret, str):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
"""Sends command to turn off fault light on the miner."""
|
||||
logging.debug(f"{self}: Sending fault_light off command.")
|
||||
self.light = False
|
||||
_ret = await self.send_ssh_command("miner fault_light off")
|
||||
logging.debug(f"{self}: fault_light off command completed.")
|
||||
if isinstance(_ret, str):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def restart_backend(self) -> bool:
|
||||
return await self.restart_bosminer()
|
||||
|
||||
async def restart_bosminer(self) -> bool:
|
||||
"""Restart bosminer hashing process."""
|
||||
logging.debug(f"{self}: Sending bosminer restart command.")
|
||||
_ret = await self.send_ssh_command("/etc/init.d/bosminer restart")
|
||||
logging.debug(f"{self}: bosminer restart command completed.")
|
||||
if isinstance(_ret, str):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
"""Reboots power to the physical miner."""
|
||||
logging.debug(f"{self}: Sending reboot command.")
|
||||
_ret = await self.send_ssh_command("/sbin/reboot")
|
||||
logging.debug(f"{self}: Reboot command completed.")
|
||||
if isinstance(_ret, str):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_config(self) -> None:
|
||||
logging.debug(f"{self}: Getting config.")
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
logging.debug(f"{self}: Opening SFTP connection.")
|
||||
async with conn.start_sftp_client() as sftp:
|
||||
logging.debug(f"{self}: Reading config file.")
|
||||
async with sftp.open("/etc/bosminer.toml") as file:
|
||||
toml_data = toml.loads(await file.read())
|
||||
logging.debug(f"{self}: Converting config file.")
|
||||
cfg = bos_config_convert(toml_data)
|
||||
self.config = cfg
|
||||
|
||||
async def get_hostname(self) -> str:
|
||||
"""Get miner hostname.
|
||||
|
||||
:return: The hostname of the miner as a string or "?"
|
||||
"""
|
||||
if self.hostname:
|
||||
return self.hostname
|
||||
try:
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
if conn is not None:
|
||||
data = await conn.run("cat /proc/sys/kernel/hostname")
|
||||
host = data.stdout.strip()
|
||||
logging.debug(f"Found hostname for {self.ip}: {host}")
|
||||
self.hostname = host
|
||||
return self.hostname
|
||||
else:
|
||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||
return "?"
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||
return "?"
|
||||
|
||||
async def get_model(self) -> str or None:
|
||||
"""Get miner model.
|
||||
|
||||
:return: Miner model or None.
|
||||
"""
|
||||
# check if model is cached
|
||||
if self.model:
|
||||
logging.debug(f"Found model for {self.ip}: {self.model} (BOS)")
|
||||
return self.model + " (BOS)"
|
||||
|
||||
# get devdetails data
|
||||
version_data = await self.api.devdetails()
|
||||
|
||||
# if we get data back, parse it for model
|
||||
if version_data:
|
||||
if not version_data["DEVDETAILS"] == []:
|
||||
# handle Antminer BOSMiner as a base
|
||||
self.model = version_data["DEVDETAILS"][0]["Model"].replace(
|
||||
"Antminer ", ""
|
||||
)
|
||||
logging.debug(f"Found model for {self.ip}: {self.model} (BOS)")
|
||||
return self.model + " (BOS)"
|
||||
|
||||
# if we don't get devdetails, log a failed attempt
|
||||
logging.warning(f"Failed to get model for miner: {self}")
|
||||
return None
|
||||
|
||||
async def get_version(self):
|
||||
"""Get miner firmware version.
|
||||
|
||||
:return: Miner firmware version or None.
|
||||
"""
|
||||
# check if version is cached
|
||||
if self.version:
|
||||
logging.debug(f"Found version for {self.ip}: {self.version}")
|
||||
return self.version
|
||||
|
||||
# get output of bos version file
|
||||
version_data = await self.send_ssh_command("cat /etc/bos_version")
|
||||
|
||||
# if we get the version data, parse it
|
||||
if version_data:
|
||||
self.version = version_data.stdout.split("-")[5]
|
||||
logging.debug(f"Found version for {self.ip}: {self.version}")
|
||||
return self.version
|
||||
|
||||
# if we fail to get version, log a failed attempt
|
||||
logging.warning(f"Failed to get model for miner: {self}")
|
||||
return None
|
||||
|
||||
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
|
||||
"""Configures miner with yaml config."""
|
||||
logging.debug(f"{self}: Sending config.")
|
||||
if ip_user:
|
||||
suffix = str(self.ip).split(".")[-1]
|
||||
toml_conf = toml.dumps(
|
||||
general_config_convert_bos(yaml_config, user_suffix=suffix)
|
||||
)
|
||||
else:
|
||||
toml_conf = toml.dumps(general_config_convert_bos(yaml_config))
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
logging.debug(f"{self}: Opening SFTP connection.")
|
||||
async with conn.start_sftp_client() as sftp:
|
||||
logging.debug(f"{self}: Opening config file.")
|
||||
async with sftp.open("/etc/bosminer.toml", "w+") as file:
|
||||
await file.write(toml_conf)
|
||||
logging.debug(f"{self}: Restarting BOSMiner")
|
||||
await conn.run("/etc/init.d/bosminer restart")
|
||||
|
||||
async def get_board_info(self) -> dict:
|
||||
"""Gets data on each board and chain in the miner."""
|
||||
logging.debug(f"{self}: Getting board info.")
|
||||
devdetails = await self.api.devdetails()
|
||||
if not devdetails.get("DEVDETAILS"):
|
||||
print("devdetails error", devdetails)
|
||||
return {0: [], 1: [], 2: []}
|
||||
devs = devdetails["DEVDETAILS"]
|
||||
boards = {}
|
||||
offset = devs[0]["ID"]
|
||||
for board in devs:
|
||||
boards[board["ID"] - offset] = []
|
||||
if not board["Chips"] == self.nominal_chips:
|
||||
nominal = False
|
||||
else:
|
||||
nominal = True
|
||||
if not board["Chips"] == self.nominal_chips:
|
||||
nominal = False
|
||||
else:
|
||||
nominal = True
|
||||
boards[board["ID"] - offset].append(
|
||||
{
|
||||
"chain": board["ID"] - offset,
|
||||
"chip_count": board["Chips"],
|
||||
"chip_status": "o" * board["Chips"],
|
||||
"nominal": nominal,
|
||||
}
|
||||
)
|
||||
logging.debug(f"Found board data for {self}: {boards}")
|
||||
return boards
|
||||
|
||||
async def get_bad_boards(self) -> dict:
|
||||
"""Checks for and provides list of non working boards."""
|
||||
boards = await self.get_board_info()
|
||||
bad_boards = {}
|
||||
for board in boards.keys():
|
||||
for chain in boards[board]:
|
||||
if not chain["chip_count"] == 63:
|
||||
if board not in bad_boards.keys():
|
||||
bad_boards[board] = []
|
||||
bad_boards[board].append(chain)
|
||||
return bad_boards
|
||||
|
||||
async def check_good_boards(self) -> str:
|
||||
"""Checks for and provides list for working boards."""
|
||||
devs = await self.api.devdetails()
|
||||
bad = 0
|
||||
chains = devs["DEVDETAILS"]
|
||||
for chain in chains:
|
||||
if chain["Chips"] == 0:
|
||||
bad += 1
|
||||
if not bad > 0:
|
||||
return str(self.ip)
|
||||
|
||||
async def get_data(self):
|
||||
data = {
|
||||
"IP": str(self.ip),
|
||||
"Model": "Unknown",
|
||||
"Hostname": "Unknown",
|
||||
"Hashrate": 0,
|
||||
"Temperature": 0,
|
||||
"Pool User": "Unknown",
|
||||
"Wattage": 0,
|
||||
"Total": 0,
|
||||
"Ideal": self.nominal_chips * 3,
|
||||
"Left Board": 0,
|
||||
"Center Board": 0,
|
||||
"Right Board": 0,
|
||||
"Nominal": False,
|
||||
"Split": "0",
|
||||
"Pool 1": "Unknown",
|
||||
"Pool 1 User": "Unknown",
|
||||
"Pool 2": "",
|
||||
"Pool 2 User": "",
|
||||
}
|
||||
model = await self.get_model()
|
||||
hostname = await self.get_hostname()
|
||||
|
||||
if model:
|
||||
data["Model"] = model
|
||||
|
||||
if hostname:
|
||||
data["Hostname"] = hostname
|
||||
|
||||
miner_data = None
|
||||
for i in range(DATA_RETRIES):
|
||||
miner_data = await self.api.multicommand(
|
||||
"summary", "temps", "tunerstatus", "pools", "devdetails"
|
||||
)
|
||||
if miner_data:
|
||||
break
|
||||
if not miner_data:
|
||||
return data
|
||||
summary = miner_data.get("summary")[0]
|
||||
temps = miner_data.get("temps")[0]
|
||||
tunerstatus = miner_data.get("tunerstatus")[0]
|
||||
pools = miner_data.get("pools")[0]
|
||||
devdetails = miner_data.get("devdetails")[0]
|
||||
|
||||
if summary:
|
||||
hr = summary.get("SUMMARY")
|
||||
if hr:
|
||||
if len(hr) > 0:
|
||||
hr = hr[0].get("MHS av")
|
||||
if hr:
|
||||
data["Hashrate"] = round(hr / 1000000, 2)
|
||||
|
||||
if temps:
|
||||
temp = temps.get("TEMPS")
|
||||
if temp:
|
||||
if len(temp) > 0:
|
||||
temp = temp[0].get("Chip")
|
||||
if temp:
|
||||
data["Temperature"] = round(temp)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
pool_2 = None
|
||||
pool_1_user = None
|
||||
pool_2_user = None
|
||||
pool_1_quota = 1
|
||||
pool_2_quota = 1
|
||||
quota = 0
|
||||
for pool in pools.get("POOLS"):
|
||||
if not pool_1_user:
|
||||
pool_1_user = pool.get("User")
|
||||
pool_1 = pool["URL"]
|
||||
pool_1_quota = pool["Quota"]
|
||||
elif not pool_2_user:
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
pool_2_quota = pool["Quota"]
|
||||
if not pool.get("User") == pool_1_user:
|
||||
if not pool_2_user == pool.get("User"):
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
pool_2_quota = pool["Quota"]
|
||||
if pool_2_user and not pool_2_user == pool_1_user:
|
||||
quota = f"{pool_1_quota}/{pool_2_quota}"
|
||||
|
||||
if pool_1:
|
||||
pool_1 = pool_1.replace("stratum+tcp://", "")
|
||||
pool_1 = pool_1.replace("stratum2+tcp://", "")
|
||||
data["Pool 1"] = pool_1
|
||||
|
||||
if pool_1_user:
|
||||
data["Pool 1 User"] = pool_1_user
|
||||
data["Pool User"] = pool_1_user
|
||||
|
||||
if pool_2:
|
||||
pool_2 = pool_2.replace("stratum+tcp://", "")
|
||||
pool_2 = pool_2.replace("stratum2+tcp://", "")
|
||||
data["Pool 2"] = pool_2
|
||||
|
||||
if pool_2_user:
|
||||
data["Pool 2 User"] = pool_2_user
|
||||
|
||||
if quota:
|
||||
data["Split"] = str(quota)
|
||||
|
||||
if tunerstatus:
|
||||
tuner = tunerstatus.get("TUNERSTATUS")
|
||||
if tuner:
|
||||
if len(tuner) > 0:
|
||||
wattage = tuner[0].get("PowerLimit")
|
||||
if wattage:
|
||||
data["Wattage"] = wattage
|
||||
|
||||
if devdetails:
|
||||
boards = devdetails.get("DEVDETAILS")
|
||||
if boards:
|
||||
if len(boards) > 0:
|
||||
board_map = {0: "Left Board", 1: "Center Board", 2: "Right Board"}
|
||||
offset = boards[0]["ID"]
|
||||
for board in boards:
|
||||
id = board["ID"] - offset
|
||||
chips = board["Chips"]
|
||||
data["Total"] += chips
|
||||
data[board_map[id]] = chips
|
||||
|
||||
if data["Total"] == data["Ideal"]:
|
||||
data["Nominal"] = True
|
||||
|
||||
return data
|
||||
|
||||
async def get_mac(self):
|
||||
result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
|
||||
return result.upper()
|
||||
46
miners/_backends/bosminer_old.py
Normal file
46
miners/_backends/bosminer_old.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from miners import BaseMiner
|
||||
from API.bosminer import BOSMinerAPI
|
||||
import toml
|
||||
from config.bos import bos_config_convert, general_config_convert_bos
|
||||
import logging
|
||||
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||
import asyncssh
|
||||
|
||||
|
||||
class BOSMinerOld(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.api = BOSMinerAPI(ip)
|
||||
self.api_type = "BOSMiner"
|
||||
self.uname = "root"
|
||||
self.pwd = "admin"
|
||||
|
||||
async def send_ssh_command(self, cmd: str) -> str or None:
|
||||
"""Send a command to the miner over ssh.
|
||||
|
||||
:return: Result of the command or None.
|
||||
"""
|
||||
result = None
|
||||
|
||||
# open an ssh connection
|
||||
async with await asyncssh.connect("192.168.1.11", username="root") as conn:
|
||||
# 3 retries
|
||||
for i in range(3):
|
||||
try:
|
||||
# run the command and get the result
|
||||
result = await conn.run(cmd)
|
||||
result = result.stdout
|
||||
except Exception as e:
|
||||
# if the command fails, log it
|
||||
logging.warning(f"{self} command {cmd} error: {e}")
|
||||
|
||||
# on the 3rd retry, return None
|
||||
if i == 3:
|
||||
return
|
||||
continue
|
||||
# return the result, either command output or None
|
||||
return str(result)
|
||||
|
||||
async def update_to_plus(self):
|
||||
result = await self.send_ssh_command("opkg update && opkg install bos_plus")
|
||||
return result
|
||||
226
miners/_backends/btminer.py
Normal file
226
miners/_backends/btminer.py
Normal file
@@ -0,0 +1,226 @@
|
||||
from API.btminer import BTMinerAPI
|
||||
from miners import BaseMiner
|
||||
from API import APIError
|
||||
import logging
|
||||
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||
|
||||
|
||||
class BTMiner(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
self.api = BTMinerAPI(ip)
|
||||
self.api_type = "BTMiner"
|
||||
|
||||
async def get_model(self):
|
||||
if self.model:
|
||||
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||
return self.model
|
||||
version_data = await self.api.devdetails()
|
||||
if version_data:
|
||||
self.model = version_data["DEVDETAILS"][0]["Model"].split("V")[0]
|
||||
logging.debug(f"Found model for {self.ip}: {self.model}")
|
||||
return self.model
|
||||
logging.warning(f"Failed to get model for miner: {self}")
|
||||
return None
|
||||
|
||||
async def get_hostname(self) -> str:
|
||||
if self.hostname:
|
||||
return self.hostname
|
||||
try:
|
||||
host_data = await self.api.get_miner_info()
|
||||
if host_data:
|
||||
host = host_data["Msg"]["hostname"]
|
||||
logging.debug(f"Found hostname for {self.ip}: {host}")
|
||||
self.hostname = host
|
||||
return self.hostname
|
||||
except APIError:
|
||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||
return "?"
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||
return "?"
|
||||
|
||||
async def get_board_info(self) -> dict:
|
||||
"""Gets data on each board and chain in the miner."""
|
||||
logging.debug(f"{self}: Getting board info.")
|
||||
devs = await self.api.devs()
|
||||
if not devs.get("DEVS"):
|
||||
print("devs error", devs)
|
||||
return {0: [], 1: [], 2: []}
|
||||
devs = devs["DEVS"]
|
||||
boards = {}
|
||||
offset = devs[0]["ID"]
|
||||
for board in devs:
|
||||
boards[board["ID"] - offset] = []
|
||||
if "Effective Chips" in board.keys():
|
||||
if not board["Effective Chips"] in self.nominal_chips:
|
||||
nominal = False
|
||||
else:
|
||||
nominal = True
|
||||
boards[board["ID"] - offset].append(
|
||||
{
|
||||
"chain": board["ID"] - offset,
|
||||
"chip_count": board["Effective Chips"],
|
||||
"chip_status": "o" * board["Effective Chips"],
|
||||
"nominal": nominal,
|
||||
}
|
||||
)
|
||||
else:
|
||||
logging.warning(f"Incorrect board data from {self}: {board}")
|
||||
print(board)
|
||||
logging.debug(f"Found board data for {self}: {boards}")
|
||||
return boards
|
||||
|
||||
async def get_mac(self):
|
||||
mac = ""
|
||||
data = await self.api.get_miner_info()
|
||||
if data:
|
||||
if "Msg" in data.keys():
|
||||
if "mac" in data["Msg"].keys():
|
||||
mac = data["Msg"]["mac"]
|
||||
return str(mac).upper()
|
||||
|
||||
async def get_data(self):
|
||||
data = {
|
||||
"IP": str(self.ip),
|
||||
"Model": "Unknown",
|
||||
"Hostname": "Unknown",
|
||||
"Hashrate": 0,
|
||||
"Temperature": 0,
|
||||
"Pool User": "Unknown",
|
||||
"Wattage": 0,
|
||||
"Total": 0,
|
||||
"Ideal": self.nominal_chips * 3,
|
||||
"Left Board": 0,
|
||||
"Center Board": 0,
|
||||
"Right Board": 0,
|
||||
"Nominal": False,
|
||||
"Split": "0",
|
||||
"Pool 1": "Unknown",
|
||||
"Pool 1 User": "Unknown",
|
||||
"Pool 2": "",
|
||||
"Pool 2 User": "",
|
||||
}
|
||||
|
||||
try:
|
||||
model = await self.get_model()
|
||||
hostname = await self.get_hostname()
|
||||
except APIError:
|
||||
logging.warning(f"Failed to get hostname and model: {self}")
|
||||
model = None
|
||||
data["Model"] = "Whatsminer"
|
||||
hostname = None
|
||||
data["Hostname"] = "Whatsminer"
|
||||
|
||||
if model:
|
||||
data["Model"] = model
|
||||
|
||||
if hostname:
|
||||
data["Hostname"] = hostname
|
||||
miner_data = None
|
||||
for i in range(DATA_RETRIES):
|
||||
try:
|
||||
miner_data = await self.api.multicommand("summary", "devs", "pools")
|
||||
if miner_data:
|
||||
break
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if not miner_data:
|
||||
return data
|
||||
|
||||
summary = miner_data.get("summary")[0]
|
||||
devs = miner_data.get("devs")[0]
|
||||
pools = miner_data.get("pools")[0]
|
||||
|
||||
if summary:
|
||||
summary_data = summary.get("SUMMARY")
|
||||
if summary_data:
|
||||
if len(summary_data) > 0:
|
||||
hr = summary_data[0].get("MHS av")
|
||||
if hr:
|
||||
data["Hashrate"] = round(hr / 1000000, 2)
|
||||
|
||||
wattage = summary_data[0].get("Power")
|
||||
if wattage:
|
||||
data["Wattage"] = round(wattage)
|
||||
|
||||
if devs:
|
||||
temp_data = devs.get("DEVS")
|
||||
if temp_data:
|
||||
for board in temp_data:
|
||||
temp = board.get("Chip Temp Avg")
|
||||
if temp and not temp == 0.0:
|
||||
data["Temperature"] = round(temp)
|
||||
break
|
||||
|
||||
if devs:
|
||||
boards = devs.get("DEVS")
|
||||
if boards:
|
||||
if len(boards) > 0:
|
||||
board_map = {0: "Left Board", 1: "Center Board", 2: "Right Board"}
|
||||
if "ID" in boards[0].keys():
|
||||
id_key = "ID"
|
||||
else:
|
||||
id_key = "ASC"
|
||||
offset = boards[0][id_key]
|
||||
for board in boards:
|
||||
id = board[id_key] - offset
|
||||
chips = board["Effective Chips"]
|
||||
data["Total"] += chips
|
||||
data[board_map[id]] = chips
|
||||
|
||||
if data["Total"] == data["Ideal"]:
|
||||
data["Nominal"] = True
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
pool_2 = None
|
||||
pool_1_user = None
|
||||
pool_2_user = None
|
||||
pool_1_quota = 1
|
||||
pool_2_quota = 1
|
||||
quota = 0
|
||||
for pool in pools.get("POOLS"):
|
||||
if not pool_1_user:
|
||||
pool_1_user = pool.get("User")
|
||||
pool_1 = pool["URL"]
|
||||
pool_1_quota = pool["Quota"]
|
||||
elif not pool_2_user:
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
pool_2_quota = pool["Quota"]
|
||||
if not pool.get("User") == pool_1_user:
|
||||
if not pool_2_user == pool.get("User"):
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
pool_2_quota = pool["Quota"]
|
||||
if pool_2_user and not pool_2_user == pool_1_user:
|
||||
quota = f"{pool_1_quota}/{pool_2_quota}"
|
||||
|
||||
if pool_1:
|
||||
if pool_1.startswith("stratum+tcp://"):
|
||||
pool_1.replace("stratum+tcp://", "")
|
||||
if pool_1.startswith("stratum2+tcp://"):
|
||||
pool_1.replace("stratum2+tcp://", "")
|
||||
data["Pool 1"] = pool_1
|
||||
|
||||
if pool_1_user:
|
||||
data["Pool 1 User"] = pool_1_user
|
||||
data["Pool User"] = pool_1_user
|
||||
|
||||
if pool_2:
|
||||
if pool_2.startswith("stratum+tcp://"):
|
||||
pool_2.replace("stratum+tcp://", "")
|
||||
if pool_2.startswith("stratum2+tcp://"):
|
||||
pool_2.replace("stratum2+tcp://", "")
|
||||
data["Pool 2"] = pool_2
|
||||
|
||||
if pool_2_user:
|
||||
data["Pool 2 User"] = pool_2_user
|
||||
|
||||
if quota:
|
||||
data["Split"] = str(quota)
|
||||
|
||||
return data
|
||||
207
miners/_backends/cgminer.py
Normal file
207
miners/_backends/cgminer.py
Normal file
@@ -0,0 +1,207 @@
|
||||
from miners import BaseMiner
|
||||
from API.cgminer import CGMinerAPI
|
||||
from API import APIError
|
||||
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||
import logging
|
||||
|
||||
|
||||
class CGMiner(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
self.api = CGMinerAPI(ip)
|
||||
self.api_type = "CGMiner"
|
||||
self.uname = "root"
|
||||
self.pwd = "admin"
|
||||
|
||||
async def get_model(self):
|
||||
if self.model:
|
||||
return self.model
|
||||
try:
|
||||
version_data = await self.api.devdetails()
|
||||
except APIError:
|
||||
return None
|
||||
if version_data:
|
||||
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
|
||||
return self.model
|
||||
return None
|
||||
|
||||
async def get_hostname(self) -> str:
|
||||
if self.hostname:
|
||||
return self.hostname
|
||||
try:
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
if conn is not None:
|
||||
data = await conn.run("cat /proc/sys/kernel/hostname")
|
||||
host = data.stdout.strip()
|
||||
self.hostname = host
|
||||
return self.hostname
|
||||
else:
|
||||
return "?"
|
||||
except Exception:
|
||||
return "?"
|
||||
|
||||
async def send_ssh_command(self, cmd):
|
||||
result = None
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
for i in range(3):
|
||||
try:
|
||||
result = await conn.run(cmd)
|
||||
result = result.stdout
|
||||
except Exception as e:
|
||||
print(f"{cmd} error: {e}")
|
||||
if i == 3:
|
||||
return
|
||||
continue
|
||||
return result
|
||||
|
||||
async def restart_backend(self) -> bool:
|
||||
return await self.restart_cgminer()
|
||||
|
||||
async def restart_cgminer(self) -> bool:
|
||||
commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"]
|
||||
commands = ";".join(commands)
|
||||
_ret = await self.send_ssh_command(commands)
|
||||
if isinstance(_ret, str):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
logging.debug(f"{self}: Sending reboot command.")
|
||||
_ret = await self.send_ssh_command("reboot")
|
||||
logging.debug(f"{self}: Reboot command completed.")
|
||||
if isinstance(_ret, str):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def start_cgminer(self) -> None:
|
||||
commands = [
|
||||
"mkdir -p /etc/tmp/",
|
||||
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
|
||||
"crontab -u root /etc/tmp/root",
|
||||
"/usr/bin/cgminer-monitor >/dev/null 2>&1",
|
||||
]
|
||||
commands = ";".join(commands)
|
||||
await self.send_ssh_command(commands)
|
||||
|
||||
async def stop_cgminer(self) -> None:
|
||||
commands = [
|
||||
"mkdir -p /etc/tmp/",
|
||||
'echo "" > /etc/tmp/root',
|
||||
"crontab -u root /etc/tmp/root",
|
||||
"killall cgminer",
|
||||
]
|
||||
commands = ";".join(commands)
|
||||
await self.send_ssh_command(commands)
|
||||
|
||||
async def get_config(self) -> None:
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
command = "cat /etc/config/cgminer"
|
||||
result = await conn.run(command, check=True)
|
||||
self.config = result.stdout
|
||||
print(str(self.config))
|
||||
|
||||
async def get_data(self):
|
||||
data = {
|
||||
"IP": str(self.ip),
|
||||
"Model": "Unknown",
|
||||
"Hostname": "Unknown",
|
||||
"Hashrate": 0,
|
||||
"Temperature": 0,
|
||||
"Pool User": "Unknown",
|
||||
"Wattage": 0,
|
||||
"Split": 0,
|
||||
"Pool 1": "Unknown",
|
||||
"Pool 1 User": "Unknown",
|
||||
"Pool 2": "",
|
||||
"Pool 2 User": "",
|
||||
}
|
||||
|
||||
model = await self.get_model()
|
||||
hostname = await self.get_hostname()
|
||||
|
||||
if model:
|
||||
data["Model"] = model
|
||||
|
||||
if hostname:
|
||||
data["Hostname"] = hostname
|
||||
miner_data = None
|
||||
for i in range(DATA_RETRIES):
|
||||
miner_data = await self.api.multicommand("summary", "pools", "stats")
|
||||
if miner_data:
|
||||
break
|
||||
|
||||
if not miner_data:
|
||||
return data
|
||||
|
||||
summary = miner_data.get("summary")[0]
|
||||
pools = miner_data.get("pools")[0]
|
||||
stats = miner_data.get("stats")[0]
|
||||
|
||||
if summary:
|
||||
hr = summary.get("SUMMARY")
|
||||
if hr:
|
||||
if len(hr) > 0:
|
||||
hr = hr[0].get("GHS av")
|
||||
if hr:
|
||||
data["Hashrate"] = round(hr / 1000, 2)
|
||||
|
||||
if stats:
|
||||
temp = stats.get("STATS")
|
||||
if temp:
|
||||
if len(temp) > 1:
|
||||
for item in ["temp2", "temp1", "temp3"]:
|
||||
temperature = temp[1].get(item)
|
||||
if temperature and not temperature == 0.0:
|
||||
data["Temperature"] = round(temperature)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
pool_2 = None
|
||||
pool_1_user = None
|
||||
pool_2_user = None
|
||||
pool_1_quota = 1
|
||||
pool_2_quota = 1
|
||||
quota = 0
|
||||
for pool in pools.get("POOLS"):
|
||||
if not pool_1_user:
|
||||
pool_1_user = pool.get("User")
|
||||
pool_1 = pool["URL"]
|
||||
pool_1_quota = pool["Quota"]
|
||||
elif not pool_2_user:
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
pool_2_quota = pool["Quota"]
|
||||
if not pool.get("User") == pool_1_user:
|
||||
if not pool_2_user == pool.get("User"):
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
pool_2_quota = pool["Quota"]
|
||||
if pool_2_user and not pool_2_user == pool_1_user:
|
||||
quota = f"{pool_1_quota}/{pool_2_quota}"
|
||||
|
||||
if pool_1:
|
||||
if pool_1.startswith("stratum+tcp://"):
|
||||
pool_1.replace("stratum+tcp://", "")
|
||||
if pool_1.startswith("stratum2+tcp://"):
|
||||
pool_1.replace("stratum2+tcp://", "")
|
||||
data["Pool 1"] = pool_1
|
||||
|
||||
if pool_1_user:
|
||||
data["Pool 1 User"] = pool_1_user
|
||||
data["Pool User"] = pool_1_user
|
||||
|
||||
if pool_2:
|
||||
if pool_2.startswith("stratum+tcp://"):
|
||||
pool_2.replace("stratum+tcp://", "")
|
||||
if pool_2.startswith("stratum2+tcp://"):
|
||||
pool_2.replace("stratum2+tcp://", "")
|
||||
data["Pool 2"] = pool_2
|
||||
|
||||
if pool_2_user:
|
||||
data["Pool 2 User"] = pool_2_user
|
||||
|
||||
if quota:
|
||||
data["Split"] = str(quota)
|
||||
|
||||
return data
|
||||
47
miners/_backends/hiveon.py
Normal file
47
miners/_backends/hiveon.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from miners._backends import BMMiner
|
||||
|
||||
|
||||
class Hiveon(BMMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
self.api_type = "Hiveon"
|
||||
self.uname = "root"
|
||||
self.pwd = "admin"
|
||||
|
||||
async def get_board_info(self) -> dict:
|
||||
"""Gets data on each board and chain in the miner."""
|
||||
board_stats = await self.api.stats()
|
||||
stats = board_stats["STATS"][1]
|
||||
boards = {}
|
||||
board_chains = {0: [2, 9, 10], 1: [3, 11, 12], 2: [4, 13, 14]}
|
||||
for idx, board in enumerate(board_chains):
|
||||
boards[board] = []
|
||||
for chain in board_chains[board]:
|
||||
count = stats[f"chain_acn{chain}"]
|
||||
chips = stats[f"chain_acs{chain}"].replace(" ", "")
|
||||
if not count == 18 or "x" in chips:
|
||||
nominal = False
|
||||
else:
|
||||
nominal = True
|
||||
boards[board].append(
|
||||
{
|
||||
"chain": chain,
|
||||
"chip_count": count,
|
||||
"chip_status": chips,
|
||||
"nominal": nominal,
|
||||
}
|
||||
)
|
||||
return boards
|
||||
|
||||
async def get_bad_boards(self) -> dict:
|
||||
"""Checks for and provides list of non working boards."""
|
||||
boards = await self.get_board_info()
|
||||
bad_boards = {}
|
||||
for board in boards.keys():
|
||||
for chain in boards[board]:
|
||||
if not chain["chip_count"] == 18 or "x" in chain["chip_status"]:
|
||||
if board not in bad_boards.keys():
|
||||
bad_boards[board] = []
|
||||
bad_boards[board].append(chain)
|
||||
return bad_boards
|
||||
3
miners/_types/__init__.py
Normal file
3
miners/_types/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .antminer import *
|
||||
from .avalonminer import *
|
||||
from .whatsminer import *
|
||||
9
miners/_types/antminer/X17/S17.py
Normal file
9
miners/_types/antminer/X17/S17.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S17(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S17"
|
||||
self.nominal_chips = 48
|
||||
9
miners/_types/antminer/X17/S17_Plus.py
Normal file
9
miners/_types/antminer/X17/S17_Plus.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S17Plus(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S17+"
|
||||
self.nominal_chips = 65
|
||||
9
miners/_types/antminer/X17/S17_Pro.py
Normal file
9
miners/_types/antminer/X17/S17_Pro.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S17Pro(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S17 Pro"
|
||||
self.nominal_chips = 48
|
||||
9
miners/_types/antminer/X17/S17e.py
Normal file
9
miners/_types/antminer/X17/S17e.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S17e(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S17e"
|
||||
self.nominal_chips = 135
|
||||
9
miners/_types/antminer/X17/T17.py
Normal file
9
miners/_types/antminer/X17/T17.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class T17(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "T17"
|
||||
self.nominal_chips = 30
|
||||
9
miners/_types/antminer/X17/T17_Plus.py
Normal file
9
miners/_types/antminer/X17/T17_Plus.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class T17Plus(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "T17+"
|
||||
self.nominal_chips = 44
|
||||
9
miners/_types/antminer/X17/T17e.py
Normal file
9
miners/_types/antminer/X17/T17e.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class T17e(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "T17e"
|
||||
self.nominal_chips = 78
|
||||
8
miners/_types/antminer/X17/__init__.py
Normal file
8
miners/_types/antminer/X17/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from .S17 import S17
|
||||
from .S17_Plus import S17Plus
|
||||
from .S17_Pro import S17Pro
|
||||
from .S17e import S17e
|
||||
|
||||
from .T17 import T17
|
||||
from .T17_Plus import T17Plus
|
||||
from .T17e import T17e
|
||||
9
miners/_types/antminer/X19/S19.py
Normal file
9
miners/_types/antminer/X19/S19.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S19(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S19"
|
||||
self.nominal_chips = 76
|
||||
9
miners/_types/antminer/X19/S19_Pro.py
Normal file
9
miners/_types/antminer/X19/S19_Pro.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S19Pro(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S19 Pro"
|
||||
self.nominal_chips = 114
|
||||
9
miners/_types/antminer/X19/S19a.py
Normal file
9
miners/_types/antminer/X19/S19a.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S19a(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S19a"
|
||||
self.nominal_chips = 72
|
||||
9
miners/_types/antminer/X19/S19j.py
Normal file
9
miners/_types/antminer/X19/S19j.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S19j(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S19j"
|
||||
self.nominal_chips = 114
|
||||
9
miners/_types/antminer/X19/S19j_Pro.py
Normal file
9
miners/_types/antminer/X19/S19j_Pro.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S19jPro(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S19j Pro"
|
||||
self.nominal_chips = 126
|
||||
9
miners/_types/antminer/X19/T19.py
Normal file
9
miners/_types/antminer/X19/T19.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class T19(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "T19"
|
||||
self.nominal_chips = 76
|
||||
9
miners/_types/antminer/X19/__init__.py
Normal file
9
miners/_types/antminer/X19/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .S19 import S19
|
||||
from .S19_Pro import S19Pro
|
||||
|
||||
from .S19j import S19j
|
||||
from .S19j_Pro import S19jPro
|
||||
|
||||
from .S19a import S19a
|
||||
|
||||
from .T19 import T19
|
||||
9
miners/_types/antminer/X9/S9.py
Normal file
9
miners/_types/antminer/X9/S9.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class S9(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "S9"
|
||||
self.nominal_chips = 63
|
||||
9
miners/_types/antminer/X9/T9.py
Normal file
9
miners/_types/antminer/X9/T9.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class T9(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "T9"
|
||||
self.nominal_chips = 57
|
||||
2
miners/_types/antminer/X9/__init__.py
Normal file
2
miners/_types/antminer/X9/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .S9 import S9
|
||||
from .T9 import T9
|
||||
3
miners/_types/antminer/__init__.py
Normal file
3
miners/_types/antminer/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .X9 import *
|
||||
from .X17 import *
|
||||
from .X19 import *
|
||||
9
miners/_types/avalonminer/A10X/A1047.py
Normal file
9
miners/_types/avalonminer/A10X/A1047.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class Avalon1047(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "Avalon 1047"
|
||||
self.nominal_chips = 114
|
||||
9
miners/_types/avalonminer/A10X/A1066.py
Normal file
9
miners/_types/avalonminer/A10X/A1066.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class Avalon1066(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "Avalon 1066"
|
||||
self.nominal_chips = 114
|
||||
2
miners/_types/avalonminer/A10X/__init__.py
Normal file
2
miners/_types/avalonminer/A10X/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .A1047 import Avalon1047
|
||||
from .A1066 import Avalon1066
|
||||
8
miners/_types/avalonminer/A8X/A821.py
Normal file
8
miners/_types/avalonminer/A8X/A821.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class Avalon821(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "Avalon 821"
|
||||
8
miners/_types/avalonminer/A8X/A841.py
Normal file
8
miners/_types/avalonminer/A8X/A841.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class Avalon841(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "Avalon 841"
|
||||
2
miners/_types/avalonminer/A8X/__init__.py
Normal file
2
miners/_types/avalonminer/A8X/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .A821 import Avalon821
|
||||
from .A841 import Avalon841
|
||||
2
miners/_types/avalonminer/__init__.py
Normal file
2
miners/_types/avalonminer/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .A8X import *
|
||||
from .A10X import *
|
||||
9
miners/_types/whatsminer/M2X/M20S.py
Normal file
9
miners/_types/whatsminer/M2X/M20S.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M20S(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M20S"
|
||||
self.nominal_chips = 66
|
||||
9
miners/_types/whatsminer/M2X/M20S_Plus.py
Normal file
9
miners/_types/whatsminer/M2X/M20S_Plus.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M20SPlus(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M20S+"
|
||||
self.nominal_chips = 66
|
||||
9
miners/_types/whatsminer/M2X/M21.py
Normal file
9
miners/_types/whatsminer/M2X/M21.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M21(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M21"
|
||||
self.nominal_chips = 105
|
||||
9
miners/_types/whatsminer/M2X/M21S.py
Normal file
9
miners/_types/whatsminer/M2X/M21S.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M21S(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M21S"
|
||||
self.nominal_chips = 105
|
||||
9
miners/_types/whatsminer/M2X/M21S_Plus.py
Normal file
9
miners/_types/whatsminer/M2X/M21S_Plus.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M21SPlus(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M21S+"
|
||||
self.nominal_chips = 105
|
||||
6
miners/_types/whatsminer/M2X/__init__.py
Normal file
6
miners/_types/whatsminer/M2X/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .M20S import M20S
|
||||
from .M20S_Plus import M20SPlus
|
||||
|
||||
from .M21 import M21
|
||||
from .M21S import M21S
|
||||
from .M21S_Plus import M21SPlus
|
||||
8
miners/_types/whatsminer/M3X/M30S.py
Normal file
8
miners/_types/whatsminer/M3X/M30S.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M30S(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M30S"
|
||||
9
miners/_types/whatsminer/M3X/M30S_Plus.py
Normal file
9
miners/_types/whatsminer/M3X/M30S_Plus.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M30SPlus(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M30S+"
|
||||
self.nominal_chips = 156
|
||||
12
miners/_types/whatsminer/M3X/M30S_Plus_Plus.py
Normal file
12
miners/_types/whatsminer/M3X/M30S_Plus_Plus.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M30SPlusPlus(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M30S++"
|
||||
self.nominal_chips = 117
|
||||
|
||||
|
||||
# TODO: handle different chip counts, 111, 117,(128)
|
||||
8
miners/_types/whatsminer/M3X/M31S.py
Normal file
8
miners/_types/whatsminer/M3X/M31S.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M31S(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M31S"
|
||||
8
miners/_types/whatsminer/M3X/M31S_Plus.py
Normal file
8
miners/_types/whatsminer/M3X/M31S_Plus.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M31SPlus(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M31S+"
|
||||
9
miners/_types/whatsminer/M3X/M32S.py
Normal file
9
miners/_types/whatsminer/M3X/M32S.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from miners import BaseMiner
|
||||
|
||||
|
||||
class M32S(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M32S"
|
||||
self.nominal_chips = 78
|
||||
8
miners/_types/whatsminer/M3X/__init__.py
Normal file
8
miners/_types/whatsminer/M3X/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from .M30S import M30S
|
||||
from .M30S_Plus import M30SPlus
|
||||
from .M30S_Plus_Plus import M30SPlusPlus
|
||||
|
||||
from .M31S import M31S
|
||||
from .M31S_Plus import M31SPlus
|
||||
|
||||
from .M32S import M32S
|
||||
2
miners/_types/whatsminer/__init__.py
Normal file
2
miners/_types/whatsminer/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .M2X import *
|
||||
from .M3X import *
|
||||
@@ -1,118 +0,0 @@
|
||||
from miners import BaseMiner
|
||||
from API.bosminer import BOSMinerAPI
|
||||
import asyncssh
|
||||
import toml
|
||||
from config.bos import bos_config_convert, general_config_convert_bos
|
||||
|
||||
|
||||
class BOSMinerS9(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
api = BOSMinerAPI(ip)
|
||||
super().__init__(ip, api)
|
||||
self.config = None
|
||||
self.uname = 'root'
|
||||
self.pwd = 'admin'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"S9 - BOSminer: {str(self.ip)}"
|
||||
|
||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
||||
"""Create a new asyncssh connection"""
|
||||
conn = await asyncssh.connect(str(self.ip), known_hosts=None, username=self.uname, password=self.pwd,
|
||||
server_host_key_algs=['ssh-rsa'])
|
||||
# return created connection
|
||||
return conn
|
||||
|
||||
async def send_ssh_command(self, cmd: str) -> None:
|
||||
"""Sends SSH command to miner."""
|
||||
# creates result variable
|
||||
result = None
|
||||
|
||||
# runs the command on the miner
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
# attempt to run command up to 3 times
|
||||
for i in range(3):
|
||||
try:
|
||||
# save result of the command
|
||||
result = await conn.run(cmd)
|
||||
except Exception as e:
|
||||
print(f"{cmd} error: {e}")
|
||||
if i == 3:
|
||||
return
|
||||
continue
|
||||
|
||||
# let the user know the result of the command
|
||||
if result is not None:
|
||||
if result.stdout != "":
|
||||
print(result.stdout)
|
||||
if result.stderr != "":
|
||||
print("ERROR: " + result.stderr)
|
||||
elif result.stderr != "":
|
||||
print("ERROR: " + result.stderr)
|
||||
else:
|
||||
print(cmd)
|
||||
|
||||
async def fault_light_on(self) -> None:
|
||||
"""Sends command to turn on fault light on the miner."""
|
||||
await self.send_ssh_command('miner fault_light on')
|
||||
|
||||
async def fault_light_off(self) -> None:
|
||||
"""Sends command to turn off fault light on the miner."""
|
||||
await self.send_ssh_command('miner fault_light off')
|
||||
|
||||
async def restart_backend(self) -> None:
|
||||
"""Restart bosminer hashing process."""
|
||||
await self.send_ssh_command('/etc/init.d/bosminer restart')
|
||||
|
||||
async def reboot(self) -> None:
|
||||
"""Reboots power to the physical miner."""
|
||||
await self.send_ssh_command('/sbin/reboot')
|
||||
|
||||
async def get_config(self) -> None:
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
async with conn.start_sftp_client() as sftp:
|
||||
async with sftp.open('/etc/bosminer.toml') as file:
|
||||
toml_data = toml.loads(await file.read())
|
||||
cfg = await bos_config_convert(toml_data)
|
||||
self.config = cfg
|
||||
|
||||
async def get_hostname(self) -> str:
|
||||
"""Attempts to get hostname from miner."""
|
||||
try:
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
||||
return data.stdout.strip()
|
||||
except Exception as e:
|
||||
print(self.ip, e)
|
||||
return "BOSMiner Unknown"
|
||||
|
||||
async def send_config(self, yaml_config) -> None:
|
||||
"""Configures miner with yaml config."""
|
||||
toml_conf = await general_config_convert_bos(yaml_config)
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
async with conn.start_sftp_client() as sftp:
|
||||
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
|
||||
await file.write(toml_conf)
|
||||
await conn.run("/etc/init.d/bosminer restart")
|
||||
|
||||
async def get_bad_boards(self) -> list:
|
||||
"""Checks for and provides list of non working boards."""
|
||||
devs = await self.api.devdetails()
|
||||
bad = 0
|
||||
chains = devs['DEVDETAILS']
|
||||
for chain in chains:
|
||||
if chain['Chips'] == 0:
|
||||
bad += 1
|
||||
if bad > 0:
|
||||
return [str(self.ip), bad]
|
||||
|
||||
async def check_good_boards(self) -> str:
|
||||
"""Checks for and provides list for working boards."""
|
||||
devs = await self.api.devdetails()
|
||||
bad = 0
|
||||
chains = devs['DEVDETAILS']
|
||||
for chain in chains:
|
||||
if chain['Chips'] == 0:
|
||||
bad += 1
|
||||
if not bad > 0:
|
||||
return str(self.ip)
|
||||
@@ -1,118 +0,0 @@
|
||||
from miners import BaseMiner
|
||||
from API.bosminer import BOSMinerAPI
|
||||
import asyncssh
|
||||
import toml
|
||||
from config.bos import bos_config_convert, general_config_convert_bos
|
||||
|
||||
|
||||
class BOSminerX17(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
api = BOSMinerAPI(ip)
|
||||
super().__init__(ip, api)
|
||||
self.config = None
|
||||
self.uname = 'root'
|
||||
self.pwd = 'admin'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"X17 - BOSminer: {str(self.ip)}"
|
||||
|
||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
||||
"""Create a new asyncssh connection"""
|
||||
conn = await asyncssh.connect(str(self.ip), known_hosts=None, username=self.uname, password=self.pwd,
|
||||
server_host_key_algs=['ssh-rsa'])
|
||||
# return created connection
|
||||
return conn
|
||||
|
||||
async def send_ssh_command(self, cmd: str) -> None:
|
||||
"""Sends SSH command to miner."""
|
||||
# creates result variable
|
||||
result = None
|
||||
|
||||
# runs the command on the miner
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
# attempt to run command up to 3 times
|
||||
for i in range(3):
|
||||
try:
|
||||
# save result of the command
|
||||
result = await conn.run(cmd)
|
||||
except Exception as e:
|
||||
print(f"{cmd} error: {e}")
|
||||
if i == 3:
|
||||
return
|
||||
continue
|
||||
|
||||
# let the user know the result of the command
|
||||
if result is not None:
|
||||
if result.stdout != "":
|
||||
print(result.stdout)
|
||||
if result.stderr != "":
|
||||
print("ERROR: " + result.stderr)
|
||||
elif result.stderr != "":
|
||||
print("ERROR: " + result.stderr)
|
||||
else:
|
||||
print(cmd)
|
||||
|
||||
async def fault_light_on(self) -> None:
|
||||
"""Sends command to turn on fault light on the miner."""
|
||||
await self.send_ssh_command('miner fault_light on')
|
||||
|
||||
async def fault_light_off(self) -> None:
|
||||
"""Sends command to turn off fault light on the miner."""
|
||||
await self.send_ssh_command('miner fault_light off')
|
||||
|
||||
async def restart_backend(self) -> None:
|
||||
"""Restart bosminer hashing process."""
|
||||
await self.send_ssh_command('/etc/init.d/bosminer restart')
|
||||
|
||||
async def reboot(self) -> None:
|
||||
"""Reboots power to the physical miner."""
|
||||
await self.send_ssh_command('/sbin/reboot')
|
||||
|
||||
async def get_config(self) -> None:
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
async with conn.start_sftp_client() as sftp:
|
||||
async with sftp.open('/etc/bosminer.toml') as file:
|
||||
toml_data = toml.loads(await file.read())
|
||||
cfg = await bos_config_convert(toml_data)
|
||||
self.config = cfg
|
||||
|
||||
async def get_hostname(self) -> str:
|
||||
"""Attempts to get hostname from miner."""
|
||||
try:
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
||||
return data.stdout.strip()
|
||||
except Exception as e:
|
||||
print(self.ip, e)
|
||||
return "BOSMiner Unknown"
|
||||
|
||||
async def send_config(self, yaml_config) -> None:
|
||||
"""Configures miner with yaml config."""
|
||||
toml_conf = await general_config_convert_bos(yaml_config)
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
async with conn.start_sftp_client() as sftp:
|
||||
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
|
||||
await file.write(toml_conf)
|
||||
await conn.run("/etc/init.d/bosminer restart")
|
||||
|
||||
async def get_bad_boards(self) -> list:
|
||||
"""Checks for and provides list of non working boards."""
|
||||
devs = await self.api.devdetails()
|
||||
bad = 0
|
||||
chains = devs['DEVDETAILS']
|
||||
for chain in chains:
|
||||
if chain['Chips'] == 0:
|
||||
bad += 1
|
||||
if bad > 0:
|
||||
return [str(self.ip), bad]
|
||||
|
||||
async def check_good_boards(self) -> str:
|
||||
"""Checks for and provides list for working boards."""
|
||||
devs = await self.api.devdetails()
|
||||
bad = 0
|
||||
chains = devs['DEVDETAILS']
|
||||
for chain in chains:
|
||||
if chain['Chips'] == 0:
|
||||
bad += 1
|
||||
if not bad > 0:
|
||||
return str(self.ip)
|
||||
@@ -0,0 +1,4 @@
|
||||
from .bmminer import *
|
||||
from .bosminer import *
|
||||
from .cgminer import *
|
||||
from .hiveon import *
|
||||
|
||||
8
miners/antminer/bmminer/X17/S17.py
Normal file
8
miners/antminer/bmminer/X17/S17.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S17
|
||||
|
||||
|
||||
class BMMinerS17(BMMiner, S17):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X17/S17_Plus.py
Normal file
8
miners/antminer/bmminer/X17/S17_Plus.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S17Plus
|
||||
|
||||
|
||||
class BMMinerS17Plus(BMMiner, S17Plus):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X17/S17_Pro.py
Normal file
8
miners/antminer/bmminer/X17/S17_Pro.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S17Pro
|
||||
|
||||
|
||||
class BMMinerS17Pro(BMMiner, S17Pro):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X17/S17e.py
Normal file
8
miners/antminer/bmminer/X17/S17e.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S17e
|
||||
|
||||
|
||||
class BMMinerS17e(BMMiner, S17e):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X17/T17.py
Normal file
8
miners/antminer/bmminer/X17/T17.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import T17
|
||||
|
||||
|
||||
class BMMinerT17(BMMiner, T17):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X17/T17_Plus.py
Normal file
8
miners/antminer/bmminer/X17/T17_Plus.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import T17Plus
|
||||
|
||||
|
||||
class BMMinerT17Plus(BMMiner, T17Plus):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X17/T17e.py
Normal file
8
miners/antminer/bmminer/X17/T17e.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import T17e
|
||||
|
||||
|
||||
class BMMinerT17e(BMMiner, T17e):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X17/__init__.py
Normal file
8
miners/antminer/bmminer/X17/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from .S17 import BMMinerS17
|
||||
from .S17_Plus import BMMinerS17Plus
|
||||
from .S17_Pro import BMMinerS17Pro
|
||||
from .S17e import BMMinerS17e
|
||||
|
||||
from .T17 import BMMinerT17
|
||||
from .T17_Plus import BMMinerT17Plus
|
||||
from .T17e import BMMinerT17e
|
||||
8
miners/antminer/bmminer/X19/S19.py
Normal file
8
miners/antminer/bmminer/X19/S19.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S19
|
||||
|
||||
|
||||
class BMMinerS19(BMMiner, S19):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X19/S19_Pro.py
Normal file
8
miners/antminer/bmminer/X19/S19_Pro.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S19Pro
|
||||
|
||||
|
||||
class BMMinerS19Pro(BMMiner, S19Pro):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X19/S19a.py
Normal file
8
miners/antminer/bmminer/X19/S19a.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S19a
|
||||
|
||||
|
||||
class BMMinerS19a(BMMiner, S19a):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X19/S19j.py
Normal file
8
miners/antminer/bmminer/X19/S19j.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S19j
|
||||
|
||||
|
||||
class BMMinerS19j(BMMiner, S19j):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X19/S19j_Pro.py
Normal file
8
miners/antminer/bmminer/X19/S19j_Pro.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S19jPro
|
||||
|
||||
|
||||
class BMMinerS19jPro(BMMiner, S19jPro):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X19/T19.py
Normal file
8
miners/antminer/bmminer/X19/T19.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import T19
|
||||
|
||||
|
||||
class BMMinerT19(BMMiner, T19):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
9
miners/antminer/bmminer/X19/__init__.py
Normal file
9
miners/antminer/bmminer/X19/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .S19 import BMMinerS19
|
||||
from .S19_Pro import BMMinerS19Pro
|
||||
|
||||
from .S19j import BMMinerS19j
|
||||
from .S19j_Pro import BMMinerS19jPro
|
||||
|
||||
from .S19a import BMMinerS19a
|
||||
|
||||
from .T19 import BMMinerT19
|
||||
8
miners/antminer/bmminer/X9/S9.py
Normal file
8
miners/antminer/bmminer/X9/S9.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import S9
|
||||
|
||||
|
||||
class BMMinerS9(BMMiner, S9):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bmminer/X9/T9.py
Normal file
8
miners/antminer/bmminer/X9/T9.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BMMiner
|
||||
from miners._types import T9
|
||||
|
||||
|
||||
class BMMinerT9(BMMiner, T9):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
2
miners/antminer/bmminer/X9/__init__.py
Normal file
2
miners/antminer/bmminer/X9/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .S9 import BMMinerS9
|
||||
from .T9 import BMMinerT9
|
||||
3
miners/antminer/bmminer/__init__.py
Normal file
3
miners/antminer/bmminer/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .X9 import *
|
||||
from .X17 import *
|
||||
from .X19 import *
|
||||
8
miners/antminer/bosminer/X17/S17.py
Normal file
8
miners/antminer/bosminer/X17/S17.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BOSMiner
|
||||
from miners._types import S17
|
||||
|
||||
|
||||
class BOSMinerS17(BOSMiner, S17):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bosminer/X17/S17_Plus.py
Normal file
8
miners/antminer/bosminer/X17/S17_Plus.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BOSMiner
|
||||
from miners._types import S17Plus
|
||||
|
||||
|
||||
class BOSMinerS17Plus(BOSMiner, S17Plus):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bosminer/X17/S17_Pro.py
Normal file
8
miners/antminer/bosminer/X17/S17_Pro.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BOSMiner
|
||||
from miners._types import S17Pro
|
||||
|
||||
|
||||
class BOSMinerS17Pro(BOSMiner, S17Pro):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bosminer/X17/S17e.py
Normal file
8
miners/antminer/bosminer/X17/S17e.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BOSMiner
|
||||
from miners._types import S17e
|
||||
|
||||
|
||||
class BOSMinerS17e(BOSMiner, S17e):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bosminer/X17/T17.py
Normal file
8
miners/antminer/bosminer/X17/T17.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BOSMiner
|
||||
from miners._types import T17
|
||||
|
||||
|
||||
class BOSMinerT17(BOSMiner, T17):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bosminer/X17/T17_Plus.py
Normal file
8
miners/antminer/bosminer/X17/T17_Plus.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BOSMiner
|
||||
from miners._types import T17Plus
|
||||
|
||||
|
||||
class BOSMinerT17Plus(BOSMiner, T17Plus):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
8
miners/antminer/bosminer/X17/T17e.py
Normal file
8
miners/antminer/bosminer/X17/T17e.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from miners._backends import BOSMiner
|
||||
from miners._types import T17e
|
||||
|
||||
|
||||
class BOSMinerT17e(BOSMiner, T17e):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user