Compare commits
262 Commits
cfg_util-b
...
v0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
# Ignore VENV
|
||||
venv
|
||||
|
||||
# Ignore builds
|
||||
build
|
||||
|
||||
# Ignore github files
|
||||
.github
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import json
|
||||
import ipaddress
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
|
||||
class APIError(Exception):
|
||||
@@ -41,36 +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
|
||||
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}") instead.""",
|
||||
APIWarning)
|
||||
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
|
||||
@@ -80,11 +89,17 @@ If you are sure you want to use this command please use API.send_command("{item}
|
||||
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
|
||||
@@ -92,7 +107,7 @@ If you are sure you want to use this command please use API.send_command("{item}
|
||||
# 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
|
||||
@@ -101,7 +116,7 @@ If you are sure you want to use this command please use API.send_command("{item}
|
||||
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
|
||||
@@ -115,7 +130,7 @@ If you are sure you want to use this command please use API.send_command("{item}
|
||||
break
|
||||
data += d
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.warning(f"{self.ip}: API Command Error: {e}")
|
||||
|
||||
data = self.load_api_data(data)
|
||||
|
||||
@@ -123,15 +138,18 @@ If you are sure you want to use this command please use API.send_command("{item}
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
# validate the command succeeded
|
||||
validation = self.validate_command_output(data)
|
||||
if not validation[0]:
|
||||
raise APIError(validation[1])
|
||||
# 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) -> tuple[bool, str | None]:
|
||||
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
|
||||
@@ -158,24 +176,26 @@ If you are sure you want to use this command please use API.send_command("{item}
|
||||
@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("[,{", "[{")
|
||||
# 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")
|
||||
|
||||
141
API/bosminer.py
141
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,77 +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.
|
||||
async def estats(self, old: bool = False) -> dict:
|
||||
"""Get stats of each device/pool with more than 1 getwork,
|
||||
ignoring zombie devices.
|
||||
|
||||
Returns a dict containing stats for all device/pool with more than 1 getwork,
|
||||
: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")
|
||||
|
||||
@@ -167,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")
|
||||
|
||||
545
API/btminer.py
545
API/btminer.py
@@ -1,32 +1,45 @@
|
||||
from API import BaseMinerAPI, APIError
|
||||
|
||||
from passlib.handlers.md5_crypt 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,47 +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
|
||||
validation = self.validate_command_output(data)
|
||||
if not validation[0]:
|
||||
raise APIError(validation[1])
|
||||
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()
|
||||
|
||||
@@ -173,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)
|
||||
@@ -194,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()
|
||||
@@ -205,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"}
|
||||
@@ -224,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()
|
||||
@@ -237,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()
|
||||
@@ -287,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()
|
||||
@@ -300,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()
|
||||
@@ -313,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()
|
||||
@@ -362,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()
|
||||
@@ -375,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()
|
||||
@@ -388,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()
|
||||
@@ -401,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()
|
||||
@@ -414,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"
|
||||
@@ -462,82 +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):
|
||||
"""
|
||||
API 'get_version' command.
|
||||
"""Get version data for the miner.
|
||||
|
||||
Returns a dict containing version information.
|
||||
: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):
|
||||
"""
|
||||
API 'get_miner_info' command.
|
||||
"""Get general miner info.
|
||||
|
||||
Returns a dict containing general miner info.
|
||||
:return: General miner 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)
|
||||
|
||||
|
||||
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"]
|
||||
184
README.md
184
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,7 +12,7 @@ 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()
|
||||
@@ -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.parse_data 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.parse_data 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,8 +101,10 @@ 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>
|
||||
@@ -105,12 +112,13 @@ Or generate a miner directly without the factory:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from miners.bosminer import BOSminer
|
||||
from cfg_util.func.parse_data import safe_parse_api_data
|
||||
from miners.bosminer import BOSMiner
|
||||
from tools.cfg_util_old.func.parse_data import safe_parse_api_data
|
||||
|
||||
|
||||
async def get_miner_hashrate(ip: str):
|
||||
# Create a BOSminer miner object
|
||||
miner = BOSminer(ip)
|
||||
miner = BOSMiner(ip)
|
||||
# Get the API data
|
||||
summary = await miner.api.summary()
|
||||
# safe_parse_api_data parses the data from a miner API
|
||||
@@ -118,8 +126,10 @@ 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>
|
||||
@@ -128,7 +138,8 @@ Or finally, just get the API directly:
|
||||
```python
|
||||
import asyncio
|
||||
from API.bosminer import BOSMinerAPI
|
||||
from cfg_util.func.parse_data 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):
|
||||
# Create a BOSminerAPI object
|
||||
@@ -141,6 +152,157 @@ 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")))
|
||||
```
|
||||
|
||||
|
||||
Now that you know that, lets move on to some common API functions that you might want to use.
|
||||
|
||||
### Common commands:
|
||||
* Getting pool data:
|
||||
|
||||
```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_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 = await safe_parse_api_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
|
||||
from tools.cfg_util_old.func.parse_data import safe_parse_api_data
|
||||
|
||||
|
||||
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, "Temperature")
|
||||
print(data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.new_event_loop().run_until_complete(
|
||||
get_miner_temperature_data(str("192.168.1.69")))
|
||||
```
|
||||
|
||||
* 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
|
||||
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_power_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)
|
||||
# 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 = await safe_parse_api_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 = await safe_parse_api_data(summary, 'SUMMARY', 0, "Power")
|
||||
|
||||
print(data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
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 = await safe_parse_api_data(api_data, "pools", 0, "POOLS", 0,
|
||||
"User")
|
||||
print(user)
|
||||
if "summary" in api_data.keys():
|
||||
hashrate = await safe_parse_api_data(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")))
|
||||
```
|
||||
|
||||
4
board_util.py
Normal file
4
board_util.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from tools.bad_board_util import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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,68 +0,0 @@
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import aiofiles
|
||||
import toml
|
||||
|
||||
from cfg_util.func.ui import update_ui_with_data
|
||||
from cfg_util.layout import window
|
||||
from config.bos import bos_config_convert, general_config_convert_bos
|
||||
|
||||
|
||||
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 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", "")
|
||||
@@ -1,301 +0,0 @@
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import time
|
||||
import warnings
|
||||
|
||||
from API import APIError
|
||||
from cfg_util.func.parse_data import safe_parse_api_data
|
||||
from cfg_util.func.ui import update_ui_with_data, update_prog_bar, set_progress_bar_len
|
||||
from cfg_util.layout import window
|
||||
from cfg_util.miner_factory import miner_factory
|
||||
from config.bos import bos_config_convert
|
||||
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
|
||||
|
||||
|
||||
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 scan_network(network):
|
||||
await update_ui_with_data("status", "Scanning")
|
||||
await update_ui_with_data("ip_count", "")
|
||||
await update_ui_with_data("hr_total", "")
|
||||
window["ip_table"].update([])
|
||||
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 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 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["model"], data_point["host"], str(data_point['TH/s']) + " TH/s", data_point["temp"],
|
||||
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[3].replace(" TH/s", "")) if not item[3] == '' else 0 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 scan_and_get_data(network):
|
||||
await update_ui_with_data("status", "Scanning")
|
||||
network_size = len(network)
|
||||
miner_generator = network.scan_network_generator()
|
||||
await set_progress_bar_len(3 * 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)))
|
||||
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in miners])
|
||||
ip_table_data = window["ip_table"].Values
|
||||
ordered_all_ips = [item[0] for item in ip_table_data]
|
||||
progress_bar_len += (network_size - len(miners))
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
await update_ui_with_data("status", "Getting 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["model"], data_point["host"], str(data_point['TH/s']) + " TH/s", data_point["temp"],
|
||||
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[3].replace(" TH/s", "")) for item in window["ip_table"].Values if not item[3] == '']
|
||||
total_hr = round(sum(hashrate_list), 2)
|
||||
await update_ui_with_data("hr_total", 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)
|
||||
warnings.filterwarnings('ignore')
|
||||
try:
|
||||
miner_data = await miner.api.multicommand("summary", "devs", "temps", "tunerstatus", "pools", "stats")
|
||||
except APIError:
|
||||
return {'TH/s': "Unknown", 'IP': str(miner.ip), 'host': "Unknown", 'user': "Unknown", 'wattage': 0}
|
||||
host = await miner.get_hostname()
|
||||
model = await miner.get_model()
|
||||
temps = 0
|
||||
if "summary" in miner_data.keys():
|
||||
if "Temperature" in miner_data['summary'][0]['SUMMARY'][0].keys():
|
||||
if not round(miner_data['summary'][0]['SUMMARY'][0]["Temperature"]) == 0:
|
||||
temps = miner_data['summary'][0]['SUMMARY'][0]["Temperature"]
|
||||
if 'MHS av' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
||||
th5s = round(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'MHS av') / 1000000, 2)
|
||||
elif 'GHS av' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
||||
if not miner_data['summary'][0]['SUMMARY'][0]['GHS av'] == "":
|
||||
th5s = round(float(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'GHS av')) / 1000,
|
||||
2)
|
||||
else:
|
||||
th5s = 0
|
||||
else:
|
||||
th5s = 0
|
||||
else:
|
||||
th5s = 0
|
||||
if "temps" in miner_data.keys() and not miner_data["temps"][0]['TEMPS'] == []:
|
||||
if "Chip" in miner_data["temps"][0]['TEMPS'][0].keys():
|
||||
for board in miner_data["temps"][0]['TEMPS']:
|
||||
if board["Chip"] is not None and not board["Chip"] == 0.0:
|
||||
temps = board["Chip"]
|
||||
if "devs" in miner_data.keys() and not miner_data["devs"][0]['DEVS'] == []:
|
||||
if "Chip Temp Avg" in miner_data["devs"][0]['DEVS'][0].keys():
|
||||
for board in miner_data["devs"][0]['DEVS']:
|
||||
if board['Chip Temp Avg'] is not None and not board['Chip Temp Avg'] == 0.0:
|
||||
temps = board['Chip Temp Avg']
|
||||
|
||||
if "stats" in miner_data.keys() and not miner_data["stats"][0]['STATS'] == []:
|
||||
for temp in ["temp2", "temp1", "temp3"]:
|
||||
if temp in miner_data["stats"][0]['STATS'][1].keys():
|
||||
if miner_data["stats"][0]['STATS'][1][temp] is not None and not miner_data["stats"][0]['STATS'][1][
|
||||
temp] == 0.0:
|
||||
temps = miner_data["stats"][0]['STATS'][1][temp]
|
||||
|
||||
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"
|
||||
|
||||
if "tunerstatus" in miner_data.keys():
|
||||
wattage = await safe_parse_api_data(miner_data, "tunerstatus", 0, 'TUNERSTATUS', 0, "PowerLimit")
|
||||
elif "Power" in miner_data["summary"][0]["SUMMARY"][0].keys():
|
||||
wattage = await safe_parse_api_data(miner_data, "summary", 0, 'SUMMARY', 0, "Power")
|
||||
else:
|
||||
wattage = 0
|
||||
return {'TH/s': th5s, 'IP': str(miner.ip), 'model': model,
|
||||
'temp': round(temps), '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))
|
||||
@@ -1,72 +0,0 @@
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
from cfg_util.layout import window
|
||||
|
||||
import pyperclip
|
||||
|
||||
|
||||
def copy_from_table(table):
|
||||
selection = table.selection()
|
||||
copy_values = []
|
||||
for each in selection:
|
||||
try:
|
||||
value = table.item(each)["values"][0]
|
||||
copy_values.append(str(value))
|
||||
except:
|
||||
pass
|
||||
copy_string = "\n".join(copy_values)
|
||||
pyperclip.copy(copy_string)
|
||||
|
||||
|
||||
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 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", str(data_list[0][index])):
|
||||
new_list = sorted(data_list, key=lambda x: int(x[index].replace(" W", "")))
|
||||
if data_list == new_list:
|
||||
new_list = sorted(data_list, reverse=True, key=lambda x: int(x[index].replace(" W", "")))
|
||||
|
||||
# hashrate
|
||||
elif re.match("[0-9]*\.?[0-9]* TH\/s", str(data_list[0][index])):
|
||||
new_list = sorted(data_list, key=lambda x: float(x[index].replace(" TH/s", "")))
|
||||
if data_list == new_list:
|
||||
new_list = sorted(data_list, reverse=True, 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]?)",
|
||||
str(data_list[0][index])):
|
||||
new_list = sorted(data_list, key=lambda x: ipaddress.ip_address(x[index]))
|
||||
if data_list == new_list:
|
||||
new_list = sorted(data_list, reverse=True, key=lambda x: ipaddress.ip_address(x[index]))
|
||||
|
||||
# everything else, hostname, temp, and user
|
||||
else:
|
||||
new_list = sorted(data_list, key=lambda x: x[index])
|
||||
if data_list == new_list:
|
||||
new_list = sorted(data_list, reverse=True, key=lambda x: x[index])
|
||||
|
||||
await update_ui_with_data("ip_table", new_list)
|
||||
await update_ui_with_data("status", "")
|
||||
@@ -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,87 +0,0 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import PySimpleGUI as sg
|
||||
|
||||
from cfg_util.layout import window, generate_config_layout
|
||||
from cfg_util.func.miners import scan_network, send_config, miner_light, get_data, generate_config, import_config, \
|
||||
scan_and_get_data
|
||||
from cfg_util.func.files import import_iplist, import_config_file, export_iplist, export_config_file
|
||||
from cfg_util.func.ui import sort_data, copy_from_table
|
||||
|
||||
from network import MinerNetwork
|
||||
|
||||
import webbrowser
|
||||
|
||||
|
||||
async def ui():
|
||||
window.read(timeout=0)
|
||||
table = window["ip_table"].Widget
|
||||
table.bind("<Control-Key-c>", lambda x: copy_from_table(table))
|
||||
while True:
|
||||
event, value = window.read(timeout=10)
|
||||
if event in (None, 'Close', sg.WIN_CLOSED):
|
||||
sys.exit()
|
||||
if isinstance(event, tuple):
|
||||
if len(window["ip_table"].Values) > 0:
|
||||
if event[0] == 'ip_table':
|
||||
if event[2][0] == -1:
|
||||
await sort_data(event[2][1])
|
||||
if event == 'open_in_web':
|
||||
for row in value["ip_table"]:
|
||||
webbrowser.open("http://" + window["ip_table"].Values[row][0])
|
||||
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":
|
||||
if len(window["ip_table"].Values) == 0:
|
||||
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_and_get_data(miner_network))
|
||||
else:
|
||||
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 == "__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)
|
||||
44
make_board_tool_exe.py
Normal file
44
make_board_tool_exe.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Make a build of the board tool.
|
||||
|
||||
Usage: make_board_tool_exe.py build
|
||||
|
||||
The build will show up in the build directory.
|
||||
"""
|
||||
import datetime
|
||||
import sys
|
||||
import os
|
||||
from cx_Freeze import setup, Executable
|
||||
|
||||
base = None
|
||||
if sys.platform == "win32":
|
||||
base = "Win32GUI"
|
||||
|
||||
version = datetime.datetime.now()
|
||||
version = version.strftime("%y.%m.%d")
|
||||
print(version)
|
||||
|
||||
|
||||
setup(
|
||||
name="UpstreamBoardUtil.exe",
|
||||
version=version,
|
||||
description="Upstream Data Board Utility Build",
|
||||
options={
|
||||
"build_exe": {
|
||||
"build_exe": f"{os.getcwd()}\\build\\board_util\\UpstreamBoardUtil-{version}-{sys.platform}\\",
|
||||
"include_files": [
|
||||
os.path.join(os.getcwd(), "settings/settings.toml"),
|
||||
],
|
||||
"include_msvcr": True,
|
||||
"add_to_path": True,
|
||||
},
|
||||
},
|
||||
executables=[
|
||||
Executable(
|
||||
"board_util.py",
|
||||
base=base,
|
||||
icon="icon.ico",
|
||||
target_name="UpstreamBoardUtil.exe",
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -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/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")]
|
||||
)
|
||||
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",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
@@ -4,11 +4,104 @@ 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:
|
||||
def __init__(
|
||||
self,
|
||||
ip: str,
|
||||
api: BMMinerAPI or BOSMinerAPI or CGMinerAPI or BTMinerAPI or UnknownAPI,
|
||||
) -> None:
|
||||
self.ip = ipaddress.ip_address(ip)
|
||||
self.uname = None
|
||||
self.pwd = None
|
||||
self.api = api
|
||||
self.api_type = None
|
||||
self.model = None
|
||||
self.light = None
|
||||
self.hostname = None
|
||||
self.nominal_chips = 1
|
||||
|
||||
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="admin",
|
||||
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:
|
||||
logging.warning(f"Connection refused: {self}")
|
||||
return None
|
||||
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_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
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from miners.bosminer import BOSminer
|
||||
import logging
|
||||
import toml
|
||||
|
||||
from miners.bosminer import BOSMiner
|
||||
from config.bos import general_config_convert_bos
|
||||
|
||||
|
||||
class BOSMinerS9(BOSminer):
|
||||
class BOSMinerS9(BOSMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.model = "S9"
|
||||
|
||||
11
miners/antminer/T9/bmminer.py
Normal file
11
miners/antminer/T9/bmminer.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from miners.bmminer import BMMiner
|
||||
|
||||
|
||||
class BMMinerT9(BMMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.model = "T9"
|
||||
self.api_type = "BMMiner"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BMMinerT9: {str(self.ip)}"
|
||||
11
miners/antminer/T9/cgminer.py
Normal file
11
miners/antminer/T9/cgminer.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from miners.cgminer import CGMiner
|
||||
|
||||
|
||||
class CGMinerT9(CGMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.model = "T9"
|
||||
self.api_type = "CGMiner"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CGMinerT9: {str(self.ip)}"
|
||||
@@ -0,0 +1,48 @@
|
||||
from miners.bmminer import BMMiner
|
||||
|
||||
|
||||
class HiveonT9(BMMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.model = "T9"
|
||||
self.api_type = "Hiveon"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"HiveonT9: {str(self.ip)}"
|
||||
|
||||
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
|
||||
|
||||
@@ -6,4 +6,4 @@ class BMMinerX17(BMMiner):
|
||||
super().__init__(ip)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CGMinerX17: {str(self.ip)}"
|
||||
return f"BMMinerX17: {str(self.ip)}"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from miners.bosminer import BOSminer
|
||||
from miners.bosminer import BOSMiner
|
||||
|
||||
|
||||
class BOSMinerX17(BOSminer):
|
||||
class BOSMinerX17(BOSMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.api_type = "BOSMiner"
|
||||
self.nominal_chips = 65
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BOSminerX17: {str(self.ip)}"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from miners.bmminer import BMMiner
|
||||
import logging
|
||||
|
||||
|
||||
class BMMinerX19(BMMiner):
|
||||
@@ -10,9 +11,12 @@ class BMMinerX19(BMMiner):
|
||||
|
||||
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.version()
|
||||
if version_data:
|
||||
self.model = version_data["VERSION"][0]["Type"].replace("Antminer ", "")
|
||||
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
|
||||
|
||||
11
miners/antminer/X19/bosminer.py
Normal file
11
miners/antminer/X19/bosminer.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from miners.bosminer import BOSMiner
|
||||
|
||||
|
||||
class BOSMinerX19(BOSMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.api_type = "BOSMiner"
|
||||
self.nominal_chips = 114
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BOSminerX19: {str(self.ip)}"
|
||||
@@ -1,4 +1,5 @@
|
||||
from miners.cgminer import CGMiner
|
||||
import logging
|
||||
|
||||
|
||||
class CGMinerX19(CGMiner):
|
||||
@@ -11,9 +12,12 @@ class CGMinerX19(CGMiner):
|
||||
|
||||
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.version()
|
||||
if version_data:
|
||||
self.model = version_data["VERSION"][0]["Type"].replace("Antminer ", "")
|
||||
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
|
||||
|
||||
61
miners/avalonminer/Avalon10/__init__.py
Normal file
61
miners/avalonminer/Avalon10/__init__.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from miners.cgminer import CGMiner
|
||||
import logging
|
||||
|
||||
|
||||
class CGMinerAvalon10(CGMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.model = "Avalon 10"
|
||||
self.api_type = "CGMiner"
|
||||
self.nominal_chips = 114
|
||||
|
||||
async def get_hostname(self):
|
||||
try:
|
||||
devdetails = await self.api.devdetails()
|
||||
if devdetails:
|
||||
if len(devdetails.get("DEVDETAILS")) > 0:
|
||||
if "Name" in devdetails["DEVDETAILS"][0]:
|
||||
host = devdetails["DEVDETAILS"][0]["Name"]
|
||||
logging.debug(f"Found hostname for {self.ip}: {host}")
|
||||
return host
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||
return "?"
|
||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||
return "?"
|
||||
|
||||
async def get_board_info(self):
|
||||
boards_chips = 0
|
||||
logging.debug(f"{self}: Getting board info.")
|
||||
stats = await self.api.stats()
|
||||
if not stats.get("STATS") and not stats.get("STATS") == []:
|
||||
print("stats error", stats)
|
||||
return {0: [], 1: [], 2: []}
|
||||
stats = stats["STATS"][0]
|
||||
for key in stats.keys():
|
||||
if key.startswith("MM") and not stats[key] == 1:
|
||||
data = stats[key]
|
||||
for line in data.split("]"):
|
||||
if "TA[" in line:
|
||||
total_chips = line.replace("TA[", "")
|
||||
boards_chips = round(int(total_chips)/3)
|
||||
boards = {}
|
||||
for board in [0, 1, 2]:
|
||||
if not boards_chips == self.nominal_chips:
|
||||
nominal = False
|
||||
else:
|
||||
nominal = True
|
||||
boards[board] = []
|
||||
boards[board].append({
|
||||
"chain": board,
|
||||
"chip_count": boards_chips,
|
||||
"chip_status": "o" * boards_chips,
|
||||
"nominal": nominal,
|
||||
})
|
||||
return boards
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
177
miners/avalonminer/Avalon8/__init__.py
Normal file
177
miners/avalonminer/Avalon8/__init__.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from miners.cgminer import CGMiner
|
||||
import re
|
||||
|
||||
|
||||
class CGMinerAvalon8(CGMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.model = "Avalon 8"
|
||||
self.api_type = "CGMiner"
|
||||
self.pattern = re.compile(
|
||||
r"Ver\[(?P<Ver>[-0-9A-Fa-f+]+)\]\s"
|
||||
"DNA\[(?P<DNA>[0-9A-Fa-f]+)\]\s"
|
||||
"Elapsed\[(?P<Elapsed>[-0-9]+)\]\s"
|
||||
"MW\[(?P<MW>[-\s0-9]+)\]\s"
|
||||
"LW\[(?P<LW>[-0-9]+)\]\s"
|
||||
"MH\[(?P<MH>[-\s0-9]+)\]\s"
|
||||
"HW\[(?P<HW>[-0-9]+)\]\s"
|
||||
"Temp\[(?P<Temp>[0-9]+)\]\s"
|
||||
"TMax\[(?P<TMax>[0-9]+)\]\s"
|
||||
"Fan\[(?P<Fan>[0-9]+)\]\s"
|
||||
"FanR\[(?P<FanR>[0-9]+)%\]\s"
|
||||
"Vi\[(?P<Vi>[-\s0-9]+)\]\s"
|
||||
"Vo\[(?P<Vo>[-\s0-9]+)\]\s"
|
||||
"("
|
||||
"PLL0\[(?P<PLL0>[-\s0-9]+)\]\s"
|
||||
"PLL1\[(?P<PLL1>[-\s0-9]+)\]\s"
|
||||
"PLL2\[(?P<PLL2>[-\s0-9]+)\]\s"
|
||||
"PLL3\[(?P<PLL3>[-\s0-9]+)\]\s"
|
||||
")?"
|
||||
"GHSmm\[(?P<GHSmm>[-.0-9]+)\]\s"
|
||||
"WU\[(?P<WU>[-.0-9]+)\]\s"
|
||||
"Freq\[(?P<Freq>[.0-9]+)\]\s"
|
||||
"PG\[(?P<PG>[0-9]+)\]\s"
|
||||
"Led\[(?P<LED>0|1)\]\s"
|
||||
"MW0\[(?P<MW0>[0-9\s]+)\]\s"
|
||||
"MW1\[(?P<MW1>[0-9\s]+)\]\s"
|
||||
"MW2\[(?P<MW2>[0-9\s]+)\]\s"
|
||||
"MW3\[(?P<MW3>[0-9\s]+)\]\s"
|
||||
"TA\[(?P<TA>[0-9]+)\]\s"
|
||||
"ECHU\[(?P<ECHU>[0-9\s]+)\]\s"
|
||||
"ECMM\[(?P<ECMM>[0-9]+)\]\s.*"
|
||||
"FAC0\[(?P<FAC0>[-0-9]+)\]\s"
|
||||
"OC\[(?P<OC>[0-9]+)\]\s"
|
||||
"SF0\[(?P<SF0>[-\s0-9]+)\]\s"
|
||||
"SF1\[(?P<SF1>[-\s0-9]+)\]\s"
|
||||
"SF2\[(?P<SF2>[-\s0-9]+)\]\s"
|
||||
"SF3\[(?P<SF3>[-\s0-9]+)\]\s"
|
||||
"PMUV\[(?P<PMUV>[-\s\S*]+)\]\s"
|
||||
"PVT_T0\[(?P<PVT_T0>[-0-9\s]+)\]\s"
|
||||
"PVT_T1\[(?P<PVT_T1>[-0-9\s]+)\]\s"
|
||||
"PVT_T2\[(?P<PVT_T2>[-0-9\s]+)\]\s"
|
||||
"PVT_T3\[(?P<PVT_T3>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_0\[(?P<PVT_V0_0>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_1\[(?P<PVT_V0_1>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_2\[(?P<PVT_V0_2>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_3\[(?P<PVT_V0_3>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_4\[(?P<PVT_V0_4>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_5\[(?P<PVT_V0_5>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_6\[(?P<PVT_V0_6>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_7\[(?P<PVT_V0_7>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_8\[(?P<PVT_V0_8>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_9\[(?P<PVT_V0_9>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_10\[(?P<PVT_V0_10>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_11\[(?P<PVT_V0_11>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_12\[(?P<PVT_V0_12>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_13\[(?P<PVT_V0_13>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_14\[(?P<PVT_V0_14>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_15\[(?P<PVT_V0_15>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_16\[(?P<PVT_V0_16>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_17\[(?P<PVT_V0_17>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_18\[(?P<PVT_V0_18>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_19\[(?P<PVT_V0_19>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_20\[(?P<PVT_V0_20>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_21\[(?P<PVT_V0_21>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_22\[(?P<PVT_V0_22>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_23\[(?P<PVT_V0_23>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_24\[(?P<PVT_V0_24>[-0-9\s]+)\]\s"
|
||||
"PVT_V0_25\[(?P<PVT_V0_25>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_0\[(?P<PVT_V1_0>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_1\[(?P<PVT_V1_1>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_2\[(?P<PVT_V1_2>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_3\[(?P<PVT_V1_3>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_4\[(?P<PVT_V1_4>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_5\[(?P<PVT_V1_5>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_6\[(?P<PVT_V1_6>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_7\[(?P<PVT_V1_7>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_8\[(?P<PVT_V1_8>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_9\[(?P<PVT_V1_9>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_10\[(?P<PVT_V1_10>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_11\[(?P<PVT_V1_11>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_12\[(?P<PVT_V1_12>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_13\[(?P<PVT_V1_13>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_14\[(?P<PVT_V1_14>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_15\[(?P<PVT_V1_15>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_16\[(?P<PVT_V1_16>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_17\[(?P<PVT_V1_17>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_18\[(?P<PVT_V1_18>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_19\[(?P<PVT_V1_19>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_20\[(?P<PVT_V1_20>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_21\[(?P<PVT_V1_21>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_22\[(?P<PVT_V1_22>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_23\[(?P<PVT_V1_23>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_24\[(?P<PVT_V1_24>[-0-9\s]+)\]\s"
|
||||
"PVT_V1_25\[(?P<PVT_V1_25>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_0\[(?P<PVT_V2_0>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_1\[(?P<PVT_V2_1>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_2\[(?P<PVT_V2_2>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_3\[(?P<PVT_V2_3>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_4\[(?P<PVT_V2_4>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_5\[(?P<PVT_V2_5>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_6\[(?P<PVT_V2_6>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_7\[(?P<PVT_V2_7>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_8\[(?P<PVT_V2_8>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_9\[(?P<PVT_V2_9>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_10\[(?P<PVT_V2_10>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_11\[(?P<PVT_V2_11>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_12\[(?P<PVT_V2_12>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_13\[(?P<PVT_V2_13>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_14\[(?P<PVT_V2_14>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_15\[(?P<PVT_V2_15>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_16\[(?P<PVT_V2_16>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_17\[(?P<PVT_V2_17>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_18\[(?P<PVT_V2_18>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_19\[(?P<PVT_V2_19>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_20\[(?P<PVT_V2_20>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_21\[(?P<PVT_V2_21>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_22\[(?P<PVT_V2_22>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_23\[(?P<PVT_V2_23>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_24\[(?P<PVT_V2_24>[-0-9\s]+)\]\s"
|
||||
"PVT_V2_25\[(?P<PVT_V2_25>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_0\[(?P<PVT_V3_0>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_1\[(?P<PVT_V3_1>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_2\[(?P<PVT_V3_2>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_3\[(?P<PVT_V3_3>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_4\[(?P<PVT_V3_4>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_5\[(?P<PVT_V3_5>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_6\[(?P<PVT_V3_6>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_7\[(?P<PVT_V3_7>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_8\[(?P<PVT_V3_8>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_9\[(?P<PVT_V3_9>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_10\[(?P<PVT_V3_10>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_11\[(?P<PVT_V3_11>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_12\[(?P<PVT_V3_12>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_13\[(?P<PVT_V3_13>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_14\[(?P<PVT_V3_14>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_15\[(?P<PVT_V3_15>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_16\[(?P<PVT_V3_16>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_17\[(?P<PVT_V3_17>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_18\[(?P<PVT_V3_18>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_19\[(?P<PVT_V3_19>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_20\[(?P<PVT_V3_20>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_21\[(?P<PVT_V3_21>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_22\[(?P<PVT_V3_22>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_23\[(?P<PVT_V3_23>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_24\[(?P<PVT_V3_24>[-0-9\s]+)\]\s"
|
||||
"PVT_V3_25\[(?P<PVT_V3_25>[-0-9\s]+)\]\s"
|
||||
"FM\[(?P<FM>[0-9]+)\]\s"
|
||||
"CRC\[(?P<CRC>[0-9\s]+)\]",
|
||||
re.X,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CGMinerAvalon8: {str(self.ip)}"
|
||||
|
||||
def parse_estats(self, estats):
|
||||
for estat in estats:
|
||||
for key in estat:
|
||||
if key[:5] == "MM ID":
|
||||
self._parse_estat(estat, key)
|
||||
|
||||
def _parse_estat(self, estat, key):
|
||||
module = estat[key]
|
||||
module_info = re.match(self.pattern, module)
|
||||
if not module_info:
|
||||
return None
|
||||
module_info = module_info.groupdict()
|
||||
print(module_info)
|
||||
11
miners/avalonminer/__init__.py
Normal file
11
miners/avalonminer/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from miners.cgminer import CGMiner
|
||||
|
||||
|
||||
class CGMinerAvalon(CGMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.model = "Avalon"
|
||||
self.api_type = "CGMiner"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CGMinerAvalon: {str(self.ip)}"
|
||||
@@ -1,36 +1,231 @@
|
||||
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:
|
||||
api = BMMinerAPI(ip)
|
||||
self.model = None
|
||||
super().__init__(ip, api)
|
||||
self.model = None
|
||||
self.config = None
|
||||
self.uname = "root"
|
||||
self.pwd = "admin"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BMMiner: {str(self.ip)}"
|
||||
|
||||
async def get_model(self):
|
||||
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:
|
||||
return "?"
|
||||
"""Get miner hostname.
|
||||
|
||||
async def send_config(self, _):
|
||||
return None # ignore for now
|
||||
: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()
|
||||
|
||||
async def restart_backend(self) -> None:
|
||||
return None # Murray
|
||||
# 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 reboot(self) -> None:
|
||||
return None # Murray
|
||||
async def send_ssh_command(self, cmd: str) -> str or None:
|
||||
"""Send a command to the miner over ssh.
|
||||
|
||||
async def get_config(self) -> None:
|
||||
return None # Murray
|
||||
: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,
|
||||
"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 5s")
|
||||
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
|
||||
|
||||
@@ -1,128 +1,350 @@
|
||||
from miners import BaseMiner
|
||||
from API.bosminer import BOSMinerAPI
|
||||
import asyncssh
|
||||
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):
|
||||
class BOSMiner(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
api = BOSMinerAPI(ip)
|
||||
super().__init__(ip, api)
|
||||
self.model = None
|
||||
self.config = None
|
||||
self.uname = 'root'
|
||||
self.pwd = 'admin'
|
||||
self.version = None
|
||||
self.uname = "root"
|
||||
self.pwd = "admin"
|
||||
self.nominal_chips = 63
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"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) -> str or None:
|
||||
"""Send a command to the miner over ssh.
|
||||
|
||||
async def send_ssh_command(self, cmd: str) -> None:
|
||||
"""Sends SSH command to miner."""
|
||||
# creates result variable
|
||||
:return: Result of the command or None.
|
||||
"""
|
||||
result = None
|
||||
|
||||
# runs the command on the miner
|
||||
# open an ssh connection
|
||||
async with (await self._get_ssh_connection()) as conn:
|
||||
# attempt to run command up to 3 times
|
||||
# 3 retries
|
||||
for i in range(3):
|
||||
try:
|
||||
# save result of the command
|
||||
# run the command and get the result
|
||||
result = await conn.run(cmd)
|
||||
result = result.stdout
|
||||
except Exception as e:
|
||||
print(f"{cmd} error: {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)
|
||||
|
||||
# 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:
|
||||
async def fault_light_on(self) -> bool:
|
||||
"""Sends command to turn on fault light on the miner."""
|
||||
await self.send_ssh_command('miner fault_light on')
|
||||
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) -> None:
|
||||
async def fault_light_off(self) -> bool:
|
||||
"""Sends command to turn off fault light on the miner."""
|
||||
await self.send_ssh_command('miner fault_light off')
|
||||
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) -> None:
|
||||
async def restart_backend(self) -> bool:
|
||||
return await self.restart_bosminer()
|
||||
|
||||
async def restart_bosminer(self) -> bool:
|
||||
"""Restart bosminer hashing process."""
|
||||
await self.send_ssh_command('/etc/init.d/bosminer restart')
|
||||
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) -> None:
|
||||
async def reboot(self) -> bool:
|
||||
"""Reboots power to the physical miner."""
|
||||
await self.send_ssh_command('/sbin/reboot')
|
||||
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:
|
||||
async with sftp.open('/etc/bosminer.toml') as file:
|
||||
logging.debug(f"{self}: Reading config file.")
|
||||
async with sftp.open("/etc/bosminer.toml") as file:
|
||||
toml_data = toml.loads(await file.read())
|
||||
cfg = await bos_config_convert(toml_data)
|
||||
logging.debug(f"{self}: Converting config file.")
|
||||
cfg = bos_config_convert(toml_data)
|
||||
self.config = cfg
|
||||
|
||||
async def get_hostname(self) -> str:
|
||||
"""Attempts to get hostname from miner."""
|
||||
"""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:
|
||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
||||
return data.stdout.strip()
|
||||
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:
|
||||
print(self.ip, e)
|
||||
return "BOSMiner Unknown"
|
||||
logging.warning(f"Failed to get hostname for miner: {self}")
|
||||
return "?"
|
||||
|
||||
async def get_model(self):
|
||||
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:
|
||||
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
|
||||
return self.model + " (BOS)"
|
||||
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 send_config(self, yaml_config) -> 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."""
|
||||
toml_conf = await general_config_convert_bos(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:
|
||||
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
|
||||
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_bad_boards(self) -> list:
|
||||
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
|
||||
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."""
|
||||
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]
|
||||
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']
|
||||
chains = devs["DEVDETAILS"]
|
||||
for chain in chains:
|
||||
if chain['Chips'] == 0:
|
||||
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,
|
||||
"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"
|
||||
)
|
||||
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]
|
||||
|
||||
if summary:
|
||||
hr = summary.get("SUMMARY")
|
||||
if hr:
|
||||
if len(hr) > 0:
|
||||
hr = hr[0].get("MHS 5s")
|
||||
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
|
||||
|
||||
return data
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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):
|
||||
@@ -8,35 +10,176 @@ class BTMiner(BaseMiner):
|
||||
api = BTMinerAPI(ip)
|
||||
self.model = None
|
||||
super().__init__(ip, api)
|
||||
self.nominal_chips = 66
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BTMiner: {str(self.ip)}"
|
||||
|
||||
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:
|
||||
return host_data["Msg"]["hostname"]
|
||||
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 send_config(self, _):
|
||||
return None # ignore for now
|
||||
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 restart_backend(self) -> None:
|
||||
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": "",
|
||||
}
|
||||
|
||||
async def reboot(self) -> None:
|
||||
return None
|
||||
model = await self.get_model()
|
||||
hostname = await self.get_hostname()
|
||||
|
||||
async def get_config(self) -> None:
|
||||
return None
|
||||
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", "devs", "pools")
|
||||
if miner_data:
|
||||
break
|
||||
|
||||
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 5s")
|
||||
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 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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from miners import BaseMiner
|
||||
from API.cgminer import CGMinerAPI
|
||||
import asyncssh
|
||||
from API import APIError
|
||||
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
|
||||
import logging
|
||||
|
||||
|
||||
class CGMiner(BaseMiner):
|
||||
@@ -9,8 +11,8 @@ class CGMiner(BaseMiner):
|
||||
super().__init__(ip, api)
|
||||
self.model = None
|
||||
self.config = None
|
||||
self.uname = 'root'
|
||||
self.pwd = 'admin'
|
||||
self.uname = "root"
|
||||
self.pwd = "admin"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CGMiner: {str(self.ip)}"
|
||||
@@ -18,116 +20,191 @@ class CGMiner(BaseMiner):
|
||||
async def get_model(self):
|
||||
if self.model:
|
||||
return self.model
|
||||
version_data = await self.api.devdetails()
|
||||
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')
|
||||
return data.stdout.strip()
|
||||
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_config(self, _):
|
||||
return None # ignore for now
|
||||
|
||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
||||
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="admin",
|
||||
password="admin",
|
||||
server_host_key_algs=['ssh-rsa'])
|
||||
return conn
|
||||
except Exception as e:
|
||||
print(e)
|
||||
except OSError:
|
||||
print(str(self.ip) + " Connection refused.")
|
||||
return None
|
||||
|
||||
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
|
||||
# handle result
|
||||
self._result_handler(result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _result_handler(result: asyncssh.process.SSHCompletedProcess) -> None:
|
||||
if result is not None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
if len(result.stdout) > 0:
|
||||
# noinspection PyUnresolvedReferences
|
||||
print("ssh stdout: \n" + result.stdout)
|
||||
# noinspection PyUnresolvedReferences
|
||||
if len(result.stderr) > 0:
|
||||
# noinspection PyUnresolvedReferences
|
||||
print("ssh stderr: \n" + result.stderrr)
|
||||
# noinspection PyUnresolvedReferences
|
||||
if len(result.stdout) <= 0 and len(result.stderr) <= 0:
|
||||
print("ssh stdout stderr empty")
|
||||
# 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 restart_backend(self) -> bool:
|
||||
return await self.restart_cgminer()
|
||||
|
||||
async def restart_cgminer(self) -> None:
|
||||
commands = ['cgminer-api restart',
|
||||
'/usr/bin/cgminer-monitor >/dev/null 2>&1']
|
||||
commands = ';'.join(commands)
|
||||
await self.send_ssh_command(commands)
|
||||
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) -> None:
|
||||
commands = ['reboot']
|
||||
commands = ';'.join(commands)
|
||||
await self.send_ssh_command(commands)
|
||||
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)
|
||||
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)
|
||||
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'
|
||||
command = "cat /etc/config/cgminer"
|
||||
result = await conn.run(command, check=True)
|
||||
self._result_handler(result)
|
||||
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 5s")
|
||||
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
|
||||
|
||||
@@ -2,6 +2,9 @@ from miners.antminer.S9.bosminer import BOSMinerS9
|
||||
from miners.antminer.S9.bmminer import BMMinerS9
|
||||
from miners.antminer.S9.cgminer import CGMinerS9
|
||||
|
||||
from miners.antminer.T9.hive import HiveonT9
|
||||
from miners.antminer.T9.cgminer import CGMinerT9
|
||||
from miners.antminer.T9.bmminer import BMMinerT9
|
||||
|
||||
from miners.antminer.X17.bosminer import BOSMinerX17
|
||||
from miners.antminer.X17.bmminer import BMMinerX17
|
||||
@@ -9,6 +12,7 @@ from miners.antminer.X17.cgminer import CGMinerX17
|
||||
|
||||
from miners.antminer.X19.bmminer import BMMinerX19
|
||||
from miners.antminer.X19.cgminer import CGMinerX19
|
||||
from miners.antminer.X19.bosminer import BOSMinerX19
|
||||
|
||||
from miners.whatsminer.M20 import BTMinerM20
|
||||
from miners.whatsminer.M21 import BTMinerM21
|
||||
@@ -16,18 +20,38 @@ from miners.whatsminer.M30 import BTMinerM30
|
||||
from miners.whatsminer.M31 import BTMinerM31
|
||||
from miners.whatsminer.M32 import BTMinerM32
|
||||
|
||||
from miners.bmminer import BMMiner
|
||||
from miners.avalonminer.Avalon8 import CGMinerAvalon8
|
||||
from miners.avalonminer.Avalon10 import CGMinerAvalon10
|
||||
|
||||
from miners.cgminer import CGMiner
|
||||
from miners.bmminer import BMMiner
|
||||
from miners.bosminer import BOSMiner
|
||||
|
||||
from miners.unknown import UnknownMiner
|
||||
|
||||
from API import APIError
|
||||
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
|
||||
from settings import MINER_FACTORY_GET_VERSION_RETRIES as GET_VERSION_RETRIES
|
||||
from settings import (
|
||||
MINER_FACTORY_GET_VERSION_RETRIES as GET_VERSION_RETRIES,
|
||||
NETWORK_PING_TIMEOUT as PING_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
class MinerFactory:
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class MinerFactory(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
self.miners = {}
|
||||
|
||||
@@ -40,51 +64,108 @@ class MinerFactory:
|
||||
Parameters:
|
||||
ips: a list of ip addresses to get miners for.
|
||||
"""
|
||||
# get the event loop
|
||||
loop = asyncio.get_event_loop()
|
||||
# create a list of tasks
|
||||
scan_tasks = []
|
||||
# for each miner IP that was passed in, add a task to get its class
|
||||
for miner in ips:
|
||||
scan_tasks.append(loop.create_task(self.get_miner(miner)))
|
||||
# asynchronously run the tasks and return them as they complete
|
||||
scanned = asyncio.as_completed(scan_tasks)
|
||||
# loop through and yield the miners as they complete
|
||||
for miner in scanned:
|
||||
yield await miner
|
||||
|
||||
async def get_miner(self, ip: ipaddress.ip_address):
|
||||
async def get_miner(self, ip: ipaddress.ip_address or str):
|
||||
"""Decide a miner type using the IP address of the miner."""
|
||||
if isinstance(ip, str):
|
||||
ip = ipaddress.ip_address(ip)
|
||||
# check if the miner already exists in cache
|
||||
if ip in self.miners:
|
||||
return self.miners[ip]
|
||||
# if everything fails, the miner is already set to unknown
|
||||
miner = UnknownMiner(str(ip))
|
||||
api = None
|
||||
for i in range(GET_VERSION_RETRIES):
|
||||
api = await self._get_api_type(ip)
|
||||
if api:
|
||||
break
|
||||
model = None
|
||||
|
||||
# try to get the API multiple times based on retries
|
||||
for i in range(GET_VERSION_RETRIES):
|
||||
model = await self._get_miner_model(ip)
|
||||
if model:
|
||||
break
|
||||
try:
|
||||
# get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None
|
||||
new_model, new_api = await asyncio.wait_for(
|
||||
self._get_miner_type(ip), timeout=PING_TIMEOUT
|
||||
)
|
||||
|
||||
# keep track of the API and model we found first
|
||||
if new_api and not api:
|
||||
api = new_api
|
||||
if new_model and not model:
|
||||
model = new_model
|
||||
|
||||
# if we find the API and model, dont need to loop anymore
|
||||
if api and model:
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
# make sure we have model information
|
||||
if model:
|
||||
# check if the miner is an Antminer
|
||||
if "Antminer" in model:
|
||||
# S9 logic
|
||||
if "Antminer S9" in model:
|
||||
if "BOSMiner" in api:
|
||||
# handle the different API types
|
||||
if not api:
|
||||
logging.warning(
|
||||
f"{str(ip)}: No API data found, using BraiinsOS."
|
||||
)
|
||||
miner = BOSMinerS9(str(ip))
|
||||
elif "BOSMiner" in api:
|
||||
miner = BOSMinerS9(str(ip))
|
||||
elif "CGMiner" in api:
|
||||
miner = CGMinerS9(str(ip))
|
||||
elif "BMMiner" in api:
|
||||
miner = BMMinerS9(str(ip))
|
||||
|
||||
elif "Antminer T9" in model:
|
||||
if "BMMiner" in api:
|
||||
if "Hiveon" in model:
|
||||
# hiveOS, return T9 Hive
|
||||
miner = HiveonT9(str(ip))
|
||||
else:
|
||||
miner = BMMinerT9(str(ip))
|
||||
elif "CGMiner" in api:
|
||||
miner = CGMinerT9(str(ip))
|
||||
|
||||
# X17 model logic
|
||||
elif "17" in model:
|
||||
# handle the different API types
|
||||
if "BOSMiner" in api:
|
||||
miner = BOSMinerX17(str(ip))
|
||||
elif "CGMiner" in api:
|
||||
miner = CGMinerX17(str(ip))
|
||||
elif "BMMiner" in api:
|
||||
miner = BMMinerX17(str(ip))
|
||||
|
||||
# X19 logic
|
||||
elif "19" in model:
|
||||
# handle the different API types
|
||||
if "BOSMiner" in api:
|
||||
miner = BOSMinerX19(str(ip))
|
||||
if "CGMiner" in api:
|
||||
miner = CGMinerX19(str(ip))
|
||||
elif "BMMiner" in api:
|
||||
miner = BMMinerX19(str(ip))
|
||||
|
||||
# Avalonminers
|
||||
elif "avalon" in model:
|
||||
if model == "avalon10":
|
||||
miner = CGMinerAvalon10(str(ip))
|
||||
else:
|
||||
miner = CGMinerAvalon8(str(ip))
|
||||
|
||||
# Whatsminers
|
||||
elif "M20" in model:
|
||||
miner = BTMinerM20(str(ip))
|
||||
elif "M21" in model:
|
||||
@@ -95,57 +176,140 @@ class MinerFactory:
|
||||
miner = BTMinerM31(str(ip))
|
||||
elif "M32" in model:
|
||||
miner = BTMinerM32(str(ip))
|
||||
|
||||
# if we cant find a model, check if we found the API
|
||||
else:
|
||||
|
||||
# return the miner base class with some API if we found it
|
||||
if api:
|
||||
if "BOSMiner" in api:
|
||||
miner = BOSMiner(str(ip))
|
||||
elif "CGMiner" in api:
|
||||
miner = CGMiner(str(ip))
|
||||
elif "BMMiner" in api:
|
||||
miner = BMMiner(str(ip))
|
||||
|
||||
# save the miner to the cache at its IP
|
||||
self.miners[ip] = miner
|
||||
|
||||
# return the miner
|
||||
return miner
|
||||
|
||||
def clear_cached_miners(self):
|
||||
"""Clear the miner factory cache."""
|
||||
# empty out self.miners
|
||||
self.miners = {}
|
||||
|
||||
async def _get_miner_model(self, ip: ipaddress.ip_address or str) -> dict or None:
|
||||
async def _get_miner_type(self, ip: ipaddress.ip_address or str) -> tuple:
|
||||
model = None
|
||||
api = None
|
||||
|
||||
devdetails = None
|
||||
version = None
|
||||
|
||||
try:
|
||||
data = await self._send_api_command(str(ip), "devdetails")
|
||||
if data.get("STATUS"):
|
||||
if not isinstance(data["STATUS"], str):
|
||||
if data["STATUS"][0].get("STATUS") not in ["I", "S"]:
|
||||
try:
|
||||
data = await self._send_api_command(str(ip), "version")
|
||||
model = data["VERSION"][0]["Type"]
|
||||
except:
|
||||
print(f"Get Model Exception: {ip}")
|
||||
else:
|
||||
model = data["DEVDETAILS"][0]["Model"]
|
||||
data = await self._send_api_command(str(ip), "devdetails+version")
|
||||
|
||||
validation = await self._validate_command(data)
|
||||
if not validation[0]:
|
||||
raise APIError(validation[1])
|
||||
|
||||
devdetails = data["devdetails"][0]
|
||||
version = data["version"][0]
|
||||
|
||||
except APIError as e:
|
||||
data = None
|
||||
|
||||
if not data:
|
||||
try:
|
||||
devdetails = await self._send_api_command(str(ip), "devdetails")
|
||||
validation = await self._validate_command(devdetails)
|
||||
if not validation[0]:
|
||||
version = await self._send_api_command(str(ip), "version")
|
||||
|
||||
validation = await self._validate_command(version)
|
||||
if not validation[0]:
|
||||
raise APIError(validation[1])
|
||||
except APIError as e:
|
||||
logging.warning(f"{ip}: API Command Error: {e}")
|
||||
return None, None
|
||||
|
||||
if devdetails:
|
||||
if "DEVDETAILS" in devdetails.keys() and not devdetails["DEVDETAILS"] == []:
|
||||
# check for model, for most miners
|
||||
if not devdetails["DEVDETAILS"][0]["Model"] == "":
|
||||
# model of most miners
|
||||
model = devdetails["DEVDETAILS"][0]["Model"]
|
||||
|
||||
# if model fails, try driver
|
||||
else:
|
||||
try:
|
||||
data = await self._send_api_command(str(ip), "version")
|
||||
model = data["VERSION"][0]["Type"]
|
||||
except:
|
||||
print(f"Get Model Exception: {ip}")
|
||||
if model:
|
||||
return model
|
||||
except OSError as e:
|
||||
if e.winerror == 121:
|
||||
return None
|
||||
else:
|
||||
print(ip, e)
|
||||
return None
|
||||
# some avalonminers have model in driver
|
||||
model = devdetails["DEVDETAILS"][0]["Driver"]
|
||||
|
||||
if version:
|
||||
# check if there are any BMMiner strings in any of the dict keys
|
||||
if any("BMMiner" in string for string in version["VERSION"][0].keys()):
|
||||
api = "BMMiner"
|
||||
|
||||
# check if there are any CGMiner strings in any of the dict keys
|
||||
elif any("CGMiner" in string for string in version["VERSION"][0].keys()):
|
||||
api = "CGMiner"
|
||||
|
||||
# check if there are any BOSMiner strings in any of the dict keys
|
||||
elif any("BOSminer" in string for string in version["VERSION"][0].keys()):
|
||||
api = "BOSMiner"
|
||||
|
||||
# if all that fails, check the Description to see if it is a whatsminer
|
||||
elif version.get("Description") and "whatsminer" in version.get("Description"):
|
||||
api = "BTMiner"
|
||||
if version and not model:
|
||||
if (
|
||||
"VERSION" in version.keys()
|
||||
and version.get("VERSION")
|
||||
and not version.get("VERSION") == []
|
||||
):
|
||||
model = version["VERSION"][0]["Type"]
|
||||
return model, api
|
||||
|
||||
async def _validate_command(self, data: dict) -> tuple:
|
||||
"""Check if the returned command output is correctly formatted."""
|
||||
# check if the data returned is correct or an error
|
||||
if not data:
|
||||
return False, "No API data."
|
||||
# if status isn't a key, it is a multicommand
|
||||
if "STATUS" not in data.keys():
|
||||
for key in data.keys():
|
||||
# make sure not to try to turn id into a dict
|
||||
if not key == "id":
|
||||
# make sure they succeeded
|
||||
if "STATUS" in data[key][0].keys():
|
||||
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
|
||||
# this is an error
|
||||
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, 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, data["STATUS"][0]["Msg"]
|
||||
return True, None
|
||||
|
||||
async def _send_api_command(self, ip: ipaddress.ip_address or str, command: str):
|
||||
try:
|
||||
# get reader and writer streams
|
||||
reader, writer = await asyncio.open_connection(str(ip), 4028)
|
||||
# handle OSError 121
|
||||
except OSError as e:
|
||||
if e.winerror == "121":
|
||||
print("Semaphore Timeout has Expired.")
|
||||
logging.warning(f"{str(ip)} - Command {command}: {e}")
|
||||
return {}
|
||||
|
||||
# create the command
|
||||
cmd = {"command": command}
|
||||
|
||||
# send the command
|
||||
writer.write(json.dumps(cmd).encode('utf-8'))
|
||||
writer.write(json.dumps(cmd).encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
# instantiate data
|
||||
@@ -159,16 +323,16 @@ class MinerFactory:
|
||||
break
|
||||
data += d
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.debug(f"{str(ip)}: {e}")
|
||||
|
||||
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()
|
||||
@@ -176,40 +340,14 @@ class MinerFactory:
|
||||
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
|
||||
str_data = str_data.replace("}{", "},{")
|
||||
# parse the json
|
||||
parsed_data = json.loads(str_data)
|
||||
data = json.loads(str_data)
|
||||
# handle bad json
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
print(e)
|
||||
raise APIError(f"Decode Error: {data}")
|
||||
data = parsed_data
|
||||
# raise APIError(f"Decode Error: {data}")
|
||||
data = None
|
||||
|
||||
# close the connection
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def _get_api_type(self, ip: ipaddress.ip_address or str) -> dict or None:
|
||||
"""Get data on the version of the miner to return the right miner."""
|
||||
api = None
|
||||
try:
|
||||
data = await self._send_api_command(str(ip), "version")
|
||||
if data.get("STATUS") and not data.get("STATUS") == "E":
|
||||
if data["STATUS"][0].get("STATUS") in ["I", "S"]:
|
||||
if any("BMMiner" in string for string in data["VERSION"][0].keys()):
|
||||
api = "BMMiner"
|
||||
elif any("CGMiner" in string for string in data["VERSION"][0].keys()):
|
||||
api = "CGMiner"
|
||||
elif any("BOSminer" in string for string in data["VERSION"][0].keys()):
|
||||
api = "BOSMiner"
|
||||
elif data.get("Description") and "whatsminer" in data.get("Description"):
|
||||
api = "BTMiner"
|
||||
if api:
|
||||
return api
|
||||
except OSError as e:
|
||||
if e.winerror == 121:
|
||||
return None
|
||||
else:
|
||||
print(ip, e)
|
||||
return None
|
||||
|
||||
@@ -13,8 +13,5 @@ class UnknownMiner(BaseMiner):
|
||||
async def get_model(self):
|
||||
return "Unknown"
|
||||
|
||||
async def send_config(self, _):
|
||||
return None
|
||||
|
||||
async def get_hostname(self):
|
||||
return "Unknown"
|
||||
|
||||
@@ -4,6 +4,7 @@ from miners.btminer import BTMiner
|
||||
class BTMinerM21(BTMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.nominal_chips = [105, 66]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"M21 - BTMiner: {str(self.ip)}"
|
||||
|
||||
@@ -4,6 +4,7 @@ from miners.btminer import BTMiner
|
||||
class BTMinerM31(BTMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.nominal_chips = [78]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"M31 - BTMiner: {str(self.ip)}"
|
||||
|
||||
58
misc/bos.py
Normal file
58
misc/bos.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import asyncio
|
||||
from network import MinerNetwork
|
||||
from miners.bosminer import BOSMiner
|
||||
|
||||
|
||||
async def get_bos_bad_tuners(ip: str = "192.168.1.0", mask: int = 24):
|
||||
# create a miner network
|
||||
miner_network = MinerNetwork(ip, mask=mask)
|
||||
|
||||
# scan for miners
|
||||
miners = await miner_network.scan_network_for_miners()
|
||||
|
||||
# create an empty list of tasks
|
||||
tuner_tasks = []
|
||||
|
||||
# loop checks if the miner is a BOSMiner
|
||||
for miner in miners:
|
||||
# can only do this if its a subclass of BOSMiner
|
||||
if BOSMiner in type(miner).__bases__:
|
||||
tuner_tasks.append(_get_tuner_status(miner))
|
||||
|
||||
# run all the tuner status commands
|
||||
tuner_status = await asyncio.gather(*tuner_tasks)
|
||||
|
||||
# create a list of all miners with bad board tuner status
|
||||
bad_tuner_miners = []
|
||||
for item in tuner_status:
|
||||
# loop through and get each miners' bad board count
|
||||
bad_boards = []
|
||||
for board in item["tuner_status"]:
|
||||
# if its not stable or still testing, its bad
|
||||
if board["status"] not in ["Stable", "Testing performance profile", "Tuning individual chips"]:
|
||||
# remove the part about the board refusing to start
|
||||
bad_boards.append({"board": board["board"],
|
||||
"error": board["status"].replace("Hashchain refused to start: ", "")})
|
||||
|
||||
# if this miner has bad boards, add it to the list of bad board miners
|
||||
if len(bad_boards) > 0:
|
||||
bad_tuner_miners.append({"ip": item["ip"], "boards": bad_boards})
|
||||
|
||||
# return the list of bad board miners
|
||||
return bad_tuner_miners
|
||||
|
||||
|
||||
async def _get_tuner_status(miner):
|
||||
# run the tunerstatus command, since the miner will always be BOSMiner
|
||||
tuner_status = await miner.api.tunerstatus()
|
||||
|
||||
# create a list to add the tuner data to
|
||||
tuner_data = []
|
||||
|
||||
# if we have data, loop through to get the hashchain status
|
||||
if tuner_status:
|
||||
for board in tuner_status["TUNERSTATUS"][0]["TunerChainStatus"]:
|
||||
tuner_data.append({"board": board["HashchainIndex"], "status": board["Status"]})
|
||||
|
||||
# return the data along with the IP or later tracking
|
||||
return {"ip": str(miner.ip), "tuner_status": tuner_data}
|
||||
@@ -1,14 +1,21 @@
|
||||
import ipaddress
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from network.net_range import MinerNetworkRange
|
||||
from miners.miner_factory import MinerFactory
|
||||
from settings import NETWORK_PING_RETRIES as PING_RETRIES, NETWORK_PING_TIMEOUT as PING_TIMEOUT, \
|
||||
NETWORK_SCAN_THREADS as SCAN_THREADS
|
||||
from settings import (
|
||||
NETWORK_PING_RETRIES as PING_RETRIES,
|
||||
NETWORK_PING_TIMEOUT as PING_TIMEOUT,
|
||||
NETWORK_SCAN_THREADS as SCAN_THREADS,
|
||||
)
|
||||
|
||||
|
||||
class MinerNetwork:
|
||||
def __init__(self, ip_addr: str or None = None, mask: str or int or None = None) -> None:
|
||||
def __init__(
|
||||
self, ip_addr: str or None = None, mask: str or int or None = None
|
||||
) -> None:
|
||||
self.network = None
|
||||
self.miner_factory = MinerFactory()
|
||||
self.ip_addr = ip_addr
|
||||
self.connected_miners = {}
|
||||
self.mask = mask
|
||||
@@ -16,42 +23,85 @@ class MinerNetwork:
|
||||
def __len__(self):
|
||||
return len([item for item in self.get_network().hosts()])
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.network)
|
||||
|
||||
def get_network(self) -> ipaddress.ip_network:
|
||||
"""Get the network using the information passed to the MinerNetwork or from cache."""
|
||||
# if we have a network cached already, use that
|
||||
if self.network:
|
||||
return self.network
|
||||
if not self.ip_addr:
|
||||
default_gateway = "192.168.1.0"
|
||||
|
||||
if "-" in self.ip_addr:
|
||||
self.network = MinerNetworkRange(self.ip_addr)
|
||||
else:
|
||||
default_gateway = self.ip_addr
|
||||
if self.mask:
|
||||
subnet_mask = str(self.mask)
|
||||
else:
|
||||
subnet_mask = "24"
|
||||
return ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False)
|
||||
# if there is no IP address passed, default to 192.168.1.0
|
||||
if not self.ip_addr:
|
||||
default_gateway = "192.168.1.0"
|
||||
# if we do have an IP address passed, use that
|
||||
else:
|
||||
default_gateway = self.ip_addr
|
||||
|
||||
# if there is no subnet mask passed, default to /24
|
||||
if not self.mask:
|
||||
subnet_mask = "24"
|
||||
# if we do have a mask passed, use that
|
||||
else:
|
||||
subnet_mask = str(self.mask)
|
||||
|
||||
# save the network and return it
|
||||
self.network = ipaddress.ip_network(
|
||||
f"{default_gateway}/{subnet_mask}", strict=False
|
||||
)
|
||||
return self.network
|
||||
|
||||
async def scan_network_for_miners(self) -> None or list:
|
||||
"""Scan the network for miners, and return found miners as a list."""
|
||||
# get the network
|
||||
local_network = self.get_network()
|
||||
print(f"Scanning {local_network} for miners...")
|
||||
|
||||
# clear cached miners
|
||||
MinerFactory().clear_cached_miners()
|
||||
|
||||
# create a list of tasks and miner IPs
|
||||
scan_tasks = []
|
||||
miner_ips = []
|
||||
|
||||
# for each IP in the network
|
||||
for host in local_network.hosts():
|
||||
|
||||
# make sure we don't exceed the allowed async tasks
|
||||
if len(scan_tasks) < SCAN_THREADS:
|
||||
# add the task to the list
|
||||
scan_tasks.append(self.ping_miner(host))
|
||||
else:
|
||||
# run the scan tasks
|
||||
miner_ips_scan = await asyncio.gather(*scan_tasks)
|
||||
# add scanned miners to the list of found miners
|
||||
miner_ips.extend(miner_ips_scan)
|
||||
# empty the task list
|
||||
scan_tasks = []
|
||||
# do a final scan to empty out the list
|
||||
miner_ips_scan = await asyncio.gather(*scan_tasks)
|
||||
miner_ips.extend(miner_ips_scan)
|
||||
|
||||
# remove all None from the miner list
|
||||
miner_ips = list(filter(None, miner_ips))
|
||||
print(f"Found {len(miner_ips)} connected miners...")
|
||||
|
||||
# create a list of tasks to get miners
|
||||
create_miners_tasks = []
|
||||
self.miner_factory.clear_cached_miners()
|
||||
|
||||
# try to get each miner found
|
||||
for miner_ip in miner_ips:
|
||||
create_miners_tasks.append(self.miner_factory.get_miner(miner_ip))
|
||||
# append to the list of tasks
|
||||
create_miners_tasks.append(MinerFactory().get_miner(miner_ip))
|
||||
|
||||
# get all miners in the list
|
||||
miners = await asyncio.gather(*create_miners_tasks)
|
||||
|
||||
# return the miner objects
|
||||
return miners
|
||||
|
||||
async def scan_network_generator(self):
|
||||
@@ -60,41 +110,65 @@ class MinerNetwork:
|
||||
|
||||
Returns an asynchronous generator containing found miners.
|
||||
"""
|
||||
# get the current event loop
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# get the network
|
||||
local_network = self.get_network()
|
||||
|
||||
# create a list of scan tasks
|
||||
scan_tasks = []
|
||||
|
||||
# for each ip on the network, loop through and scan it
|
||||
for host in local_network.hosts():
|
||||
# make sure we don't exceed the allowed async tasks
|
||||
if len(scan_tasks) >= SCAN_THREADS:
|
||||
# scanned is a loopable list of awaitables
|
||||
scanned = asyncio.as_completed(scan_tasks)
|
||||
# when we scan, empty the scan tasks
|
||||
scan_tasks = []
|
||||
|
||||
# yield miners as they are scanned
|
||||
for miner in scanned:
|
||||
yield await miner
|
||||
|
||||
# add the ping to the list of tasks if we dont scan
|
||||
scan_tasks.append(loop.create_task(self.ping_miner(host)))
|
||||
|
||||
# do one last scan at the end to close out the list
|
||||
scanned = asyncio.as_completed(scan_tasks)
|
||||
for miner in scanned:
|
||||
yield await miner
|
||||
|
||||
@staticmethod
|
||||
async def ping_miner(ip: ipaddress.ip_address) -> None or ipaddress.ip_address:
|
||||
for i in range(PING_RETRIES):
|
||||
connection_fut = asyncio.open_connection(str(ip), 4028)
|
||||
try:
|
||||
# get the read and write streams from the connection
|
||||
reader, writer = await asyncio.wait_for(connection_fut, timeout=PING_TIMEOUT)
|
||||
# immediately close connection, we know connection happened
|
||||
writer.close()
|
||||
# make sure the writer is closed
|
||||
await writer.wait_closed()
|
||||
# ping was successful
|
||||
return ip
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
# ping failed if we time out
|
||||
continue
|
||||
except ConnectionRefusedError:
|
||||
# handle for other connection errors
|
||||
print(f"{str(ip)}: Connection Refused.")
|
||||
# ping failed, likely with an exception
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return await ping_miner(ip)
|
||||
|
||||
|
||||
async def ping_miner(
|
||||
ip: ipaddress.ip_address, port=4028
|
||||
) -> None or ipaddress.ip_address:
|
||||
for i in range(PING_RETRIES):
|
||||
connection_fut = asyncio.open_connection(str(ip), port)
|
||||
try:
|
||||
# get the read and write streams from the connection
|
||||
reader, writer = await asyncio.wait_for(
|
||||
connection_fut, timeout=PING_TIMEOUT
|
||||
)
|
||||
# immediately close connection, we know connection happened
|
||||
writer.close()
|
||||
# make sure the writer is closed
|
||||
await writer.wait_closed()
|
||||
# ping was successful
|
||||
return ip
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
# ping failed if we time out
|
||||
continue
|
||||
return
|
||||
except ConnectionRefusedError:
|
||||
# handle for other connection errors
|
||||
logging.debug(f"{str(ip)}: Connection Refused.")
|
||||
# ping failed, likely with an exception
|
||||
except Exception as e:
|
||||
logging.warning(f"{str(ip)}: {e}")
|
||||
continue
|
||||
return
|
||||
|
||||
31
network/net_range.py
Normal file
31
network/net_range.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import ipaddress
|
||||
|
||||
|
||||
class MinerNetworkRange:
|
||||
"""A MinerNetwork that takes a range of IP addresses.
|
||||
|
||||
:param ip_range: A range of IP addresses to put in the network.
|
||||
Takes a string formatted as
|
||||
{ip_range_1_start}-{ip_range_1_end}, {ip_range_2_start}-{ip_range_2_end}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, ip_range: str):
|
||||
ip_ranges = ip_range.replace(" ", "").split(",")
|
||||
self.host_ips = []
|
||||
for item in ip_ranges:
|
||||
start, end = item.split("-")
|
||||
start_ip = ipaddress.ip_address(start)
|
||||
end_ip = ipaddress.ip_address(end)
|
||||
networks = ipaddress.summarize_address_range(start_ip, end_ip)
|
||||
for network in networks:
|
||||
self.host_ips.append(network.network_address)
|
||||
for host in network.hosts():
|
||||
if host not in self.host_ips:
|
||||
self.host_ips.append(host)
|
||||
if network.broadcast_address not in self.host_ips:
|
||||
self.host_ips.append(network.broadcast_address)
|
||||
|
||||
def hosts(self):
|
||||
for x in self.host_ips:
|
||||
yield x
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -1,21 +1,48 @@
|
||||
import toml
|
||||
import os
|
||||
|
||||
NETWORK_PING_RETRIES: int = 3
|
||||
NETWORK_PING_TIMEOUT: int = 5
|
||||
NETWORK_SCAN_THREADS: int = 300
|
||||
|
||||
CFG_UTIL_REBOOT_THREADS: int = 300
|
||||
CFG_UTIL_CONFIG_THREADS: int = 300
|
||||
|
||||
MINER_FACTORY_GET_VERSION_RETRIES: int = 3
|
||||
|
||||
WHATSMINER_PWD = "admin"
|
||||
|
||||
DEBUG = False
|
||||
|
||||
try:
|
||||
with open(os.path.join(os.getcwd(), "settings.toml"), "r") as settings_file:
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "settings.toml"), "r"
|
||||
) as settings_file:
|
||||
settings = toml.loads(settings_file.read())
|
||||
except:
|
||||
pass
|
||||
|
||||
settings_keys = settings.keys()
|
||||
|
||||
if "ping_retries" in settings_keys:
|
||||
NETWORK_PING_RETRIES: int = settings["ping_retries"]
|
||||
if "ping_timeout" in settings_keys:
|
||||
NETWORK_PING_TIMEOUT: int = settings["ping_timeout"]
|
||||
if "scan_threads" in settings_keys:
|
||||
NETWORK_SCAN_THREADS: int = settings["scan_threads"]
|
||||
|
||||
if "reboot_threads" in settings_keys:
|
||||
CFG_UTIL_REBOOT_THREADS: int = settings["reboot_threads"]
|
||||
if "config_threads" in settings_keys:
|
||||
CFG_UTIL_CONFIG_THREADS: int = settings["config_threads"]
|
||||
|
||||
|
||||
if "get_version_retries" in settings_keys:
|
||||
MINER_FACTORY_GET_VERSION_RETRIES: int = settings["get_version_retries"]
|
||||
except:
|
||||
NETWORK_PING_RETRIES: int = 3
|
||||
NETWORK_PING_TIMEOUT: int = 5
|
||||
NETWORK_SCAN_THREADS: int = 300
|
||||
|
||||
CFG_UTIL_CONFIG_THREADS: int = 300
|
||||
|
||||
MINER_FACTORY_GET_VERSION_RETRIES: int = 3
|
||||
if "whatsminer_pwd" in settings_keys:
|
||||
WHATSMINER_PWD: str = settings["whatsminer_pwd"]
|
||||
|
||||
if "debug" in settings_keys:
|
||||
DEBUG: int = settings["debug"]
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
get_version_retries = 3
|
||||
ping_retries = 3
|
||||
ping_timeout = 5
|
||||
ping_timeout = 5 # Seconds
|
||||
scan_threads = 300
|
||||
config_threads = 300
|
||||
config_threads = 300
|
||||
reboot_threads = 300
|
||||
|
||||
|
||||
### IMPORTANT ###
|
||||
# You need to change the password of the miners using the whatsminer
|
||||
# tool or the privileged API will not work using admin as the password.
|
||||
# If you change the password, you can pass that password here.
|
||||
|
||||
whatsminer_pwd = "admin"
|
||||
|
||||
### DEBUG MODE ###
|
||||
# change this to debug = true
|
||||
# to enable debug mode.
|
||||
debug = false
|
||||
# debug = true
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
|
||||
|
||||
|
||||
[//]: # (If you can read this, you are viewing this document incorrectly.)
|
||||
[//]: # (This is a Markdown document. Use an online Markdown viewer to)
|
||||
[//]: # (view this file, such as https://dillinger.io/)
|
||||
|
||||
|
||||
|
||||
# CFG-Util
|
||||
|
||||
## Interact with bitcoin mining ASICs using a simple GUI.
|
||||
@@ -6,35 +15,37 @@
|
||||
## 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
|
||||
* 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, and get data on them.
|
||||
|
||||
### 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 separated by a new line
|
||||
* 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 separated 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
|
||||
* 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
|
||||
### Buttons:
|
||||
* ALL: Selects all miners in the table, or deselects all if they are already all selected.
|
||||
* GET DATA: Gets data for the currently selected miners, or all miners if none are selected.
|
||||
* Additionally, if no miners have been scanned, this will also run a scan then get data on those miners.
|
||||
* REFRESH DATA: Refreshes data for the currently selected miners, or all miners if none are selected.
|
||||
* OPEN IN WEB: Opens all currently selected miners web interfaces in your default browser.
|
||||
* REBOOT: Reboots all selected miners.
|
||||
* RESTART BACKEND: Restarts the mining process on the miner (bosminer daemon, bmminer daemon, cgminer daemon, etc).
|
||||
* SEND SSH COMMAND: Open a new window to send a SSH command to all selected miners (or all miners if none are selected).
|
||||
|
||||
### Table:
|
||||
* Click any header in the table to sort that row.
|
||||
* You can copy (CTRL + C) a list of IP's directly from the rows selected in the table.
|
||||
|
||||
* #### IP:
|
||||
* Contains all the IP's scanned
|
||||
* Contains all the IP's scanned.
|
||||
|
||||
* #### Model:
|
||||
* The model of the miners scanned.
|
||||
@@ -57,8 +68,8 @@
|
||||
* 0 W will be displayed if it is unknown.
|
||||
|
||||
### 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
|
||||
* 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.
|
||||
|
||||
0
tools/__init__.py
Normal file
0
tools/__init__.py
Normal file
29
tools/bad_board_util/__init__.py
Normal file
29
tools/bad_board_util/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from tools.bad_board_util.ui import ui
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from logger import logger
|
||||
|
||||
logger.info("Initializing logger for Board Util.")
|
||||
|
||||
|
||||
# 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():
|
||||
logging.info("Starting Board Util.")
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(ui())
|
||||
logging.info("Closing Board Util.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
tools/bad_board_util/func/__init__.py
Normal file
0
tools/bad_board_util/func/__init__.py
Normal file
29
tools/bad_board_util/func/decorators.py
Normal file
29
tools/bad_board_util/func/decorators.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from tools.bad_board_util.layout import window
|
||||
|
||||
|
||||
def disable_buttons(func):
|
||||
button_list = [
|
||||
"scan",
|
||||
"import_iplist",
|
||||
"export_iplist",
|
||||
"select_all_ips",
|
||||
"refresh_data",
|
||||
"open_in_web",
|
||||
"save_report_button",
|
||||
"light",
|
||||
]
|
||||
|
||||
# handle the inner function that the decorator is wrapping
|
||||
async def inner(*args, **kwargs):
|
||||
# disable the buttons
|
||||
for button in button_list:
|
||||
window[button].Update(disabled=True)
|
||||
|
||||
# call the original wrapped function
|
||||
await func(*args, **kwargs)
|
||||
|
||||
# re-enable the buttons after the wrapped function completes
|
||||
for button in button_list:
|
||||
window[button].Update(disabled=False)
|
||||
|
||||
return inner
|
||||
104
tools/bad_board_util/func/files.py
Normal file
104
tools/bad_board_util/func/files.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import xlsxwriter
|
||||
|
||||
import aiofiles
|
||||
|
||||
from tools.bad_board_util.func.ui import update_ui_with_data
|
||||
from tools.bad_board_util.layout import window
|
||||
from tools.bad_board_util.func.decorators import disable_buttons
|
||||
from miners.miner_factory import MinerFactory
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def save_report(file_location):
|
||||
data = {}
|
||||
workbook = xlsxwriter.Workbook(file_location)
|
||||
sheet = workbook.add_worksheet()
|
||||
for line in window["ip_table"].Values:
|
||||
data[line[0]] = {
|
||||
"Model": line[1],
|
||||
"Total Chips": line[2],
|
||||
"Left Chips": line[3],
|
||||
"Center Chips": line[5],
|
||||
"Right Chips": line[7],
|
||||
"Nominal": 1,
|
||||
}
|
||||
|
||||
async for miner in MinerFactory().get_miner_generator([key for key in data.keys()]):
|
||||
if miner:
|
||||
data[miner.ip]["Nominal"] = miner.nominal
|
||||
|
||||
list_data = []
|
||||
for ip in data.keys():
|
||||
new_data = data[ip]
|
||||
new_data["IP"] = ip
|
||||
list_data.append(new_data)
|
||||
|
||||
data = sorted(data, reverse=True, key=lambda x: x["Total Chips"])
|
||||
|
||||
headers = [
|
||||
"IP",
|
||||
"Miner Model",
|
||||
"Total Chip Count",
|
||||
"Left Board Chips",
|
||||
"Center Board Chips",
|
||||
"Right Board Chips",
|
||||
]
|
||||
print(data)
|
||||
row = 0
|
||||
col = 0
|
||||
for item in headers:
|
||||
sheet.write(row, col, item)
|
||||
col += 1
|
||||
|
||||
row = 1
|
||||
for line in data:
|
||||
col = 0
|
||||
for point in line:
|
||||
sheet.write(row, col, point)
|
||||
col += 1
|
||||
row += 1
|
||||
|
||||
workbook.close()
|
||||
|
||||
|
||||
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", "")
|
||||
308
tools/bad_board_util/func/miners.py
Normal file
308
tools/bad_board_util/func/miners.py
Normal file
@@ -0,0 +1,308 @@
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import warnings
|
||||
|
||||
from tools.bad_board_util.func.ui import (
|
||||
update_ui_with_data,
|
||||
update_prog_bar,
|
||||
set_progress_bar_len,
|
||||
)
|
||||
from tools.bad_board_util.layout import window
|
||||
from miners.miner_factory import MinerFactory
|
||||
from tools.bad_board_util.func.decorators import disable_buttons
|
||||
|
||||
|
||||
@disable_buttons
|
||||
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 MinerFactory().get_miner(ip)
|
||||
index = [item[0] for item in window["ip_table"].Values].index(ip)
|
||||
index_tags = ip_list.item(index + 1)["tags"]
|
||||
if "light" not in index_tags and "light+bad" not in index_tags:
|
||||
tag = "light"
|
||||
if "bad" in index_tags:
|
||||
index_tags.remove("bad")
|
||||
tag = "light+bad"
|
||||
index_tags.append(tag)
|
||||
ip_list.item(index + 1, tags=index_tags)
|
||||
await miner.fault_light_on()
|
||||
else:
|
||||
if "light+bad" in index_tags:
|
||||
index_tags.remove("light+bad")
|
||||
index_tags.append("bad")
|
||||
if "light" in index_tags:
|
||||
index_tags.remove("light")
|
||||
ip_list.item(index + 1, tags=index_tags)
|
||||
await miner.fault_light_off()
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def scan_network(network):
|
||||
await update_ui_with_data("status", "Scanning")
|
||||
await update_ui_with_data("ip_count", "")
|
||||
window["ip_table"].update([])
|
||||
network_size = len(network)
|
||||
miner_generator = network.scan_network_generator()
|
||||
await set_progress_bar_len(2 * network_size)
|
||||
progress_bar_len = 0
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
miners = []
|
||||
async for miner in miner_generator:
|
||||
if miner:
|
||||
miners.append(miner)
|
||||
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 = MinerFactory().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", "")
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def refresh_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
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
reset_table_values = []
|
||||
for item in window["ip_table"].Values:
|
||||
if item[0] in ip_list:
|
||||
reset_table_values.append([item[0]])
|
||||
else:
|
||||
reset_table_values.append(item)
|
||||
window["ip_table"].update(reset_table_values)
|
||||
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]
|
||||
row_colors = []
|
||||
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"])
|
||||
board_left = ""
|
||||
board_center = ""
|
||||
board_right = ""
|
||||
if data_point["data"]:
|
||||
if 0 in data_point["data"].keys():
|
||||
board_left = " ".join(
|
||||
[chain["chip_status"] for chain in data_point["data"][0]]
|
||||
).replace("o", "•")
|
||||
if 1 in data_point["data"].keys():
|
||||
board_center = " ".join(
|
||||
[chain["chip_status"] for chain in data_point["data"][1]]
|
||||
).replace("o", "•")
|
||||
if 2 in data_point["data"].keys():
|
||||
board_right = " ".join(
|
||||
[chain["chip_status"] for chain in data_point["data"][2]]
|
||||
).replace("o", "•")
|
||||
data = [
|
||||
data_point["IP"],
|
||||
data_point["model"],
|
||||
len(board_left),
|
||||
board_left,
|
||||
len(board_center),
|
||||
board_center,
|
||||
len(board_right),
|
||||
board_right,
|
||||
]
|
||||
ip_table_data[ip_table_index] = data
|
||||
window["ip_table"].update(ip_table_data)
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def scan_and_get_data(network):
|
||||
# update status and reset the table
|
||||
await update_ui_with_data("status", "Scanning")
|
||||
await update_ui_with_data("ip_count", "")
|
||||
await update_ui_with_data("ip_table", [])
|
||||
|
||||
# set progress bar length to network size
|
||||
network_size = len(network)
|
||||
await set_progress_bar_len(3 * network_size)
|
||||
progress_bar_len = 0
|
||||
|
||||
miners = []
|
||||
|
||||
# scan the network for miners using a generator
|
||||
async for miner in network.scan_network_generator():
|
||||
# the generator will either return None or an IP address
|
||||
if miner:
|
||||
miners.append(miner)
|
||||
|
||||
# add to the progress bar length after scanning an address
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
|
||||
# add progress for the miners that we aren't going to identify
|
||||
progress_bar_len += network_size - len(miners)
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
|
||||
all_miners = []
|
||||
|
||||
# identify different miner instances using the miner factory generator
|
||||
async for found_miner in MinerFactory().get_miner_generator(miners):
|
||||
# miner factory generator will always return a miner
|
||||
all_miners.append(found_miner)
|
||||
|
||||
# sort the list of miners by IP address
|
||||
all_miners.sort(key=lambda x: x.ip)
|
||||
|
||||
# add the new miner to the table
|
||||
window["ip_table"].update([[str(miner.ip)] for miner in all_miners])
|
||||
|
||||
# update progress bar
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
|
||||
# update the count of found miners
|
||||
await update_ui_with_data("ip_count", str(len(all_miners)))
|
||||
|
||||
# update progress bar for miners we wont get data for
|
||||
progress_bar_len += network_size - len(miners)
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
|
||||
# get the list of IP addresses from the table
|
||||
ip_table_data = window["ip_table"].Values
|
||||
ordered_all_ips = [item[0] for item in ip_table_data]
|
||||
|
||||
await update_ui_with_data("status", "Getting Data")
|
||||
row_colors = []
|
||||
|
||||
# create an in place generator for getting data
|
||||
for all_data in asyncio.as_completed(
|
||||
[get_formatted_data(miner) for miner in miners]
|
||||
):
|
||||
# wait for a generator item to return
|
||||
data_point = await all_data
|
||||
|
||||
# make sure the IP is one we have
|
||||
# this will likely never fail, but a good failsafe
|
||||
if data_point["IP"] in ordered_all_ips:
|
||||
# get the index of the IP in the table
|
||||
ip_table_index = ordered_all_ips.index(data_point["IP"])
|
||||
|
||||
board_left = ""
|
||||
board_center = ""
|
||||
board_right = ""
|
||||
|
||||
# make sure we have data, some miners don't allow getting board data
|
||||
if data_point["data"]:
|
||||
|
||||
# check if the 0th board (L board) is in the data
|
||||
if 0 in data_point["data"].keys():
|
||||
board_left = " ".join(
|
||||
[chain["chip_status"] for chain in data_point["data"][0]]
|
||||
).replace("o", "•")
|
||||
else:
|
||||
# if the board isn't in data, highlight it red
|
||||
row_colors.append((ip_table_index, "bad"))
|
||||
|
||||
# check if the 1st board (C board) is in the data
|
||||
if 1 in data_point["data"].keys():
|
||||
board_center = " ".join(
|
||||
[chain["chip_status"] for chain in data_point["data"][1]]
|
||||
).replace("o", "•")
|
||||
else:
|
||||
# if the board isn't in data, highlight it red
|
||||
row_colors.append((ip_table_index, "bad"))
|
||||
|
||||
# check if the 2nd board (R board) is in the data
|
||||
if 2 in data_point["data"].keys():
|
||||
board_right = " ".join(
|
||||
[chain["chip_status"] for chain in data_point["data"][2]]
|
||||
).replace("o", "•")
|
||||
else:
|
||||
# if the board isn't in data, highlight it red
|
||||
row_colors.append((ip_table_index, "bad"))
|
||||
|
||||
# check if the miner has all nominal chips
|
||||
if False in [
|
||||
# True/False if the miner is nominal
|
||||
chain["nominal"]
|
||||
# for each board in the miner
|
||||
for board in [
|
||||
data_point["data"][key] for key in data_point["data"].keys()
|
||||
]
|
||||
# for each chain in each board in the miner
|
||||
for chain in board
|
||||
]:
|
||||
# if the miner doesn't have all chips, highlight it red
|
||||
row_colors.append((ip_table_index, "bad"))
|
||||
else:
|
||||
# the row is bad if we have no data
|
||||
row_colors.append((ip_table_index, "bad"))
|
||||
|
||||
# split the chip data into thirds
|
||||
board_left_chips = "\n".join(split_chips(board_left, 3))
|
||||
board_center_chips = "\n".join(split_chips(board_center, 3))
|
||||
board_right_chips = "\n".join(split_chips(board_right, 3))
|
||||
|
||||
# create data for the table
|
||||
data = [
|
||||
data_point["IP"],
|
||||
data_point["model"],
|
||||
(len(board_left) + len(board_center) + len(board_right)),
|
||||
len(board_left),
|
||||
board_left_chips,
|
||||
len(board_center),
|
||||
board_center_chips,
|
||||
len(board_right),
|
||||
board_right_chips,
|
||||
]
|
||||
|
||||
# put the data at the index of the IP address
|
||||
ip_table_data[ip_table_index] = data
|
||||
window["ip_table"].update(ip_table_data)
|
||||
|
||||
# configure "bad" tag to highlight red
|
||||
table = window["ip_table"].Widget
|
||||
|
||||
# set tags on the row if they have been set
|
||||
for row in row_colors:
|
||||
table.item(row[0] + 1, tags=row[1])
|
||||
|
||||
# add to the progress bar
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
|
||||
# reset status
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
def split_chips(string, number_of_splits):
|
||||
k, m = divmod(len(string), number_of_splits)
|
||||
return (
|
||||
string[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)]
|
||||
for i in range(number_of_splits)
|
||||
)
|
||||
|
||||
|
||||
async def get_formatted_data(ip: ipaddress.ip_address):
|
||||
miner = await MinerFactory().get_miner(ip)
|
||||
model = await miner.get_model()
|
||||
warnings.filterwarnings("ignore")
|
||||
board_data = await miner.get_board_info()
|
||||
data = {"IP": str(ip), "model": str(model), "data": board_data}
|
||||
return data
|
||||
392
tools/bad_board_util/func/pdf.py
Normal file
392
tools/bad_board_util/func/pdf.py
Normal file
@@ -0,0 +1,392 @@
|
||||
import datetime
|
||||
from base64 import b64decode
|
||||
from io import BytesIO
|
||||
|
||||
from reportlab.lib.pagesizes import letter, inch
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.lib.styles import ParagraphStyle, TA_CENTER
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate,
|
||||
KeepInFrame,
|
||||
Table,
|
||||
Image,
|
||||
Paragraph,
|
||||
TableStyle,
|
||||
PageBreak,
|
||||
Spacer,
|
||||
)
|
||||
from reportlab.lib import colors
|
||||
|
||||
import ipaddress
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.dates
|
||||
import matplotlib.pyplot as plt
|
||||
from svglib.svglib import svg2rlg
|
||||
from matplotlib import cm
|
||||
from matplotlib.ticker import FormatStrFormatter
|
||||
|
||||
|
||||
from miners.miner_factory import MinerFactory
|
||||
from tools.bad_board_util.func.decorators import disable_buttons
|
||||
from tools.bad_board_util.img import IMAGE_SELECTION_MATRIX, LOGO
|
||||
from tools.bad_board_util.layout import window
|
||||
|
||||
IP_STYLE = ParagraphStyle(
|
||||
"IP Style",
|
||||
alignment=TA_CENTER,
|
||||
fontSize=7,
|
||||
fontName="Helvetica-Bold",
|
||||
)
|
||||
TITLE_STYLE = ParagraphStyle(
|
||||
"Title",
|
||||
alignment=TA_CENTER,
|
||||
fontSize=20,
|
||||
spaceAfter=40,
|
||||
fontName="Helvetica-Bold",
|
||||
)
|
||||
|
||||
|
||||
def add_first_page_number(canvas, doc):
|
||||
canvas.saveState()
|
||||
canvas.drawString(letter[0] - 60, 20, "Page " + str(doc.page))
|
||||
canvas.restoreState()
|
||||
|
||||
|
||||
def add_page_header(canvas, doc):
|
||||
canvas.saveState()
|
||||
canvas.drawCentredString(
|
||||
(letter[0] / 16) * 14,
|
||||
letter[1] - 57,
|
||||
datetime.datetime.now().strftime("%Y-%b-%d"),
|
||||
)
|
||||
img_dec = b64decode(LOGO)
|
||||
img = BytesIO(img_dec)
|
||||
img.seek(0)
|
||||
|
||||
canvas.drawImage(
|
||||
ImageReader(img),
|
||||
30,
|
||||
letter[1] - 65,
|
||||
150,
|
||||
35,
|
||||
)
|
||||
canvas.drawString(letter[0] - 60, 20, "Page " + str(doc.page))
|
||||
canvas.restoreState()
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def save_report(file_location):
|
||||
p1_logo, p1_title = create_first_page()
|
||||
data = {}
|
||||
for line in window["ip_table"].Values:
|
||||
data[line[0]] = {
|
||||
"Model": line[1],
|
||||
"Total Chips": line[2],
|
||||
"Left Chips": line[3],
|
||||
"Center Chips": line[5],
|
||||
"Right Chips": line[7],
|
||||
"Nominal": 1,
|
||||
}
|
||||
|
||||
async for miner in MinerFactory().get_miner_generator([key for key in data.keys()]):
|
||||
if miner:
|
||||
data[str(miner.ip)]["Nominal"] = miner.nominal_chips
|
||||
|
||||
list_data = []
|
||||
for ip in data.keys():
|
||||
new_data = data[ip]
|
||||
new_data["IP"] = ip
|
||||
list_data.append(new_data)
|
||||
|
||||
list_data = sorted(
|
||||
list_data, reverse=False, key=lambda x: ipaddress.ip_address(x["IP"])
|
||||
)
|
||||
|
||||
image_selection_data = {}
|
||||
for miner in list_data:
|
||||
miner_bad_boards = ""
|
||||
if miner["Left Chips"] < miner["Nominal"]:
|
||||
miner_bad_boards += "l"
|
||||
if miner["Center Chips"] < miner["Nominal"]:
|
||||
miner_bad_boards += "c"
|
||||
if miner["Right Chips"] < miner["Nominal"]:
|
||||
miner_bad_boards += "r"
|
||||
image_selection_data[miner["IP"]] = miner_bad_boards
|
||||
|
||||
doc = SimpleDocTemplate(
|
||||
file_location,
|
||||
pagesize=letter,
|
||||
topMargin=1 * inch,
|
||||
leftMargin=1 * inch,
|
||||
rightMargin=1 * inch,
|
||||
bottomMargin=1 * inch,
|
||||
title=f"Board Report {datetime.datetime.now().strftime('%Y/%b/%d')}",
|
||||
)
|
||||
|
||||
pie_chart, board_table = create_boards_pie_chart(image_selection_data)
|
||||
|
||||
table_data = get_table_data(image_selection_data)
|
||||
|
||||
miner_img_table = Table(
|
||||
table_data,
|
||||
colWidths=0.8 * inch,
|
||||
# repeatRows=1,
|
||||
# rowHeights=[4 * inch],
|
||||
)
|
||||
|
||||
miner_img_table.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("SPAN", (0, 0), (-1, 0)),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 0),
|
||||
("BOTTOMPADDING", (0, 1), (-1, -1), 0),
|
||||
("TOPPADDING", (0, 1), (-1, -1), 0),
|
||||
("BOTTOMPADDING", (0, 0), (-1, 0), 20),
|
||||
("TOPPADDING", (0, 0), (-1, 0), 20),
|
||||
]
|
||||
)
|
||||
)
|
||||
elements = []
|
||||
elements.append(p1_logo)
|
||||
elements.append(p1_title)
|
||||
elements.append(PageBreak())
|
||||
elements.append(pie_chart)
|
||||
elements.append(Spacer(0, 60))
|
||||
elements.append(board_table)
|
||||
elements.append(PageBreak())
|
||||
elements.append(miner_img_table)
|
||||
elements.append(PageBreak())
|
||||
elements.append(
|
||||
Paragraph(
|
||||
"Board Data",
|
||||
style=TITLE_STYLE,
|
||||
)
|
||||
)
|
||||
elements.append(create_data_table(list_data))
|
||||
elements.append(PageBreak())
|
||||
|
||||
doc.build(
|
||||
elements,
|
||||
onFirstPage=add_first_page_number,
|
||||
onLaterPages=add_page_header,
|
||||
)
|
||||
|
||||
|
||||
def create_boards_pie_chart(data):
|
||||
labels = ["All Working", "1 Bad Board", "2 Bad Boards", "3 Bad Boards"]
|
||||
num_bad_boards = [0, 0, 0, 0]
|
||||
for item in data.keys():
|
||||
num_bad_boards[len(data[item])] += 1
|
||||
cmap = plt.get_cmap("Blues")
|
||||
cs = cmap(np.linspace(0.2, 0.8, num=len(num_bad_boards)))
|
||||
|
||||
fig1, ax = plt.subplots()
|
||||
ax.pie(
|
||||
num_bad_boards,
|
||||
labels=labels,
|
||||
autopct="%1.2f%%",
|
||||
shadow=True,
|
||||
startangle=180,
|
||||
colors=cs,
|
||||
pctdistance=0.8,
|
||||
)
|
||||
ax.axis("equal")
|
||||
ax.set_title("Broken Boards", fontsize=24, pad=20)
|
||||
|
||||
imgdata = BytesIO()
|
||||
fig1.savefig(imgdata, format="svg")
|
||||
imgdata.seek(0) # rewind the data
|
||||
drawing = svg2rlg(imgdata)
|
||||
imgdata.close()
|
||||
plt.close("all")
|
||||
pie_chart = KeepInFrame(375, 375, [Image(drawing)], hAlign="CENTER")
|
||||
|
||||
table_data = [labels, num_bad_boards]
|
||||
|
||||
t = Table(table_data)
|
||||
|
||||
table_style = TableStyle(
|
||||
[
|
||||
# ("FONTSIZE", (0, 0), (-1, -1), 13),
|
||||
# line for below titles
|
||||
("LINEBELOW", (0, 0), (-1, 0), 2, colors.black),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
# line for above totals
|
||||
("LINEABOVE", (0, -1), (-1, -1), 2, colors.black),
|
||||
# line for beside unit #
|
||||
("LINEAFTER", (0, 0), (0, -1), 2, colors.black),
|
||||
# gridlines and outline of table
|
||||
("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
|
||||
("BOX", (0, 0), (-1, -1), 2, colors.black),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 3),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 3),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 3),
|
||||
]
|
||||
)
|
||||
|
||||
t.setStyle(table_style)
|
||||
|
||||
# zebra stripes on table
|
||||
for each in range(len(table_data)):
|
||||
if each % 2 == 0:
|
||||
bg_color = colors.whitesmoke
|
||||
else:
|
||||
bg_color = colors.lightgrey
|
||||
|
||||
t.setStyle(TableStyle([("BACKGROUND", (0, each), (-1, each), bg_color)]))
|
||||
|
||||
return pie_chart, t
|
||||
|
||||
|
||||
def create_first_page():
|
||||
title_style = ParagraphStyle(
|
||||
"Title",
|
||||
alignment=TA_CENTER,
|
||||
fontSize=50,
|
||||
spaceAfter=40,
|
||||
spaceBefore=150,
|
||||
fontName="Helvetica-Bold",
|
||||
)
|
||||
|
||||
img_dec = b64decode(LOGO)
|
||||
img = BytesIO(img_dec)
|
||||
img.seek(0)
|
||||
|
||||
logo = KeepInFrame(450, 105, [Image(img)])
|
||||
title = Paragraph("Board Report", style=title_style)
|
||||
return logo, title
|
||||
|
||||
|
||||
def create_data_table(data):
|
||||
left_bad_boards = 0
|
||||
right_bad_boards = 0
|
||||
center_bad_boards = 0
|
||||
table_data = []
|
||||
for miner in data:
|
||||
miner_bad_boards = 0
|
||||
if miner["Left Chips"] < miner["Nominal"]:
|
||||
miner_bad_boards += 1
|
||||
left_bad_boards += 1
|
||||
if miner["Center Chips"] < miner["Nominal"]:
|
||||
miner_bad_boards += 1
|
||||
right_bad_boards += 1
|
||||
if miner["Right Chips"] < miner["Nominal"]:
|
||||
miner_bad_boards += 1
|
||||
center_bad_boards += 1
|
||||
table_data.append(
|
||||
[
|
||||
miner["IP"],
|
||||
miner["Total Chips"],
|
||||
miner["Left Chips"],
|
||||
miner["Center Chips"],
|
||||
miner["Right Chips"],
|
||||
miner_bad_boards,
|
||||
]
|
||||
)
|
||||
|
||||
table_data.append(
|
||||
[
|
||||
"Total",
|
||||
sum([miner[1] for miner in table_data]),
|
||||
sum([miner[2] for miner in table_data]),
|
||||
sum([miner[3] for miner in table_data]),
|
||||
sum([miner[4] for miner in table_data]),
|
||||
sum([miner[5] for miner in table_data]),
|
||||
]
|
||||
)
|
||||
|
||||
table_data[:0] = (
|
||||
[
|
||||
"IP",
|
||||
"Total Chips",
|
||||
"Left Board Chips",
|
||||
"Center Board Chips",
|
||||
"Right Board Chips",
|
||||
"Failed Boards",
|
||||
],
|
||||
)
|
||||
|
||||
# create the table
|
||||
t = Table(table_data, repeatRows=1)
|
||||
|
||||
# generate a basic table style
|
||||
table_style = TableStyle(
|
||||
[
|
||||
("FONTSIZE", (0, 0), (-1, -1), 8),
|
||||
# line for below titles
|
||||
("LINEBELOW", (0, 0), (-1, 0), 2, colors.black),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
# line for above totals
|
||||
("LINEABOVE", (0, -1), (-1, -1), 2, colors.black),
|
||||
("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"),
|
||||
# line for beside unit #
|
||||
("LINEAFTER", (0, 0), (0, -1), 2, colors.black),
|
||||
("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
|
||||
# gridlines and outline of table
|
||||
("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
|
||||
("BOX", (0, 0), (-1, -1), 2, colors.black),
|
||||
]
|
||||
)
|
||||
|
||||
for (
|
||||
row,
|
||||
values,
|
||||
) in enumerate(table_data):
|
||||
if not row == 0 and not row == (len(table_data) - 1):
|
||||
failed_boards = values[5]
|
||||
if not failed_boards == 0:
|
||||
table_style.add("TEXTCOLOR", (5, row), (5, row), colors.red)
|
||||
|
||||
# set the styles to the table
|
||||
t.setStyle(table_style)
|
||||
|
||||
# zebra stripes on table
|
||||
for each in range(len(table_data)):
|
||||
if each % 2 == 0:
|
||||
bg_color = colors.whitesmoke
|
||||
else:
|
||||
bg_color = colors.lightgrey
|
||||
|
||||
t.setStyle(TableStyle([("BACKGROUND", (0, each), (-1, each), bg_color)]))
|
||||
|
||||
return t
|
||||
|
||||
|
||||
def get_table_data(data):
|
||||
table_elems = [[Paragraph("Hashboard Visual Representation", style=TITLE_STYLE)]]
|
||||
table_row = []
|
||||
table_style = TableStyle(
|
||||
[
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 0),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 0),
|
||||
("BOX", (0, 0), (-1, -1), 2, colors.black),
|
||||
]
|
||||
)
|
||||
table_width = 0.8 * inch
|
||||
for ip in data.keys():
|
||||
img_dec = b64decode(IMAGE_SELECTION_MATRIX[data[ip]])
|
||||
img = BytesIO(img_dec)
|
||||
img.seek(0)
|
||||
image = KeepInFrame(table_width, table_width, [Image(img)])
|
||||
ip_para = Paragraph(ip, style=IP_STYLE)
|
||||
|
||||
table_row.append(
|
||||
Table([[ip_para], [image]], colWidths=table_width, style=table_style)
|
||||
)
|
||||
|
||||
# table_row.append(image)
|
||||
# table_row_txt.append(ip_para)
|
||||
|
||||
if len(table_row) > 7:
|
||||
# table_elems.append(table_row_txt)
|
||||
# table_elems.append(table_row)
|
||||
table_elems.append(table_row)
|
||||
# table_row_txt = []
|
||||
table_row = []
|
||||
if not table_row == []:
|
||||
table_elems.append(table_row)
|
||||
return table_elems
|
||||
96
tools/bad_board_util/func/ui.py
Normal file
96
tools/bad_board_util/func/ui.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
from tools.bad_board_util.layout import window
|
||||
|
||||
import pyperclip
|
||||
|
||||
|
||||
def table_select_all():
|
||||
window["ip_table"].update(
|
||||
select_rows=([row for row in range(len(window["ip_table"].Values))])
|
||||
)
|
||||
|
||||
|
||||
def copy_from_table(table):
|
||||
selection = table.selection()
|
||||
copy_values = []
|
||||
for each in selection:
|
||||
try:
|
||||
# value = table.item(each)["values"][0]
|
||||
table_values = table.item(each)["values"]
|
||||
ip = table_values[0]
|
||||
model = table_values[1]
|
||||
total = str(table_values[2])
|
||||
l_brd_chips = str(table_values[3])
|
||||
c_brd_chips = str(table_values[5])
|
||||
r_brd_chips = str(table_values[7])
|
||||
all_values = [ip, model, total, l_brd_chips, c_brd_chips, r_brd_chips]
|
||||
value = ", ".join(all_values)
|
||||
|
||||
copy_values.append(str(value))
|
||||
except Exception as e:
|
||||
print("Copy Error:", e)
|
||||
copy_string = "\n".join(copy_values)
|
||||
pyperclip.copy(copy_string)
|
||||
|
||||
|
||||
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 sort_data(index: int or str):
|
||||
if window["scan"].Disabled:
|
||||
return
|
||||
await update_ui_with_data("status", "Sorting Data")
|
||||
data_list = window["ip_table"].Values
|
||||
table = window["ip_table"].Widget
|
||||
all_data = []
|
||||
for idx, item in enumerate(data_list):
|
||||
all_data.append({"data": item, "tags": table.item(int(idx) + 1)["tags"]})
|
||||
# ip addresses
|
||||
if 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]?)",
|
||||
str(all_data[0]["data"][index]),
|
||||
):
|
||||
new_list = sorted(
|
||||
all_data, key=lambda x: ipaddress.ip_address(x["data"][index])
|
||||
)
|
||||
if all_data == new_list:
|
||||
new_list = sorted(
|
||||
all_data,
|
||||
reverse=True,
|
||||
key=lambda x: ipaddress.ip_address(x["data"][index]),
|
||||
)
|
||||
|
||||
# everything else, model, chips
|
||||
else:
|
||||
new_list = sorted(all_data, key=lambda x: x["data"][index])
|
||||
if all_data == new_list:
|
||||
new_list = sorted(all_data, reverse=True, key=lambda x: x["data"][index])
|
||||
|
||||
new_data = []
|
||||
for item in new_list:
|
||||
new_data.append(item["data"])
|
||||
|
||||
await update_ui_with_data("ip_table", new_data)
|
||||
for idx, item in enumerate(new_list):
|
||||
table.item(idx + 1, tags=item["tags"])
|
||||
|
||||
await update_ui_with_data("status", "")
|
||||
27
tools/bad_board_util/img/__init__.py
Normal file
27
tools/bad_board_util/img/__init__.py
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
86
tools/bad_board_util/ui.py
Normal file
86
tools/bad_board_util/ui.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import PySimpleGUI as sg
|
||||
|
||||
from tools.bad_board_util.layout import window
|
||||
from tools.bad_board_util.func.miners import (
|
||||
refresh_data,
|
||||
scan_and_get_data,
|
||||
miner_light,
|
||||
)
|
||||
from tools.bad_board_util.func.files import import_iplist, export_iplist
|
||||
from tools.bad_board_util.func.pdf import save_report
|
||||
from tools.bad_board_util.func.ui import sort_data, copy_from_table, table_select_all
|
||||
|
||||
from network import MinerNetwork
|
||||
|
||||
import webbrowser
|
||||
|
||||
|
||||
async def ui():
|
||||
window.read(timeout=0)
|
||||
table = window["ip_table"].Widget
|
||||
table.bind("<Control-Key-c>", lambda x: copy_from_table(table))
|
||||
table.bind("<Control-Key-a>", lambda x: table_select_all())
|
||||
# light tag shows red row for fault lights
|
||||
table.tag_configure("bad", foreground="white", background="orange")
|
||||
table.tag_configure("light", foreground="white", background="red")
|
||||
table.tag_configure("light+bad", foreground="white", background="red")
|
||||
while True:
|
||||
event, value = window.read(timeout=0)
|
||||
if event in (None, "Close", sg.WIN_CLOSED):
|
||||
sys.exit()
|
||||
if isinstance(event, tuple):
|
||||
if len(window["ip_table"].Values) > 0:
|
||||
if event[0] == "ip_table":
|
||||
if event[2][0] == -1:
|
||||
await sort_data(event[2][1])
|
||||
if event == "open_in_web":
|
||||
for row in value["ip_table"]:
|
||||
webbrowser.open("http://" + window["ip_table"].Values[row][0])
|
||||
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_and_get_data(miner_network))
|
||||
if event == "save_report":
|
||||
if not value["save_report"] == "":
|
||||
asyncio.create_task(save_report(value["save_report"]))
|
||||
window["save_report"].update("")
|
||||
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 == "light":
|
||||
if len(window["ip_table"].Values) > 0:
|
||||
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 == "refresh_data":
|
||||
asyncio.create_task(
|
||||
refresh_data(
|
||||
[window["ip_table"].Values[item][0] for item in value["ip_table"]]
|
||||
)
|
||||
)
|
||||
if event == "__TIMEOUT__":
|
||||
await asyncio.sleep(0)
|
||||
20
tools/cfg_util/__init__.py
Normal file
20
tools/cfg_util/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from .ui import ui
|
||||
|
||||
# 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():
|
||||
asyncio.run(ui())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
78
tools/cfg_util/commands/__init__.py
Normal file
78
tools/cfg_util/commands/__init__.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from miners.miner_factory import MinerFactory
|
||||
from tools.cfg_util.layout import window
|
||||
from tools.cfg_util.tables import TableManager
|
||||
from tools.cfg_util.decorators import disable_buttons
|
||||
|
||||
|
||||
@disable_buttons("Flashing Lights")
|
||||
async def btn_light(ip_idxs: list):
|
||||
table_manager = TableManager()
|
||||
_table = window["cmd_table"].Widget
|
||||
iids = _table.get_children()
|
||||
for idx in ip_idxs:
|
||||
item = _table.item(iids[idx])
|
||||
ip = item["values"][0]
|
||||
new_light_val = not table_manager.data[ip]["Light"]
|
||||
miner = await MinerFactory().get_miner(ip)
|
||||
if new_light_val:
|
||||
success = await miner.fault_light_on()
|
||||
else:
|
||||
success = await miner.fault_light_off()
|
||||
if success:
|
||||
table_manager.data[ip]["Light"] = new_light_val
|
||||
table_manager.data[ip]["Command Output"] = "Fault Light command succeeded."
|
||||
else:
|
||||
table_manager.data[ip]["Command Output"] = "Fault Light command failed."
|
||||
table_manager.update_tables()
|
||||
|
||||
|
||||
@disable_buttons("Rebooting")
|
||||
async def btn_reboot(ip_idxs: list):
|
||||
table_manager = TableManager()
|
||||
_table = window["cmd_table"].Widget
|
||||
iids = _table.get_children()
|
||||
for idx in ip_idxs:
|
||||
item = _table.item(iids[idx])
|
||||
ip = item["values"][0]
|
||||
miner = await MinerFactory().get_miner(ip)
|
||||
success = await miner.reboot()
|
||||
if success:
|
||||
table_manager.data[ip]["Command Output"] = "Reboot command succeeded."
|
||||
else:
|
||||
table_manager.data[ip]["Command Output"] = "Reboot command failed."
|
||||
table_manager.update_tables()
|
||||
|
||||
|
||||
@disable_buttons("Restarting Backend")
|
||||
async def btn_backend(ip_idxs: list):
|
||||
table_manager = TableManager()
|
||||
_table = window["cmd_table"].Widget
|
||||
iids = _table.get_children()
|
||||
for idx in ip_idxs:
|
||||
item = _table.item(iids[idx])
|
||||
ip = item["values"][0]
|
||||
miner = await MinerFactory().get_miner(ip)
|
||||
success = await miner.restart_backend()
|
||||
if success:
|
||||
table_manager.data[ip][
|
||||
"Command Output"
|
||||
] = "Restart Backend command succeeded."
|
||||
else:
|
||||
table_manager.data[ip]["Command Output"] = "Restart Backend command failed."
|
||||
table_manager.update_tables()
|
||||
|
||||
|
||||
@disable_buttons("Sending Command")
|
||||
async def btn_command(ip_idxs: list, command: str):
|
||||
table_manager = TableManager()
|
||||
_table = window["cmd_table"].Widget
|
||||
iids = _table.get_children()
|
||||
for idx in ip_idxs:
|
||||
item = _table.item(iids[idx])
|
||||
ip = item["values"][0]
|
||||
miner = await MinerFactory().get_miner(ip)
|
||||
success = await miner.send_ssh_command(command)
|
||||
if not isinstance(success, str):
|
||||
success = f"Command {command} failed."
|
||||
table_manager.data[ip]["Command Output"] = success
|
||||
table_manager.update_tables()
|
||||
160
tools/cfg_util/configure/__init__.py
Normal file
160
tools/cfg_util/configure/__init__.py
Normal file
@@ -0,0 +1,160 @@
|
||||
import PySimpleGUI as sg
|
||||
from config.bos import bos_config_convert
|
||||
import time
|
||||
from tools.cfg_util.layout import window, update_prog_bar
|
||||
from tools.cfg_util.decorators import disable_buttons
|
||||
from miners.miner_factory import MinerFactory
|
||||
import asyncio
|
||||
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
|
||||
from tools.cfg_util.general import update_miners_data
|
||||
|
||||
|
||||
progress_bar_len = 0
|
||||
|
||||
|
||||
@disable_buttons("Importing")
|
||||
async def btn_import(table, selected):
|
||||
if not len(selected) > 0:
|
||||
return
|
||||
ip = [window[table].Values[row][0] for row in selected][0]
|
||||
miner = await MinerFactory().get_miner(ip)
|
||||
await miner.get_config()
|
||||
config = miner.config
|
||||
window["cfg_config_txt"].update(config)
|
||||
|
||||
|
||||
@disable_buttons("Configuring")
|
||||
async def btn_config(table, selected, config: str, last_oct_ip: bool):
|
||||
ips = [window[table].Values[row][0] for row in selected]
|
||||
await send_config(ips, config, last_oct_ip)
|
||||
|
||||
|
||||
async def send_config(ips: list, config: str, last_octet_ip: bool):
|
||||
global progress_bar_len
|
||||
progress_bar_len = 0
|
||||
await update_prog_bar(progress_bar_len, max=(2 * len(ips)))
|
||||
get_miner_genenerator = MinerFactory().get_miner_generator(ips)
|
||||
all_miners = []
|
||||
async for miner in get_miner_genenerator:
|
||||
all_miners.append(miner)
|
||||
progress_bar_len += 1
|
||||
await update_prog_bar(progress_bar_len)
|
||||
|
||||
config_sender_generator = send_config_generator(
|
||||
all_miners, config, last_octet_ip_user=last_octet_ip
|
||||
)
|
||||
async for _config_sender in config_sender_generator:
|
||||
progress_bar_len += 1
|
||||
await update_prog_bar(progress_bar_len)
|
||||
await asyncio.sleep(3)
|
||||
await update_miners_data(ips)
|
||||
|
||||
|
||||
async def send_config_generator(miners: list, config, last_octet_ip_user: bool):
|
||||
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, ip_user=last_octet_ip_user))
|
||||
)
|
||||
configured = asyncio.as_completed(config_tasks)
|
||||
for sent_config in configured:
|
||||
yield await sent_config
|
||||
|
||||
|
||||
def generate_config(username: str, workername: str, v2_allowed: bool):
|
||||
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["cfg_config_txt"].update(bos_config_convert(config))
|
||||
|
||||
|
||||
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"]:
|
||||
generate_config(
|
||||
values["generate_config_window_username"],
|
||||
values["generate_config_window_workername"],
|
||||
values["generate_config_window_allow_v2"],
|
||||
)
|
||||
generate_config_window.close()
|
||||
break
|
||||
|
||||
|
||||
def generate_config_layout():
|
||||
config_layout = [
|
||||
[
|
||||
sg.Text(
|
||||
"Enter your pool username and password below to generate a config for SlushPool."
|
||||
)
|
||||
],
|
||||
[sg.Text("")],
|
||||
[
|
||||
sg.Text("Username:", size=(19, 1)),
|
||||
sg.InputText(
|
||||
key="generate_config_window_username", do_not_clear=True, size=(45, 1)
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.Text("Worker Name (OPT):", size=(19, 1)),
|
||||
sg.InputText(
|
||||
key="generate_config_window_workername", do_not_clear=True, size=(45, 1)
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.Text("Allow Stratum V2?:", size=(19, 1)),
|
||||
sg.Checkbox("", key="generate_config_window_allow_v2", default=True),
|
||||
],
|
||||
[sg.Button("Generate", key="generate_config_window_generate")],
|
||||
]
|
||||
return config_layout
|
||||
24
tools/cfg_util/decorators.py
Normal file
24
tools/cfg_util/decorators.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from tools.cfg_util.layout import window
|
||||
from tools.cfg_util.layout import BUTTON_KEYS
|
||||
|
||||
|
||||
def disable_buttons(status: str = ""):
|
||||
def decorator(func):
|
||||
# handle the inner function that the decorator is wrapping
|
||||
async def inner(*args, **kwargs):
|
||||
# disable the buttons
|
||||
for button in BUTTON_KEYS:
|
||||
window[button].Update(disabled=True)
|
||||
window["status"].update(status)
|
||||
|
||||
# call the original wrapped function
|
||||
await func(*args, **kwargs)
|
||||
|
||||
# re-enable the buttons after the wrapped function completes
|
||||
for button in BUTTON_KEYS:
|
||||
window[button].Update(disabled=False)
|
||||
window["status"].update("")
|
||||
|
||||
return inner
|
||||
|
||||
return decorator
|
||||
85
tools/cfg_util/general/__init__.py
Normal file
85
tools/cfg_util/general/__init__.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import asyncio
|
||||
import webbrowser
|
||||
|
||||
from miners.miner_factory import MinerFactory
|
||||
from tools.cfg_util.decorators import disable_buttons
|
||||
from tools.cfg_util.layout import TABLE_KEYS
|
||||
from tools.cfg_util.layout import window, update_prog_bar
|
||||
from tools.cfg_util.tables import TableManager
|
||||
|
||||
progress_bar_len = 0
|
||||
|
||||
DEFAULT_DATA = [
|
||||
"Model",
|
||||
"Hostname",
|
||||
"Hashrate",
|
||||
"Temperature",
|
||||
"Pool User",
|
||||
"Pool 1",
|
||||
"Pool 1 User",
|
||||
"Pool 2",
|
||||
"Pool 2 User",
|
||||
"Wattage",
|
||||
"Split",
|
||||
]
|
||||
|
||||
|
||||
def btn_all(table, selected):
|
||||
if table in TABLE_KEYS["table"]:
|
||||
if len(selected) == len(window[table].Values):
|
||||
window[table].update(select_rows=())
|
||||
else:
|
||||
window[table].update(
|
||||
select_rows=([row for row in range(len(window[table].Values))])
|
||||
)
|
||||
|
||||
if table in TABLE_KEYS["tree"]:
|
||||
if len(selected) == len(window[table].Widget.get_children()):
|
||||
_tree = window[table]
|
||||
_tree.Widget.selection_set([])
|
||||
else:
|
||||
_tree = window[table]
|
||||
rows_to_select = [i for i in _tree.Widget.get_children()]
|
||||
_tree.Widget.selection_set(rows_to_select)
|
||||
|
||||
|
||||
def btn_web(table, selected):
|
||||
for row in selected:
|
||||
webbrowser.open("http://" + window[table].Values[row][0])
|
||||
|
||||
|
||||
@disable_buttons("Refreshing")
|
||||
async def btn_refresh(table, selected):
|
||||
ips = [window[table].Values[row][0] for row in selected]
|
||||
if not len(selected) > 0:
|
||||
ips = [window[table].Values[row][0] for row in range(len(window[table].Values))]
|
||||
|
||||
await update_miners_data(ips)
|
||||
|
||||
|
||||
async def update_miners_data(miners: list):
|
||||
data = []
|
||||
for miner in miners:
|
||||
_data = {}
|
||||
for key in DEFAULT_DATA:
|
||||
_data[key] = ""
|
||||
_data["IP"] = str(miner)
|
||||
data.append(_data)
|
||||
|
||||
TableManager().update_data(data)
|
||||
|
||||
global progress_bar_len
|
||||
progress_bar_len = 0
|
||||
await update_prog_bar(progress_bar_len, max=len(miners))
|
||||
data_generator = asyncio.as_completed(
|
||||
[_get_data(await MinerFactory().get_miner(miner)) for miner in miners]
|
||||
)
|
||||
for all_data in data_generator:
|
||||
data = await all_data
|
||||
TableManager().update_item(data)
|
||||
progress_bar_len += 1
|
||||
await update_prog_bar(progress_bar_len)
|
||||
|
||||
|
||||
async def _get_data(miner):
|
||||
return await miner.get_data()
|
||||
27
tools/cfg_util/imgs.py
Normal file
27
tools/cfg_util/imgs.py
Normal file
File diff suppressed because one or more lines are too long
438
tools/cfg_util/layout.py
Normal file
438
tools/cfg_util/layout.py
Normal file
@@ -0,0 +1,438 @@
|
||||
import PySimpleGUI as sg
|
||||
|
||||
from .imgs import WINDOW_ICON
|
||||
|
||||
sg.set_options(font=("Liberation Mono", 10))
|
||||
|
||||
TABLE_HEADERS = {
|
||||
"SCAN": [
|
||||
"IP",
|
||||
"Model",
|
||||
"Hostname",
|
||||
"Hashrate",
|
||||
"Temperature",
|
||||
"Pool User",
|
||||
"Wattage",
|
||||
],
|
||||
"CMD": ["IP", "Model", "Output"],
|
||||
"POOLS_ALL": [
|
||||
"IP",
|
||||
"Split",
|
||||
"Pool 1 User",
|
||||
"Pool 2 User",
|
||||
],
|
||||
"POOLS_1": [
|
||||
"IP",
|
||||
"Split",
|
||||
"Pool 1",
|
||||
"Pool 1 User",
|
||||
],
|
||||
"POOLS_2": [
|
||||
"IP",
|
||||
"Split",
|
||||
"Pool 2",
|
||||
"Pool 2 User",
|
||||
],
|
||||
"CONFIG": ["IP", "Model", "Pool 1 User"],
|
||||
}
|
||||
|
||||
TABLE_KEYS = {
|
||||
"table": ["scan_table", "pools_table", "cfg_table"],
|
||||
"tree": ["cmd_table"],
|
||||
}
|
||||
|
||||
MINER_COUNT_BUTTONS = [
|
||||
"scan_miner_count",
|
||||
"cmd_miner_count",
|
||||
"cfg_miner_count",
|
||||
"pools_miner_count",
|
||||
]
|
||||
|
||||
SORT_KEY_BUTTONS = [
|
||||
"scan_sort_key",
|
||||
"cmd_sort_key",
|
||||
"cfg_sort_key",
|
||||
"pools_sort_key",
|
||||
]
|
||||
|
||||
HASHRATE_TOTAL_BUTTONS = [
|
||||
"scan_total_hashrate",
|
||||
"cfg_total_hashrate",
|
||||
"pools_total_hashrate",
|
||||
]
|
||||
|
||||
BUTTON_KEYS = [
|
||||
"btn_scan",
|
||||
"btn_cmd",
|
||||
"scan_all",
|
||||
"scan_refresh",
|
||||
"scan_web",
|
||||
"cmd_all",
|
||||
"cmd_light",
|
||||
"cmd_reboot",
|
||||
"cmd_backend",
|
||||
"pools_all",
|
||||
"pools_refresh",
|
||||
"pools_web",
|
||||
"cfg_import",
|
||||
"cfg_config",
|
||||
"cfg_generate",
|
||||
"cfg_all",
|
||||
"cfg_web",
|
||||
]
|
||||
|
||||
TABLE_HEIGHT = 27
|
||||
|
||||
IMAGE_COL_WIDTH = 6
|
||||
IP_COL_WIDTH = 17
|
||||
MODEL_COL_WIDTH = 15
|
||||
HOST_COL_WIDTH = 15
|
||||
HASHRATE_COL_WIDTH = 12
|
||||
TEMP_COL_WIDTH = 12
|
||||
USER_COL_WIDTH = 31
|
||||
WATTAGE_COL_WIDTH = 8
|
||||
SPLIT_COL_WIDTH = 6
|
||||
SCAN_COL_WIDTHS = [
|
||||
IP_COL_WIDTH,
|
||||
MODEL_COL_WIDTH,
|
||||
HOST_COL_WIDTH,
|
||||
HASHRATE_COL_WIDTH,
|
||||
TEMP_COL_WIDTH,
|
||||
USER_COL_WIDTH,
|
||||
WATTAGE_COL_WIDTH,
|
||||
]
|
||||
TABLE_TOTAL_WIDTH = sum(SCAN_COL_WIDTHS)
|
||||
|
||||
|
||||
async def update_prog_bar(count: int, max: int = None):
|
||||
bar = window["progress_bar"]
|
||||
bar.update_bar(count, max=max)
|
||||
if max:
|
||||
bar.maxlen = max
|
||||
if not hasattr(bar, "maxlen"):
|
||||
if not max:
|
||||
max = 100
|
||||
bar.maxlen = max
|
||||
|
||||
percent_done = 100 * (count / bar.maxlen)
|
||||
window["progress_percent"].Update(f"{round(percent_done, 2)} %")
|
||||
if percent_done == 100:
|
||||
window["progress_percent"].Update("")
|
||||
|
||||
|
||||
def get_scan_layout():
|
||||
scan_layout = [
|
||||
[
|
||||
sg.Text("Scan IP"),
|
||||
sg.InputText(key="scan_ip", size=(31, 1)),
|
||||
sg.Button("Scan", key="btn_scan"),
|
||||
sg.Push(),
|
||||
sg.Button(
|
||||
"Sort: IP",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="scan_sort_key",
|
||||
),
|
||||
sg.Button(
|
||||
"Hashrate: 0 TH/s",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="scan_total_hashrate",
|
||||
),
|
||||
sg.Button(
|
||||
"Miners: 0",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="scan_miner_count",
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.Button("ALL", key="scan_all"),
|
||||
sg.Button("REFRESH DATA", key="scan_refresh"),
|
||||
sg.Button("OPEN IN WEB", key="scan_web"),
|
||||
],
|
||||
[
|
||||
sg.Table(
|
||||
values=[],
|
||||
headings=[heading for heading in TABLE_HEADERS["SCAN"]],
|
||||
auto_size_columns=False,
|
||||
max_col_width=15,
|
||||
justification="center",
|
||||
key="scan_table",
|
||||
col_widths=SCAN_COL_WIDTHS,
|
||||
background_color="white",
|
||||
text_color="black",
|
||||
size=(TABLE_TOTAL_WIDTH, TABLE_HEIGHT),
|
||||
expand_x=True,
|
||||
enable_click_events=True,
|
||||
)
|
||||
],
|
||||
]
|
||||
return scan_layout
|
||||
|
||||
|
||||
def get_command_layout():
|
||||
data = sg.TreeData()
|
||||
col_widths = [
|
||||
IP_COL_WIDTH,
|
||||
MODEL_COL_WIDTH,
|
||||
TABLE_TOTAL_WIDTH - (IP_COL_WIDTH + MODEL_COL_WIDTH + IMAGE_COL_WIDTH + 4),
|
||||
]
|
||||
|
||||
command_layout = [
|
||||
[
|
||||
sg.Text("Custom Command"),
|
||||
sg.InputText(key="cmd_txt", expand_x=True),
|
||||
sg.Button("Send Command", key="btn_cmd"),
|
||||
sg.Push(),
|
||||
sg.Button(
|
||||
"Sort: IP",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="cmd_sort_key",
|
||||
),
|
||||
sg.Button(
|
||||
"Miners: 0",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="cmd_miner_count",
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.Button("ALL", key="cmd_all"),
|
||||
sg.Button("LIGHT", key="cmd_light"),
|
||||
sg.Button("REBOOT", key="cmd_reboot"),
|
||||
sg.Button("RESTART BACKEND", key="cmd_backend"),
|
||||
],
|
||||
[
|
||||
sg.Tree(
|
||||
data,
|
||||
headings=[heading for heading in TABLE_HEADERS["CMD"]],
|
||||
auto_size_columns=False,
|
||||
max_col_width=15,
|
||||
justification="center",
|
||||
key="cmd_table",
|
||||
col_widths=col_widths,
|
||||
background_color="white",
|
||||
text_color="black",
|
||||
expand_x=True,
|
||||
expand_y=True,
|
||||
col0_heading="Light",
|
||||
col0_width=IMAGE_COL_WIDTH,
|
||||
enable_events=True,
|
||||
)
|
||||
],
|
||||
]
|
||||
return command_layout
|
||||
|
||||
|
||||
def get_pools_layout():
|
||||
pool_col_width = int((TABLE_TOTAL_WIDTH - (IP_COL_WIDTH + SPLIT_COL_WIDTH)) / 2)
|
||||
col_widths = [
|
||||
IP_COL_WIDTH,
|
||||
SPLIT_COL_WIDTH,
|
||||
pool_col_width,
|
||||
pool_col_width,
|
||||
]
|
||||
pools_layout = [
|
||||
[
|
||||
sg.Button("ALL", key="pools_all"),
|
||||
sg.Button("REFRESH DATA", key="pools_refresh"),
|
||||
sg.Button("OPEN IN WEB", key="pools_web"),
|
||||
sg.Push(),
|
||||
sg.Button(
|
||||
"Sort: IP",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="pools_sort_key",
|
||||
),
|
||||
sg.Button(
|
||||
"Hashrate: 0 TH/s",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="pools_total_hashrate",
|
||||
),
|
||||
sg.Button(
|
||||
"Miners: 0",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="pools_miner_count",
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.TabGroup(
|
||||
[
|
||||
[
|
||||
sg.Tab(
|
||||
"All",
|
||||
[
|
||||
[
|
||||
sg.Table(
|
||||
values=[],
|
||||
headings=[
|
||||
heading
|
||||
for heading in TABLE_HEADERS["POOLS_ALL"]
|
||||
],
|
||||
auto_size_columns=False,
|
||||
max_col_width=15,
|
||||
justification="center",
|
||||
key="pools_table",
|
||||
background_color="white",
|
||||
text_color="black",
|
||||
col_widths=col_widths,
|
||||
size=(0, TABLE_HEIGHT),
|
||||
expand_x=True,
|
||||
enable_click_events=True,
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
],
|
||||
[
|
||||
sg.Tab(
|
||||
"Pool 1",
|
||||
[
|
||||
[
|
||||
sg.Table(
|
||||
values=[],
|
||||
headings=[
|
||||
heading
|
||||
for heading in TABLE_HEADERS["POOLS_1"]
|
||||
],
|
||||
auto_size_columns=False,
|
||||
max_col_width=15,
|
||||
justification="center",
|
||||
key="pools_1_table",
|
||||
background_color="white",
|
||||
text_color="black",
|
||||
col_widths=col_widths,
|
||||
size=(0, TABLE_HEIGHT),
|
||||
expand_x=True,
|
||||
enable_click_events=True,
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
],
|
||||
[
|
||||
sg.Tab(
|
||||
"Pool 2",
|
||||
[
|
||||
[
|
||||
sg.Table(
|
||||
values=[],
|
||||
headings=[
|
||||
heading
|
||||
for heading in TABLE_HEADERS["POOLS_2"]
|
||||
],
|
||||
auto_size_columns=False,
|
||||
max_col_width=15,
|
||||
justification="center",
|
||||
key="pools_2_table",
|
||||
background_color="white",
|
||||
text_color="black",
|
||||
col_widths=col_widths,
|
||||
size=(0, TABLE_HEIGHT),
|
||||
expand_x=True,
|
||||
enable_click_events=True,
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
],
|
||||
]
|
||||
)
|
||||
],
|
||||
]
|
||||
return pools_layout
|
||||
|
||||
|
||||
def get_config_layout():
|
||||
config_layout = [
|
||||
[
|
||||
sg.Button("IMPORT", key="cfg_import"),
|
||||
sg.Button("CONFIG", key="cfg_config"),
|
||||
sg.Button("GENERATE", key="cfg_generate"),
|
||||
sg.Push(),
|
||||
sg.Button(
|
||||
"Sort: IP",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="cfg_sort_key",
|
||||
),
|
||||
sg.Button(
|
||||
"Hashrate: 0 TH/s",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="cfg_total_hashrate",
|
||||
),
|
||||
sg.Button(
|
||||
"Miners: 0",
|
||||
disabled=True,
|
||||
button_color=("black", "white smoke"),
|
||||
disabled_button_color=("black", "white smoke"),
|
||||
key="cfg_miner_count",
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.Button("ALL", key="cfg_all"),
|
||||
sg.Button("OPEN IN WEB", key="cfg_web"),
|
||||
sg.Push(),
|
||||
sg.Checkbox("Append IP to Username", key="cfg_append_ip"),
|
||||
],
|
||||
[
|
||||
sg.Table(
|
||||
values=[],
|
||||
headings=[heading for heading in TABLE_HEADERS["CONFIG"]],
|
||||
auto_size_columns=False,
|
||||
max_col_width=15,
|
||||
justification="center",
|
||||
key="cfg_table",
|
||||
background_color="white",
|
||||
text_color="black",
|
||||
col_widths=[
|
||||
IP_COL_WIDTH,
|
||||
MODEL_COL_WIDTH,
|
||||
TABLE_TOTAL_WIDTH - ((2 * 40) - 4),
|
||||
],
|
||||
size=(0, TABLE_HEIGHT),
|
||||
expand_x=True,
|
||||
enable_click_events=True,
|
||||
),
|
||||
sg.Multiline(size=(40, TABLE_HEIGHT + 1), key="cfg_config_txt"),
|
||||
],
|
||||
]
|
||||
return config_layout
|
||||
|
||||
|
||||
layout = [
|
||||
[
|
||||
sg.Text("", size=(20, 1), key="status"),
|
||||
sg.ProgressBar(
|
||||
max_value=100, size_px=(0, 20), expand_x=True, key="progress_bar"
|
||||
),
|
||||
sg.Text("", size=(20, 1), key="progress_percent", justification="r"),
|
||||
],
|
||||
[
|
||||
sg.TabGroup(
|
||||
[
|
||||
[sg.Tab("Scan", get_scan_layout())],
|
||||
[sg.Tab("Pools", get_pools_layout())],
|
||||
[sg.Tab("Configure", get_config_layout())],
|
||||
[sg.Tab("Command", get_command_layout())],
|
||||
]
|
||||
)
|
||||
],
|
||||
]
|
||||
|
||||
window = sg.Window("Upstream Config Util", layout, icon=WINDOW_ICON)
|
||||
95
tools/cfg_util/scan/__init__.py
Normal file
95
tools/cfg_util/scan/__init__.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import asyncio
|
||||
|
||||
from miners.miner_factory import MinerFactory
|
||||
from network import MinerNetwork
|
||||
from tools.cfg_util.decorators import disable_buttons
|
||||
from tools.cfg_util.layout import window, update_prog_bar
|
||||
from tools.cfg_util.tables import clear_tables, TableManager
|
||||
|
||||
progress_bar_len = 0
|
||||
|
||||
DEFAULT_DATA = [
|
||||
"Model",
|
||||
"Hostname",
|
||||
"Hashrate",
|
||||
"Temperature",
|
||||
"Pool User",
|
||||
"Pool 1",
|
||||
"Pool 1 User",
|
||||
"Pool 2",
|
||||
"Pool 2 User",
|
||||
"Wattage",
|
||||
"Split",
|
||||
]
|
||||
|
||||
|
||||
async def btn_all():
|
||||
table = "scan_table"
|
||||
window[table].update(
|
||||
select_rows=([row for row in range(len(window[table].Values))])
|
||||
)
|
||||
|
||||
|
||||
async def btn_scan(scan_ip: str):
|
||||
network = MinerNetwork("192.168.1.0")
|
||||
if scan_ip:
|
||||
if "/" in scan_ip:
|
||||
ip, mask = scan_ip.split("/")
|
||||
network = MinerNetwork(ip, mask=mask)
|
||||
else:
|
||||
network = MinerNetwork(scan_ip)
|
||||
asyncio.create_task(_scan_miners(network))
|
||||
|
||||
|
||||
@disable_buttons("Scanning")
|
||||
async def _scan_miners(network: MinerNetwork):
|
||||
clear_tables()
|
||||
scan_generator = network.scan_network_generator()
|
||||
MinerFactory().clear_cached_miners()
|
||||
|
||||
global progress_bar_len
|
||||
progress_bar_len = 0
|
||||
|
||||
network_size = len(network)
|
||||
await update_prog_bar(progress_bar_len, max=(3 * network_size))
|
||||
|
||||
scanned_miners = []
|
||||
async for miner in scan_generator:
|
||||
if miner:
|
||||
scanned_miners.append(miner)
|
||||
progress_bar_len += 1
|
||||
await update_prog_bar(progress_bar_len)
|
||||
|
||||
progress_bar_len += network_size - len(scanned_miners)
|
||||
await update_prog_bar(progress_bar_len)
|
||||
|
||||
get_miner_genenerator = MinerFactory().get_miner_generator(scanned_miners)
|
||||
|
||||
resolved_miners = []
|
||||
async for found_miner in get_miner_genenerator:
|
||||
resolved_miners.append(found_miner)
|
||||
resolved_miners.sort(key=lambda x: x.ip)
|
||||
_data = {}
|
||||
for key in DEFAULT_DATA:
|
||||
_data[key] = ""
|
||||
_data["IP"] = str(found_miner.ip)
|
||||
TableManager().update_item(_data)
|
||||
progress_bar_len += 1
|
||||
await update_prog_bar(progress_bar_len)
|
||||
progress_bar_len += network_size - len(resolved_miners)
|
||||
await update_prog_bar(progress_bar_len)
|
||||
await _get_miners_data(resolved_miners)
|
||||
|
||||
|
||||
async def _get_miners_data(miners: list):
|
||||
global progress_bar_len
|
||||
data_generator = asyncio.as_completed([_get_data(miner) for miner in miners])
|
||||
for all_data in data_generator:
|
||||
data = await all_data
|
||||
TableManager().update_item(data)
|
||||
progress_bar_len += 1
|
||||
await update_prog_bar(progress_bar_len)
|
||||
|
||||
|
||||
async def _get_data(miner):
|
||||
return await miner.get_data()
|
||||
183
tools/cfg_util/tables.py
Normal file
183
tools/cfg_util/tables.py
Normal file
@@ -0,0 +1,183 @@
|
||||
from tools.cfg_util.layout import (
|
||||
MINER_COUNT_BUTTONS,
|
||||
HASHRATE_TOTAL_BUTTONS,
|
||||
SORT_KEY_BUTTONS,
|
||||
TABLE_KEYS,
|
||||
TABLE_HEADERS,
|
||||
window,
|
||||
)
|
||||
from tools.cfg_util.imgs import TkImages, LIGHT, FAULT_LIGHT
|
||||
import PySimpleGUI as sg
|
||||
import ipaddress
|
||||
|
||||
|
||||
def update_miner_count(count):
|
||||
for button in MINER_COUNT_BUTTONS:
|
||||
window[button].update(f"Miners: {count}")
|
||||
|
||||
|
||||
def update_total_hr(hashrate: float):
|
||||
if hashrate > 999:
|
||||
hashrate = f"{round(hashrate/1000, 2)} PH/s"
|
||||
else:
|
||||
hashrate = f"{round(hashrate)} TH/s"
|
||||
for button in HASHRATE_TOTAL_BUTTONS:
|
||||
window[button].update(f"Hashrate: {hashrate}")
|
||||
|
||||
|
||||
def update_sort_key_btns():
|
||||
for button in SORT_KEY_BUTTONS:
|
||||
window[button].update(f"Sort: {TableManager().sort_key}")
|
||||
|
||||
|
||||
def update_tables(data: list or None = None):
|
||||
TableManager().update_data(data)
|
||||
|
||||
|
||||
def clear_tables():
|
||||
TableManager().clear_tables()
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class TableManager(metaclass=Singleton):
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
self.images = TkImages()
|
||||
self.data = {}
|
||||
self.sort_key = "IP"
|
||||
self.sort_reverse = False
|
||||
|
||||
def update_data(self, data: list):
|
||||
if not data:
|
||||
return
|
||||
|
||||
for line in data:
|
||||
self.update_item(line)
|
||||
|
||||
def update_sort_key(self, sort_key):
|
||||
if self.sort_key == sort_key:
|
||||
self.sort_reverse = not self.sort_reverse
|
||||
self.sort_key = sort_key
|
||||
update_sort_key_btns()
|
||||
self.update_tables()
|
||||
|
||||
def update_item(self, data: dict):
|
||||
if not data or data == {} or not data.get("IP"):
|
||||
return
|
||||
|
||||
if not data.get("Light"):
|
||||
data["Light"] = False
|
||||
|
||||
if not data["IP"] in self.data.keys():
|
||||
self.data[data["IP"]] = {}
|
||||
|
||||
for key in data.keys():
|
||||
self.data[data["IP"]][key] = data[key]
|
||||
|
||||
self.update_tables()
|
||||
|
||||
def update_tables(self):
|
||||
tables = {
|
||||
"SCAN": [["" for _ in TABLE_HEADERS["SCAN"]] for _ in self.data],
|
||||
"CMD": [["" for _ in TABLE_HEADERS["CMD"]] for _ in self.data],
|
||||
"POOLS_ALL": [["" for _ in TABLE_HEADERS["POOLS_ALL"]] for _ in self.data],
|
||||
"POOLS_1": [["" for _ in TABLE_HEADERS["POOLS_1"]] for _ in self.data],
|
||||
"POOLS_2": [["" for _ in TABLE_HEADERS["POOLS_2"]] for _ in self.data],
|
||||
"CONFIG": [["" for _ in TABLE_HEADERS["CONFIG"]] for _ in self.data],
|
||||
}
|
||||
|
||||
ip_sorted_keys = sorted(self.data.keys(), key=lambda x: ipaddress.ip_address(x))
|
||||
sorted_keys = sorted(
|
||||
ip_sorted_keys, reverse=self.sort_reverse, key=lambda x: self._get_sort(x)
|
||||
)
|
||||
|
||||
for data_idx, key in enumerate(sorted_keys):
|
||||
item = self.data[key]
|
||||
keys = item.keys()
|
||||
|
||||
if "Hashrate" in keys:
|
||||
if not isinstance(item["Hashrate"], str):
|
||||
item[
|
||||
"Hashrate"
|
||||
] = f"{format(float(item['Hashrate']), '.2f').rjust(6, ' ')} TH/s"
|
||||
for key in keys:
|
||||
for table in TABLE_HEADERS.keys():
|
||||
for idx, header in enumerate(TABLE_HEADERS[table]):
|
||||
if key == header:
|
||||
tables[table][data_idx][idx] = item[key]
|
||||
|
||||
window["scan_table"].update(tables["SCAN"])
|
||||
window["pools_table"].update(tables["POOLS_ALL"])
|
||||
window["pools_1_table"].update(tables["POOLS_1"])
|
||||
window["pools_2_table"].update(tables["POOLS_2"])
|
||||
window["cfg_table"].update(tables["CONFIG"])
|
||||
|
||||
treedata = sg.TreeData()
|
||||
for idx, item in enumerate(tables["CMD"]):
|
||||
ico = LIGHT
|
||||
if self.data[item[0]]["Light"]:
|
||||
ico = FAULT_LIGHT
|
||||
treedata.insert("", idx, "", item, icon=ico)
|
||||
|
||||
window["cmd_table"].update(treedata)
|
||||
|
||||
update_miner_count(len(self.data))
|
||||
total_hr = 0
|
||||
for key in self.data.keys():
|
||||
hashrate = 0
|
||||
if not self.data[key]["Hashrate"] == "":
|
||||
hashrate = (
|
||||
self.data[key]["Hashrate"].replace(" ", "").replace("TH/s", "")
|
||||
)
|
||||
total_hr += float(hashrate)
|
||||
update_total_hr(round(total_hr))
|
||||
|
||||
def _get_sort(self, data_key: str):
|
||||
if self.sort_key not in self.data[data_key]:
|
||||
return ""
|
||||
|
||||
if self.sort_key == "IP":
|
||||
return ipaddress.ip_address(self.data[data_key]["IP"])
|
||||
|
||||
if self.sort_key == "Hashrate":
|
||||
if self.data[data_key]["Hashrate"] == "":
|
||||
return -1
|
||||
if not isinstance(self.data[data_key]["Hashrate"], str):
|
||||
return self.data[data_key]["Hashrate"]
|
||||
return float(
|
||||
self.data[data_key]["Hashrate"].replace(" ", "").replace("TH/s", "")
|
||||
)
|
||||
|
||||
if self.sort_key in ["Wattage", "Temperature"]:
|
||||
if isinstance(self.data[data_key][self.sort_key], str):
|
||||
return -300
|
||||
|
||||
if self.sort_key == "Split":
|
||||
if self.data[data_key][self.sort_key] == "":
|
||||
return -1
|
||||
if "/" not in self.data[data_key][self.sort_key]:
|
||||
return 0
|
||||
|
||||
if not self.sort_reverse:
|
||||
return int(self.data[data_key][self.sort_key].split("/")[0])
|
||||
else:
|
||||
return int(self.data[data_key][self.sort_key].split("/")[1])
|
||||
|
||||
return self.data[data_key][self.sort_key]
|
||||
|
||||
def clear_tables(self):
|
||||
self.data = {}
|
||||
for table in TABLE_KEYS["table"]:
|
||||
window[table].update([])
|
||||
for tree in TABLE_KEYS["tree"]:
|
||||
window[tree].update(sg.TreeData())
|
||||
update_miner_count(0)
|
||||
138
tools/cfg_util/ui.py
Normal file
138
tools/cfg_util/ui.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import PySimpleGUI as sg
|
||||
import asyncio
|
||||
import sys
|
||||
from tools.cfg_util.imgs import TkImages
|
||||
from tools.cfg_util.scan import btn_scan
|
||||
from tools.cfg_util.commands import (
|
||||
btn_light,
|
||||
btn_reboot,
|
||||
btn_backend,
|
||||
btn_command,
|
||||
)
|
||||
from tools.cfg_util.configure import (
|
||||
generate_config_ui,
|
||||
btn_import,
|
||||
btn_config,
|
||||
)
|
||||
from tools.cfg_util.layout import window
|
||||
from tools.cfg_util.general import btn_all, btn_web, btn_refresh
|
||||
from tools.cfg_util.tables import TableManager
|
||||
import tkinter as tk
|
||||
|
||||
|
||||
def _tree_header_click_handler(event, table):
|
||||
region = table.Widget.identify("region", event.x, event.y)
|
||||
if region == "heading":
|
||||
col = int(table.Widget.identify_column(event.x)[1:]) - 1
|
||||
|
||||
if col == -1:
|
||||
# handle the "Light" column, which needs a key of #0
|
||||
col = "#0"
|
||||
|
||||
heading = table.Widget.heading(col)["text"]
|
||||
|
||||
mgr = TableManager()
|
||||
mgr.update_sort_key(heading)
|
||||
|
||||
|
||||
async def ui():
|
||||
window.read(0)
|
||||
|
||||
# create images used in the table, they will not show if not saved here
|
||||
tk_imgs = TkImages()
|
||||
|
||||
# left justify hostnames
|
||||
window["scan_table"].Widget.column(2, anchor=tk.W)
|
||||
|
||||
# cmd table sort event
|
||||
window["cmd_table"].Widget.bind(
|
||||
"<Button-1>", lambda x: _tree_header_click_handler(x, window["cmd_table"])
|
||||
)
|
||||
|
||||
while True:
|
||||
event, value = window.read(0)
|
||||
if event in (None, "Close", sg.WIN_CLOSED):
|
||||
sys.exit()
|
||||
|
||||
if isinstance(event, tuple):
|
||||
if event[0].endswith("_table"):
|
||||
if event[2][0] == -1:
|
||||
mgr = TableManager()
|
||||
table = window[event[0]].Widget
|
||||
mgr.update_sort_key(table.heading(event[2][1])["text"])
|
||||
|
||||
# scan tab
|
||||
|
||||
if event == "scan_all":
|
||||
_table = "scan_table"
|
||||
btn_all(_table, value[_table])
|
||||
if event == "scan_web":
|
||||
_table = "scan_table"
|
||||
btn_web(_table, value[_table])
|
||||
if event == "scan_refresh":
|
||||
_table = "scan_table"
|
||||
asyncio.create_task(btn_refresh(_table, value[_table]))
|
||||
if event == "btn_scan":
|
||||
asyncio.create_task(btn_scan(value["scan_ip"]))
|
||||
|
||||
# pools tab
|
||||
if event == "pools_all":
|
||||
_table = "pools_table"
|
||||
btn_all(_table, value[_table])
|
||||
if event == "pools_web":
|
||||
_table = "pools_table"
|
||||
btn_web(_table, value[_table])
|
||||
if event == "pools_refresh":
|
||||
_table = "pools_table"
|
||||
asyncio.create_task(btn_refresh(_table, value[_table]))
|
||||
|
||||
# configure tab
|
||||
if event == "cfg_all":
|
||||
_table = "cfg_table"
|
||||
btn_all(_table, value[_table])
|
||||
if event == "cfg_web":
|
||||
_table = "cfg_table"
|
||||
btn_web(_table, value[_table])
|
||||
if event == "cfg_generate":
|
||||
await generate_config_ui()
|
||||
if event == "cfg_import":
|
||||
_table = "cfg_table"
|
||||
asyncio.create_task(btn_import(_table, value[_table]))
|
||||
if event == "cfg_config":
|
||||
_table = "cfg_table"
|
||||
asyncio.create_task(
|
||||
btn_config(
|
||||
_table,
|
||||
value[_table],
|
||||
value["cfg_config_txt"],
|
||||
value["cfg_append_ip"],
|
||||
)
|
||||
)
|
||||
|
||||
# commands tab
|
||||
if event == "cmd_all":
|
||||
_table = "cmd_table"
|
||||
btn_all(_table, value[_table])
|
||||
if event == "cmd_light":
|
||||
_table = "cmd_table"
|
||||
_ips = value[_table]
|
||||
asyncio.create_task(btn_light(_ips))
|
||||
if event == "cmd_reboot":
|
||||
_table = "cmd_table"
|
||||
_ips = value[_table]
|
||||
asyncio.create_task(btn_reboot(_ips))
|
||||
if event == "cmd_backend":
|
||||
_table = "cmd_table"
|
||||
_ips = value[_table]
|
||||
asyncio.create_task(btn_backend(_ips))
|
||||
if event == "btn_cmd":
|
||||
_table = "cmd_table"
|
||||
_ips = value[_table]
|
||||
asyncio.create_task(btn_command(_ips, value["cmd_txt"]))
|
||||
|
||||
if event == "__TIMEOUT__":
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(ui())
|
||||
1
tools/cfg_util_old/__init__.py
Normal file
1
tools/cfg_util_old/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from tools.cfg_util_old.cfg_util_sg import main
|
||||
26
tools/cfg_util_old/cfg_util_sg/__init__.py
Normal file
26
tools/cfg_util_old/cfg_util_sg/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from tools.cfg_util_old.cfg_util_sg.ui import ui
|
||||
|
||||
|
||||
# initialize logger and get settings
|
||||
from logger import logger
|
||||
|
||||
logger.info("Initializing logger for CFG Util.")
|
||||
|
||||
# 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():
|
||||
logging.info("Starting CFG Util.")
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(ui())
|
||||
logging.info("Closing CFG Util.")
|
||||
0
tools/cfg_util_old/cfg_util_sg/func/__init__.py
Normal file
0
tools/cfg_util_old/cfg_util_sg/func/__init__.py
Normal file
37
tools/cfg_util_old/cfg_util_sg/func/decorators.py
Normal file
37
tools/cfg_util_old/cfg_util_sg/func/decorators.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from tools.cfg_util_old.cfg_util_sg.layout import window
|
||||
|
||||
|
||||
def disable_buttons(func):
|
||||
button_list = [
|
||||
"scan",
|
||||
"import_file_config",
|
||||
"export_file_config",
|
||||
"import_iplist",
|
||||
"export_iplist",
|
||||
"export_csv",
|
||||
"select_all_ips",
|
||||
"refresh_data",
|
||||
"open_in_web",
|
||||
"reboot_miners",
|
||||
"restart_miner_backend",
|
||||
"import_config",
|
||||
"send_config",
|
||||
"light",
|
||||
"generate_config",
|
||||
"send_miner_ssh_command_window",
|
||||
]
|
||||
|
||||
# handle the inner function that the decorator is wrapping
|
||||
async def inner(*args, **kwargs):
|
||||
# disable the buttons
|
||||
for button in button_list:
|
||||
window[button].Update(disabled=True)
|
||||
|
||||
# call the original wrapped function
|
||||
await func(*args, **kwargs)
|
||||
|
||||
# re-enable the buttons after the wrapped function completes
|
||||
for button in button_list:
|
||||
window[button].Update(disabled=False)
|
||||
|
||||
return inner
|
||||
96
tools/cfg_util_old/cfg_util_sg/func/files.py
Normal file
96
tools/cfg_util_old/cfg_util_sg/func/files.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import aiofiles
|
||||
import toml
|
||||
|
||||
from tools.cfg_util_old.cfg_util_sg.func.ui import update_ui_with_data
|
||||
from tools.cfg_util_old.cfg_util_sg.layout import window
|
||||
from config.bos import bos_config_convert, general_config_convert_bos
|
||||
|
||||
|
||||
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_csv(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(", ".join([str(part).rstrip().lstrip() for part in item]))
|
||||
+ "\n"
|
||||
)
|
||||
else:
|
||||
async with aiofiles.open(file_location, mode="w") as file:
|
||||
for item in window["ip_table"].Values:
|
||||
await file.write(
|
||||
str(", ".join([str(part).rstrip().lstrip() for part in item]))
|
||||
+ "\n"
|
||||
)
|
||||
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 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", 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.dumps(general_config_convert_bos(config))
|
||||
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(config)
|
||||
await update_ui_with_data("status", "")
|
||||
583
tools/cfg_util_old/cfg_util_sg/func/miners.py
Normal file
583
tools/cfg_util_old/cfg_util_sg/func/miners.py
Normal file
@@ -0,0 +1,583 @@
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import time
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
from API import APIError
|
||||
from tools.cfg_util_old.cfg_util_sg.func.parse_data import safe_parse_api_data
|
||||
from tools.cfg_util_old.cfg_util_sg.func.ui import (
|
||||
update_ui_with_data,
|
||||
update_prog_bar,
|
||||
set_progress_bar_len,
|
||||
)
|
||||
from tools.cfg_util_old.cfg_util_sg.layout import window
|
||||
from miners.miner_factory import MinerFactory
|
||||
from config.bos import bos_config_convert
|
||||
from tools.cfg_util_old.cfg_util_sg.func.decorators import disable_buttons
|
||||
from settings import (
|
||||
CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS,
|
||||
CFG_UTIL_REBOOT_THREADS as REBOOT_THREADS,
|
||||
)
|
||||
|
||||
|
||||
async def import_config(idx):
|
||||
await update_ui_with_data("status", "Importing")
|
||||
miner_ip = window["ip_table"].Values[idx[0]][0]
|
||||
logging.debug(f"{miner_ip}: Importing config.")
|
||||
miner = await MinerFactory().get_miner(ipaddress.ip_address(miner_ip))
|
||||
await miner.get_config()
|
||||
config = miner.config
|
||||
await update_ui_with_data("config", str(config))
|
||||
logging.debug(f"{miner_ip}: Config import completed.")
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def scan_network(network):
|
||||
await update_ui_with_data("status", "Scanning")
|
||||
await update_ui_with_data("ip_count", "")
|
||||
await update_ui_with_data("hr_total", "")
|
||||
window["ip_table"].update([])
|
||||
network_size = len(network)
|
||||
miner_generator = network.scan_network_generator()
|
||||
await set_progress_bar_len(2 * network_size)
|
||||
progress_bar_len = 0
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
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 = MinerFactory().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", "")
|
||||
|
||||
|
||||
@disable_buttons
|
||||
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 MinerFactory().get_miner(ip)
|
||||
index = [item[0] for item in window["ip_table"].Values].index(ip)
|
||||
index_tags = ip_list.item(index + 1)["tags"]
|
||||
if "light" not in index_tags:
|
||||
index_tags.append("light")
|
||||
ip_list.item(index + 1, tags=index_tags)
|
||||
await miner.fault_light_on()
|
||||
else:
|
||||
index_tags.remove("light")
|
||||
ip_list.item(index + 1, tags=index_tags)
|
||||
await miner.fault_light_off()
|
||||
|
||||
|
||||
async def send_command_generator(miners: list, command: str):
|
||||
loop = asyncio.get_event_loop()
|
||||
command_tasks = []
|
||||
for miner in miners:
|
||||
if len(command_tasks) >= CONFIG_THREADS:
|
||||
cmd_sent = asyncio.as_completed(command_tasks)
|
||||
command_tasks = []
|
||||
for done in cmd_sent:
|
||||
yield await done
|
||||
command_tasks.append(loop.create_task(send_ssh_command(miner, command)))
|
||||
cmd_sent = asyncio.as_completed(command_tasks)
|
||||
for done in cmd_sent:
|
||||
yield await done
|
||||
|
||||
|
||||
async def send_ssh_command(miner, command: str):
|
||||
proc = await miner.send_ssh_command(command)
|
||||
return {"IP": miner.ip, "proc": proc}
|
||||
|
||||
|
||||
async def send_miners_ssh_commands(ips: list, command: str, ssh_cmd_window):
|
||||
get_miner_genenerator = MinerFactory().get_miner_generator(ips)
|
||||
all_miners = []
|
||||
async for miner in get_miner_genenerator:
|
||||
all_miners.append(miner)
|
||||
|
||||
data = []
|
||||
send_cmd_generator = send_command_generator(all_miners, command)
|
||||
async for command_sent in send_cmd_generator:
|
||||
data.append(command_sent)
|
||||
|
||||
proc_table_data = [[ip, ""] for ip in ips]
|
||||
for item in data:
|
||||
|
||||
if item["proc"].returncode == 0:
|
||||
return_data = item["proc"].stdout
|
||||
else:
|
||||
return_data = item["proc"].stderr
|
||||
if str(item["IP"]) in ips:
|
||||
proc_table_index = ips.index(str(item["IP"]))
|
||||
proc_table_data[proc_table_index] = [
|
||||
str(item["IP"]),
|
||||
return_data.replace("\n", " "),
|
||||
]
|
||||
ssh_cmd_window["ssh_cmd_table"].update(proc_table_data)
|
||||
|
||||
|
||||
async def reboot_generator(miners: list):
|
||||
loop = asyncio.get_event_loop()
|
||||
reboot_tasks = []
|
||||
for miner in miners:
|
||||
if len(reboot_tasks) >= REBOOT_THREADS:
|
||||
rebooted = asyncio.as_completed(reboot_tasks)
|
||||
reboot_tasks = []
|
||||
for done in rebooted:
|
||||
yield await done
|
||||
reboot_tasks.append(loop.create_task(miner.reboot()))
|
||||
rebooted = asyncio.as_completed(reboot_tasks)
|
||||
for done in rebooted:
|
||||
yield await done
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def reboot_miners(ips: list):
|
||||
await update_ui_with_data("status", "Rebooting")
|
||||
await set_progress_bar_len(2 * len(ips))
|
||||
progress_bar_len = 0
|
||||
get_miner_genenerator = MinerFactory().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))
|
||||
|
||||
reboot_miners_generator = reboot_generator(all_miners)
|
||||
async for _rebooter in reboot_miners_generator:
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def restart_backend_generator(miners: list):
|
||||
loop = asyncio.get_event_loop()
|
||||
reboot_tasks = []
|
||||
for miner in miners:
|
||||
if len(reboot_tasks) >= REBOOT_THREADS:
|
||||
rebooted = asyncio.as_completed(reboot_tasks)
|
||||
reboot_tasks = []
|
||||
for done in rebooted:
|
||||
yield await done
|
||||
reboot_tasks.append(loop.create_task(miner.restart_backend()))
|
||||
rebooted = asyncio.as_completed(reboot_tasks)
|
||||
for done in rebooted:
|
||||
yield await done
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def restart_miners_backend(ips: list):
|
||||
await update_ui_with_data("status", "Restarting Backends")
|
||||
await set_progress_bar_len(2 * len(ips))
|
||||
progress_bar_len = 0
|
||||
get_miner_genenerator = MinerFactory().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))
|
||||
|
||||
restart_backend_gen = restart_backend_generator(all_miners)
|
||||
async for _rebooter in restart_backend_gen:
|
||||
progress_bar_len += 1
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def send_config_generator(miners: list, config, last_octet_ip_user: bool = False):
|
||||
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, ip_user=last_octet_ip_user))
|
||||
)
|
||||
configured = asyncio.as_completed(config_tasks)
|
||||
for sent_config in configured:
|
||||
yield await sent_config
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def send_config(ips: list, config, last_octet_ip: bool = False):
|
||||
await update_ui_with_data("status", "Configuring")
|
||||
await set_progress_bar_len(2 * len(ips))
|
||||
progress_bar_len = 0
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
get_miner_genenerator = MinerFactory().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, last_octet_ip_user=last_octet_ip
|
||||
)
|
||||
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", "Getting Data")
|
||||
await asyncio.sleep(3)
|
||||
await refresh_data(ips)
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def refresh_data(ip_list: list):
|
||||
await update_ui_with_data("status", "Getting Data")
|
||||
await update_ui_with_data("hr_total", "")
|
||||
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
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
reset_table_values = []
|
||||
for item in window["ip_table"].Values:
|
||||
if item[0] in ip_list:
|
||||
reset_table_values.append([item[0]])
|
||||
else:
|
||||
reset_table_values.append(item)
|
||||
window["ip_table"].update(reset_table_values)
|
||||
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["model"],
|
||||
data_point["host"],
|
||||
str(data_point["TH/s"]) + " TH/s ",
|
||||
data_point["temp"],
|
||||
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 = []
|
||||
hr_idx = 3
|
||||
for item, _ in enumerate(window["ip_table"].Values):
|
||||
if len(window["ip_table"].Values[item]) > hr_idx:
|
||||
if not window["ip_table"].Values[item][hr_idx] == "":
|
||||
hashrate_list.append(
|
||||
float(window["ip_table"].Values[item][hr_idx].replace(" TH/s ", ""))
|
||||
)
|
||||
else:
|
||||
hashrate_list.append(0)
|
||||
else:
|
||||
hashrate_list.append(0)
|
||||
|
||||
total_hr = round(sum(hashrate_list), 2)
|
||||
window["hr_total"].update(f"{total_hr} TH/s")
|
||||
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def scan_and_get_data(network):
|
||||
await update_ui_with_data("status", "Scanning")
|
||||
await update_ui_with_data("hr_total", "")
|
||||
await update_ui_with_data("ip_count", "")
|
||||
await update_ui_with_data("ip_table", [])
|
||||
network_size = len(network)
|
||||
miner_generator = network.scan_network_generator()
|
||||
MinerFactory().clear_cached_miners()
|
||||
|
||||
logging.info(f"Scanning network: {str(network)}")
|
||||
|
||||
await set_progress_bar_len(3 * 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))
|
||||
logging.info(f"Found {len(miners)} Miners")
|
||||
logging.debug(f"Found miners: {miners}")
|
||||
progress_bar_len += network_size - len(miners)
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
get_miner_genenerator = MinerFactory().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))
|
||||
logging.info(f"Resolved {len(all_miners)} Miners")
|
||||
logging.debug(f"Resolved to miner types: {all_miners}")
|
||||
await update_ui_with_data("ip_count", str(len(all_miners)))
|
||||
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in miners])
|
||||
ip_table_data = window["ip_table"].Values
|
||||
ordered_all_ips = [item[0] for item in ip_table_data]
|
||||
progress_bar_len += network_size - len(miners)
|
||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||
await update_ui_with_data("status", "Getting Data")
|
||||
logging.debug("Getting data on miners.")
|
||||
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["model"],
|
||||
data_point["host"],
|
||||
str(data_point["TH/s"]) + " TH/s ",
|
||||
data_point["temp"],
|
||||
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[3].replace(" TH/s ", ""))
|
||||
for item in window["ip_table"].Values
|
||||
if not item[3] == ""
|
||||
]
|
||||
total_hr = round(sum(hashrate_list), 2)
|
||||
await update_ui_with_data("hr_total", f"{total_hr} TH/s")
|
||||
await update_ui_with_data("status", "")
|
||||
|
||||
|
||||
async def get_formatted_data(ip: ipaddress.ip_address):
|
||||
miner = await MinerFactory().get_miner(ip)
|
||||
logging.debug(f"Getting data for miner: {miner.ip}")
|
||||
warnings.filterwarnings("ignore")
|
||||
miner_data = None
|
||||
host = await miner.get_hostname()
|
||||
try:
|
||||
model = await miner.get_model()
|
||||
except APIError:
|
||||
model = "?"
|
||||
if not model:
|
||||
model = "?"
|
||||
temps = 0
|
||||
th5s = 0
|
||||
wattage = 0
|
||||
user = "?"
|
||||
|
||||
try:
|
||||
miner_data = await miner.api.multicommand(
|
||||
"summary", "devs", "temps", "tunerstatus", "pools", "stats"
|
||||
)
|
||||
except APIError:
|
||||
try:
|
||||
# no devs command, it will fail in this case
|
||||
miner_data = await miner.api.multicommand(
|
||||
"summary", "temps", "tunerstatus", "pools", "stats"
|
||||
)
|
||||
except APIError as e:
|
||||
logging.warning(f"{str(ip)}: {e}")
|
||||
return {
|
||||
"TH/s": 0,
|
||||
"IP": str(miner.ip),
|
||||
"model": "Unknown",
|
||||
"temp": 0,
|
||||
"host": "Unknown",
|
||||
"user": "Unknown",
|
||||
"wattage": 0,
|
||||
}
|
||||
if miner_data:
|
||||
logging.info(f"Received miner data for miner: {miner.ip}")
|
||||
# get all data from summary
|
||||
if "summary" in miner_data.keys():
|
||||
if (
|
||||
not miner_data["summary"][0].get("SUMMARY") == []
|
||||
and "SUMMARY" in miner_data["summary"][0].keys()
|
||||
):
|
||||
# temperature data, this is the idea spot to get this
|
||||
if "Temperature" in miner_data["summary"][0]["SUMMARY"][0].keys():
|
||||
if (
|
||||
not round(miner_data["summary"][0]["SUMMARY"][0]["Temperature"])
|
||||
== 0
|
||||
):
|
||||
temps = miner_data["summary"][0]["SUMMARY"][0]["Temperature"]
|
||||
# hashrate data
|
||||
if "MHS av" in miner_data["summary"][0]["SUMMARY"][0].keys():
|
||||
th5s = format(
|
||||
round(
|
||||
await safe_parse_api_data(
|
||||
miner_data, "summary", 0, "SUMMARY", 0, "MHS av"
|
||||
)
|
||||
/ 1000000,
|
||||
2,
|
||||
),
|
||||
".2f",
|
||||
).rjust(6, " ")
|
||||
elif "GHS av" in miner_data["summary"][0]["SUMMARY"][0].keys():
|
||||
if not miner_data["summary"][0]["SUMMARY"][0]["GHS av"] == "":
|
||||
th5s = format(
|
||||
round(
|
||||
float(
|
||||
await safe_parse_api_data(
|
||||
miner_data, "summary", 0, "SUMMARY", 0, "GHS av"
|
||||
)
|
||||
)
|
||||
/ 1000,
|
||||
2,
|
||||
),
|
||||
".2f",
|
||||
).rjust(6, " ")
|
||||
|
||||
# alternate temperature data, for BraiinsOS
|
||||
if "temps" in miner_data.keys():
|
||||
if not miner_data["temps"][0].get("TEMPS") == []:
|
||||
if "Chip" in miner_data["temps"][0]["TEMPS"][0].keys():
|
||||
for board in miner_data["temps"][0]["TEMPS"]:
|
||||
if board["Chip"] is not None and not board["Chip"] == 0.0:
|
||||
temps = board["Chip"]
|
||||
# alternate temperature data, for Whatsminers
|
||||
if "devs" in miner_data.keys():
|
||||
if not miner_data["devs"][0].get("DEVS") == []:
|
||||
if "Chip Temp Avg" in miner_data["devs"][0]["DEVS"][0].keys():
|
||||
for board in miner_data["devs"][0]["DEVS"]:
|
||||
if (
|
||||
board["Chip Temp Avg"] is not None
|
||||
and not board["Chip Temp Avg"] == 0.0
|
||||
):
|
||||
temps = board["Chip Temp Avg"]
|
||||
# alternate temperature data
|
||||
if "stats" in miner_data.keys():
|
||||
if not miner_data["stats"][0]["STATS"] == []:
|
||||
for temp in ["temp2", "temp1", "temp3"]:
|
||||
if temp in miner_data["stats"][0]["STATS"][1].keys():
|
||||
if (
|
||||
miner_data["stats"][0]["STATS"][1][temp] is not None
|
||||
and not miner_data["stats"][0]["STATS"][1][temp] == 0.0
|
||||
):
|
||||
temps = miner_data["stats"][0]["STATS"][1][temp]
|
||||
# alternate temperature data, for Avalonminers
|
||||
miner_data["stats"][0]["STATS"][0].keys()
|
||||
if any(
|
||||
"MM ID" in string
|
||||
for string in miner_data["stats"][0]["STATS"][0].keys()
|
||||
):
|
||||
temp_all = []
|
||||
for key in [
|
||||
string
|
||||
for string in miner_data["stats"][0]["STATS"][0].keys()
|
||||
if "MM ID" in string
|
||||
]:
|
||||
for value in [
|
||||
string
|
||||
for string in miner_data["stats"][0]["STATS"][0][key].split(" ")
|
||||
if "TMax" in string
|
||||
]:
|
||||
temp_all.append(int(value.split("[")[1].replace("]", "")))
|
||||
temps = round(sum(temp_all) / len(temp_all))
|
||||
|
||||
# pool information
|
||||
if "pools" in miner_data.keys():
|
||||
if not miner_data["pools"][0].get("POOLS") == []:
|
||||
user = await safe_parse_api_data(
|
||||
miner_data, "pools", 0, "POOLS", 0, "User"
|
||||
)
|
||||
else:
|
||||
print(miner_data["pools"][0])
|
||||
user = "Blank"
|
||||
|
||||
# braiins tuner status / wattage
|
||||
if "tunerstatus" in miner_data.keys():
|
||||
wattage = await safe_parse_api_data(
|
||||
miner_data, "tunerstatus", 0, "TUNERSTATUS", 0, "PowerLimit"
|
||||
)
|
||||
elif "Power" in miner_data["summary"][0]["SUMMARY"][0].keys():
|
||||
wattage = await safe_parse_api_data(
|
||||
miner_data, "summary", 0, "SUMMARY", 0, "Power"
|
||||
)
|
||||
|
||||
ret_data = {
|
||||
"TH/s": th5s,
|
||||
"IP": str(miner.ip),
|
||||
"model": model,
|
||||
"temp": round(temps),
|
||||
"host": host,
|
||||
"user": user,
|
||||
"wattage": wattage,
|
||||
}
|
||||
|
||||
logging.debug(f"{ret_data}")
|
||||
|
||||
return ret_data
|
||||
|
||||
|
||||
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(bos_config_convert(config))
|
||||
@@ -4,7 +4,7 @@ from API import APIError
|
||||
# noinspection PyPep8
|
||||
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 len(path) == idx + 1:
|
||||
if isinstance(path[idx], str):
|
||||
if isinstance(data, dict):
|
||||
if path[idx] in data.keys():
|
||||
@@ -17,34 +17,50 @@ async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int =
|
||||
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)
|
||||
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
|
||||
# noinspection PyPep8
|
||||
if parsed_data == None:
|
||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||
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}")
|
||||
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}")
|
||||
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)
|
||||
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
|
||||
# noinspection PyPep8
|
||||
if parsed_data == None:
|
||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||
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}")
|
||||
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}")
|
||||
raise APIError(
|
||||
f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}"
|
||||
)
|
||||
return False
|
||||
124
tools/cfg_util_old/cfg_util_sg/func/ui.py
Normal file
124
tools/cfg_util_old/cfg_util_sg/func/ui.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
from tools.cfg_util_old.cfg_util_sg.layout import window
|
||||
|
||||
import pyperclip
|
||||
|
||||
|
||||
def table_select_all():
|
||||
window["ip_table"].update(
|
||||
select_rows=([row for row in range(len(window["ip_table"].Values))])
|
||||
)
|
||||
|
||||
|
||||
def copy_from_table(table):
|
||||
selection = table.selection()
|
||||
copy_values = []
|
||||
for each in selection:
|
||||
try:
|
||||
value = table.item(each)["values"][0]
|
||||
copy_values.append(str(value))
|
||||
except:
|
||||
pass
|
||||
copy_string = "\n".join(copy_values)
|
||||
pyperclip.copy(copy_string)
|
||||
|
||||
|
||||
def copy_from_ssh_table(table):
|
||||
selection = table.selection()
|
||||
copy_values = []
|
||||
for each in selection:
|
||||
try:
|
||||
value = ", ".join(table.item(each)["values"])
|
||||
copy_values.append(str(value))
|
||||
except:
|
||||
pass
|
||||
copy_string = "\n".join(copy_values)
|
||||
pyperclip.copy(copy_string)
|
||||
|
||||
|
||||
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 sort_data(index: int or str):
|
||||
if window["scan"].Disabled:
|
||||
return
|
||||
await update_ui_with_data("status", "Sorting Data")
|
||||
data_list = window["ip_table"].Values
|
||||
table = window["ip_table"].Widget
|
||||
all_data = []
|
||||
for idx, item in enumerate(data_list):
|
||||
all_data.append({"data": item, "tags": table.item(int(idx) + 1)["tags"]})
|
||||
|
||||
# wattage
|
||||
if re.match("[0-9]* W", str(all_data[0]["data"][index])):
|
||||
new_list = sorted(
|
||||
all_data, key=lambda x: int(x["data"][index].replace(" W", ""))
|
||||
)
|
||||
if all_data == new_list:
|
||||
new_list = sorted(
|
||||
all_data,
|
||||
reverse=True,
|
||||
key=lambda x: int(x["data"][index].replace(" W", "")),
|
||||
)
|
||||
|
||||
# hashrate
|
||||
elif re.match("[0-9]*\.?[0-9]* TH\/s", str(all_data[0]["data"][index])):
|
||||
new_list = sorted(
|
||||
all_data, key=lambda x: float(x["data"][index].replace(" TH/s", ""))
|
||||
)
|
||||
if all_data == new_list:
|
||||
new_list = sorted(
|
||||
all_data,
|
||||
reverse=True,
|
||||
key=lambda x: float(x["data"][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]?)",
|
||||
str(all_data[0]["data"][index]),
|
||||
):
|
||||
new_list = sorted(
|
||||
all_data, key=lambda x: ipaddress.ip_address(x["data"][index])
|
||||
)
|
||||
if all_data == new_list:
|
||||
new_list = sorted(
|
||||
all_data,
|
||||
reverse=True,
|
||||
key=lambda x: ipaddress.ip_address(x["data"][index]),
|
||||
)
|
||||
|
||||
# everything else, hostname, temp, and user
|
||||
else:
|
||||
new_list = sorted(all_data, key=lambda x: x["data"][index])
|
||||
if all_data == new_list:
|
||||
new_list = sorted(all_data, reverse=True, key=lambda x: x["data"][index])
|
||||
|
||||
new_data = []
|
||||
for item in new_list:
|
||||
new_data.append(item["data"])
|
||||
|
||||
await update_ui_with_data("ip_table", new_data)
|
||||
for idx, item in enumerate(new_list):
|
||||
table.item(idx + 1, tags=item["tags"])
|
||||
|
||||
await update_ui_with_data("status", "")
|
||||
163
tools/cfg_util_old/cfg_util_sg/layout.py
Normal file
163
tools/cfg_util_old/cfg_util_sg/layout.py
Normal file
File diff suppressed because one or more lines are too long
206
tools/cfg_util_old/cfg_util_sg/ui.py
Normal file
206
tools/cfg_util_old/cfg_util_sg/ui.py
Normal file
@@ -0,0 +1,206 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import PySimpleGUI as sg
|
||||
import tkinter as tk
|
||||
|
||||
from tools.cfg_util_old.cfg_util_sg.layout import (
|
||||
window,
|
||||
generate_config_layout,
|
||||
send_ssh_cmd_layout,
|
||||
)
|
||||
from tools.cfg_util_old.cfg_util_sg.func.miners import (
|
||||
send_config,
|
||||
miner_light,
|
||||
refresh_data,
|
||||
generate_config,
|
||||
import_config,
|
||||
scan_and_get_data,
|
||||
restart_miners_backend,
|
||||
reboot_miners,
|
||||
send_miners_ssh_commands,
|
||||
)
|
||||
from tools.cfg_util_old.cfg_util_sg.func.files import (
|
||||
import_iplist,
|
||||
import_config_file,
|
||||
export_iplist,
|
||||
export_config_file,
|
||||
export_csv,
|
||||
)
|
||||
from tools.cfg_util_old.cfg_util_sg.func.decorators import disable_buttons
|
||||
from tools.cfg_util_old.cfg_util_sg.func.ui import (
|
||||
sort_data,
|
||||
copy_from_table,
|
||||
table_select_all,
|
||||
copy_from_ssh_table,
|
||||
)
|
||||
|
||||
from network import MinerNetwork
|
||||
|
||||
import webbrowser
|
||||
|
||||
|
||||
async def ui():
|
||||
window.read(timeout=0)
|
||||
table = window["ip_table"].Widget
|
||||
table.bind("<Control-Key-c>", lambda x: copy_from_table(table))
|
||||
table.bind("<Control-Key-a>", lambda x: table_select_all())
|
||||
# light tag shows red row for fault lights
|
||||
table.tag_configure("light", foreground="white", background="red")
|
||||
# left justify the hostnames
|
||||
table.column(2, anchor=tk.W)
|
||||
while True:
|
||||
event, value = window.read(timeout=0)
|
||||
if event in (None, "Close", sg.WIN_CLOSED):
|
||||
sys.exit()
|
||||
if isinstance(event, tuple):
|
||||
if len(window["ip_table"].Values) > 0:
|
||||
if event[0] == "ip_table":
|
||||
if event[2][0] == -1:
|
||||
await sort_data(event[2][1])
|
||||
if event == "open_in_web":
|
||||
for row in value["ip_table"]:
|
||||
webbrowser.open("http://" + window["ip_table"].Values[row][0])
|
||||
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_and_get_data(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 == "restart_miner_backend":
|
||||
if len(window["ip_table"].Values) > 0:
|
||||
asyncio.create_task(
|
||||
restart_miners_backend(
|
||||
[
|
||||
window["ip_table"].Values[item][0]
|
||||
for item in value["ip_table"]
|
||||
]
|
||||
)
|
||||
)
|
||||
if event == "reboot_miners":
|
||||
if len(window["ip_table"].Values) > 0:
|
||||
asyncio.create_task(
|
||||
reboot_miners(
|
||||
[
|
||||
window["ip_table"].Values[item][0]
|
||||
for item in value["ip_table"]
|
||||
]
|
||||
)
|
||||
)
|
||||
if event == "send_miner_ssh_command_window":
|
||||
ips = [window["ip_table"].Values[item][0] for item in value["ip_table"]]
|
||||
if len(ips) == 0:
|
||||
ips = [item[0] for item in window["ip_table"].Values]
|
||||
if not len(ips) == 0:
|
||||
await generate_ssh_cmd_ui(ips)
|
||||
if event == "light":
|
||||
if len(window["ip_table"].Values) > 0:
|
||||
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 == "export_csv":
|
||||
asyncio.create_task(
|
||||
export_csv(
|
||||
value["file_iplist"],
|
||||
[window["ip_table"].Values[item] for item in value["ip_table"]],
|
||||
)
|
||||
)
|
||||
if event == "send_config":
|
||||
if len(window["ip_table"].Values) > 0:
|
||||
asyncio.create_task(
|
||||
send_config(
|
||||
[
|
||||
window["ip_table"].Values[item][0]
|
||||
for item in value["ip_table"]
|
||||
],
|
||||
value["config"],
|
||||
last_octet_ip=value["last_octet_user"],
|
||||
)
|
||||
)
|
||||
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 == "refresh_data":
|
||||
if len(window["ip_table"].Values) > 0:
|
||||
asyncio.create_task(
|
||||
refresh_data(
|
||||
[
|
||||
window["ip_table"].Values[item][0]
|
||||
for item in value["ip_table"]
|
||||
]
|
||||
)
|
||||
)
|
||||
if event == "generate_config":
|
||||
await generate_config_ui()
|
||||
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
|
||||
|
||||
|
||||
@disable_buttons
|
||||
async def generate_ssh_cmd_ui(selected_miners: list):
|
||||
ssh_cmd_window = sg.Window(
|
||||
"Send Command", send_ssh_cmd_layout(selected_miners), modal=True
|
||||
)
|
||||
ssh_cmd_window.read(timeout=0)
|
||||
table = ssh_cmd_window["ssh_cmd_table"].Widget
|
||||
table.bind("<Control-Key-c>", lambda x: copy_from_ssh_table(table))
|
||||
# left justify the results
|
||||
table.column(1, anchor=tk.W)
|
||||
while True:
|
||||
event, values = ssh_cmd_window.read(timeout=0)
|
||||
if event in (None, "Close", sg.WIN_CLOSED):
|
||||
break
|
||||
if event == "ssh_command_window_send_cmd":
|
||||
asyncio.create_task(
|
||||
send_miners_ssh_commands(
|
||||
selected_miners, values["ssh_command_window_cmd"], ssh_cmd_window
|
||||
)
|
||||
)
|
||||
if event == "__TIMEOUT__":
|
||||
await asyncio.sleep(0)
|
||||
0
tools/cfg_util_old/func/__init__.py
Normal file
0
tools/cfg_util_old/func/__init__.py
Normal file
66
tools/cfg_util_old/func/parse_data.py
Normal file
66
tools/cfg_util_old/func/parse_data.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from API import APIError
|
||||
|
||||
|
||||
# noinspection PyPep8
|
||||
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
|
||||
# noinspection PyPep8
|
||||
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
|
||||
# noinspection PyPep8
|
||||
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
|
||||
10
tools/web_monitor/__init__.py
Normal file
10
tools/web_monitor/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from tools.web_monitor.app import app
|
||||
import uvicorn
|
||||
|
||||
|
||||
def main():
|
||||
uvicorn.run("app:app", host="0.0.0.0", port=80)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
36
tools/web_monitor/_settings/__init__.py
Normal file
36
tools/web_monitor/_settings/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from fastapi import Request, APIRouter
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from tools.web_monitor.template import templates
|
||||
from tools.web_monitor.func import get_current_miner_list
|
||||
from tools.web_monitor._settings.func import get_current_settings, update_settings
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.route("/", methods=["GET", "POST"])
|
||||
async def settings(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
"settings.html",
|
||||
{
|
||||
"request": request,
|
||||
"cur_miners": get_current_miner_list(),
|
||||
"settings": get_current_settings(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/update")
|
||||
async def update_settings_page(request: Request):
|
||||
data = await request.form()
|
||||
graph_data_sleep_time = data.get("graph_data_sleep_time")
|
||||
miner_data_timeout = data.get("miner_data_timeout")
|
||||
miner_identify_timeout = data.get("miner_identify_timeout")
|
||||
new_settings = {
|
||||
"graph_data_sleep_time": int(graph_data_sleep_time),
|
||||
"miner_data_timeout": int(miner_data_timeout),
|
||||
"miner_identify_timeout": int(miner_identify_timeout),
|
||||
}
|
||||
update_settings(new_settings)
|
||||
return RedirectResponse(request.url_for("settings"))
|
||||
24
tools/web_monitor/_settings/func.py
Normal file
24
tools/web_monitor/_settings/func.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import toml
|
||||
import os
|
||||
|
||||
|
||||
def get_current_settings():
|
||||
try:
|
||||
with open(
|
||||
os.path.join(os.getcwd(), "settings/web_settings.toml"), "r"
|
||||
) as settings_file:
|
||||
settings = toml.loads(settings_file.read())
|
||||
except:
|
||||
settings = {
|
||||
"graph_data_sleep_time": 1,
|
||||
"miner_data_timeout": 5,
|
||||
"miner_identify_timeout": 5,
|
||||
}
|
||||
return settings
|
||||
|
||||
|
||||
def update_settings(settings):
|
||||
with open(
|
||||
os.path.join(os.getcwd(), "settings/web_settings.toml"), "w"
|
||||
) as settings_file:
|
||||
settings_file.write(toml.dumps(settings))
|
||||
3
tools/web_monitor/_settings/web_settings.toml
Normal file
3
tools/web_monitor/_settings/web_settings.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
graph_data_sleep_time = 1
|
||||
miner_data_timeout = 5
|
||||
miner_identify_timeout = 5
|
||||
35
tools/web_monitor/app.py
Normal file
35
tools/web_monitor/app.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from tools.web_monitor.dashboard import router as dashboard_router
|
||||
from tools.web_monitor.miner import router as miner_router
|
||||
from tools.web_monitor.scan import router as scan_router
|
||||
from tools.web_monitor._settings import router as settings_router
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.mount(
|
||||
"/static",
|
||||
StaticFiles(directory=os.path.join(os.path.dirname(__file__), "static")),
|
||||
name="static",
|
||||
)
|
||||
|
||||
app.include_router(dashboard_router, tags=["dashboard"])
|
||||
app.include_router(miner_router, tags=["miner"], prefix="/miner")
|
||||
app.include_router(scan_router, tags=["scan"], prefix="/scan")
|
||||
app.include_router(settings_router, tags=["settings"], prefix="/settings")
|
||||
|
||||
|
||||
@app.get("/remove_all_miners")
|
||||
async def remove_all_miners(request: Request):
|
||||
file = open("miner_list.txt", "w")
|
||||
file.close()
|
||||
return RedirectResponse(request.url_for("settings"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app:app", host="0.0.0.0", port=80)
|
||||
22
tools/web_monitor/dashboard/__init__.py
Normal file
22
tools/web_monitor/dashboard/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi import Request, APIRouter
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from tools.web_monitor.template import templates
|
||||
from tools.web_monitor.func import get_current_miner_list
|
||||
|
||||
from .ws import router as ws_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(ws_router)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def index(request: Request):
|
||||
return RedirectResponse(request.url_for("dashboard"))
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
def dashboard(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
"index.html", {"request": request, "cur_miners": get_current_miner_list()}
|
||||
)
|
||||
43
tools/web_monitor/dashboard/func.py
Normal file
43
tools/web_monitor/dashboard/func.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import asyncio
|
||||
|
||||
from miners.miner_factory import MinerFactory
|
||||
from tools.web_monitor._settings.func import get_current_settings
|
||||
|
||||
|
||||
async def get_miner_data_dashboard(miner_ip):
|
||||
try:
|
||||
settings = get_current_settings()
|
||||
miner_identify_timeout = settings["miner_identify_timeout"]
|
||||
miner_data_timeout = settings["miner_data_timeout"]
|
||||
|
||||
miner_ip = await asyncio.wait_for(
|
||||
MinerFactory().get_miner(miner_ip), miner_identify_timeout
|
||||
)
|
||||
|
||||
miner_summary = await asyncio.wait_for(
|
||||
miner_ip.api.summary(), miner_data_timeout
|
||||
)
|
||||
if miner_summary:
|
||||
if "MHS av" in miner_summary["SUMMARY"][0].keys():
|
||||
hashrate = format(
|
||||
round(miner_summary["SUMMARY"][0]["MHS av"] / 1000000, 2), ".2f"
|
||||
)
|
||||
elif "GHS av" in miner_summary["SUMMARY"][0].keys():
|
||||
hashrate = format(
|
||||
round(miner_summary["SUMMARY"][0]["GHS av"] / 1000, 2), ".2f"
|
||||
)
|
||||
else:
|
||||
hashrate = 0
|
||||
else:
|
||||
hashrate = 0
|
||||
|
||||
return {"ip": str(miner_ip.ip), "hashrate": hashrate}
|
||||
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
return {"ip": miner_ip, "error": "The miner is not responding."}
|
||||
|
||||
except KeyError:
|
||||
return {
|
||||
"ip": miner_ip,
|
||||
"error": "The miner returned unusable/unsupported data.",
|
||||
}
|
||||
41
tools/web_monitor/dashboard/ws.py
Normal file
41
tools/web_monitor/dashboard/ws.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
import websockets.exceptions
|
||||
from fastapi import WebSocket, WebSocketDisconnect, APIRouter
|
||||
|
||||
from tools.web_monitor.func import get_current_miner_list
|
||||
from tools.web_monitor._settings.func import get_current_settings
|
||||
from tools.web_monitor.dashboard.func import get_miner_data_dashboard
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.websocket("/dashboard/ws")
|
||||
async def dashboard_websocket(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
graph_sleep_time = get_current_settings()["graph_data_sleep_time"]
|
||||
try:
|
||||
while True:
|
||||
miners = get_current_miner_list()
|
||||
all_miner_data = []
|
||||
data_gen = asyncio.as_completed(
|
||||
[get_miner_data_dashboard(miner_ip) for miner_ip in miners]
|
||||
)
|
||||
for all_data in data_gen:
|
||||
data_point = await all_data
|
||||
all_miner_data.append(data_point)
|
||||
all_miner_data.sort(key=lambda x: x["ip"])
|
||||
await websocket.send_json(
|
||||
{
|
||||
"datetime": datetime.datetime.now().isoformat(),
|
||||
"miners": all_miner_data,
|
||||
}
|
||||
)
|
||||
await asyncio.sleep(graph_sleep_time)
|
||||
except WebSocketDisconnect:
|
||||
print("Websocket disconnected.")
|
||||
pass
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
pass
|
||||
12
tools/web_monitor/func/__init__.py
Normal file
12
tools/web_monitor/func/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import os
|
||||
import ipaddress
|
||||
|
||||
|
||||
def get_current_miner_list():
|
||||
cur_miners = []
|
||||
if os.path.exists(os.path.join(os.getcwd(), "miner_list.txt")):
|
||||
with open(os.path.join(os.getcwd(), "miner_list.txt")) as file:
|
||||
for line in file.readlines():
|
||||
cur_miners.append(line.strip())
|
||||
cur_miners = sorted(cur_miners, key=lambda x: ipaddress.ip_address(x))
|
||||
return cur_miners
|
||||
22
tools/web_monitor/miner/__init__.py
Normal file
22
tools/web_monitor/miner/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi import Request, APIRouter
|
||||
|
||||
from tools.web_monitor.template import templates
|
||||
from tools.web_monitor.func import get_current_miner_list
|
||||
|
||||
from .ws import router as ws_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(ws_router)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def miner(_request: Request, _miner_ip):
|
||||
return get_miner
|
||||
|
||||
|
||||
@router.get("/{miner_ip}")
|
||||
def get_miner(request: Request, miner_ip):
|
||||
return templates.TemplateResponse(
|
||||
"miner.html",
|
||||
{"request": request, "cur_miners": get_current_miner_list(), "miner": miner_ip},
|
||||
)
|
||||
16
tools/web_monitor/miner/remove.py
Normal file
16
tools/web_monitor/miner/remove.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from tools.web_monitor.app import app
|
||||
from tools.web_monitor.func import get_current_miner_list
|
||||
|
||||
|
||||
@app.get("/{miner_ip}/remove")
|
||||
def get_miner(request: Request, miner_ip):
|
||||
miners = get_current_miner_list()
|
||||
miners.remove(miner_ip)
|
||||
with open("miner_list.txt", "w") as file:
|
||||
for miner_ip in miners:
|
||||
file.write(miner_ip + "\n")
|
||||
|
||||
return RedirectResponse(request.url_for("dashboard"))
|
||||
167
tools/web_monitor/miner/ws.py
Normal file
167
tools/web_monitor/miner/ws.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
import websockets.exceptions
|
||||
from fastapi import WebSocket, WebSocketDisconnect, APIRouter
|
||||
|
||||
from miners.miner_factory import MinerFactory
|
||||
from tools.web_monitor._settings.func import get_current_settings
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.websocket("/{miner_ip}/ws")
|
||||
async def miner_websocket(websocket: WebSocket, miner_ip):
|
||||
await websocket.accept()
|
||||
settings = get_current_settings()
|
||||
miner_identify_timeout = settings["miner_identify_timeout"]
|
||||
miner_data_timeout = settings["miner_data_timeout"]
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
cur_miner = await asyncio.wait_for(
|
||||
MinerFactory().get_miner(str(miner_ip)), miner_identify_timeout
|
||||
)
|
||||
|
||||
data = await asyncio.wait_for(
|
||||
cur_miner.api.multicommand(
|
||||
"summary", "fans", "stats", "devs", "temps"
|
||||
),
|
||||
miner_data_timeout,
|
||||
)
|
||||
miner_model = await cur_miner.get_model()
|
||||
|
||||
miner_summary = None
|
||||
miner_fans = None
|
||||
if "summary" in data.keys():
|
||||
miner_summary = data["summary"][0]
|
||||
elif "SUMMARY" in data.keys():
|
||||
miner_summary = data
|
||||
miner_fans = {"FANS": []}
|
||||
for item in ["Fan Speed In", "Fan Speed Out"]:
|
||||
if item in miner_summary["SUMMARY"][0].keys():
|
||||
miner_fans["FANS"].append(
|
||||
{"RPM": miner_summary["SUMMARY"][0][item]}
|
||||
)
|
||||
|
||||
if "fans" in data.keys():
|
||||
miner_fans = data["fans"][0]
|
||||
|
||||
miner_temp_list = []
|
||||
if "temps" in data.keys():
|
||||
miner_temps = data["temps"][0]
|
||||
for board in miner_temps["TEMPS"]:
|
||||
if board["Chip"] is not None and not board["Chip"] == 0.0:
|
||||
miner_temp_list.append(board["Chip"])
|
||||
|
||||
if "devs" in data.keys() and not len(miner_temp_list) > 0:
|
||||
if not data["devs"][0].get("DEVS") == []:
|
||||
if "Chip Temp Avg" in data["devs"][0]["DEVS"][0].keys():
|
||||
for board in data["devs"][0]["DEVS"]:
|
||||
if (
|
||||
board["Chip Temp Avg"] is not None
|
||||
and not board["Chip Temp Avg"] == 0.0
|
||||
):
|
||||
miner_temp_list.append(board["Chip Temp Avg"])
|
||||
|
||||
if "stats" in data.keys() and not len(miner_temp_list) > 0:
|
||||
if not data["stats"][0]["STATS"] == []:
|
||||
for temp in ["temp2", "temp1", "temp3"]:
|
||||
if temp in data["stats"][0]["STATS"][1].keys():
|
||||
if (
|
||||
data["stats"][0]["STATS"][1][temp] is not None
|
||||
and not data["stats"][0]["STATS"][1][temp] == 0.0
|
||||
):
|
||||
miner_temp_list.append(
|
||||
data["stats"][0]["STATS"][1][temp]
|
||||
)
|
||||
data["stats"][0]["STATS"][0].keys()
|
||||
if any(
|
||||
"MM ID" in string
|
||||
for string in data["stats"][0]["STATS"][0].keys()
|
||||
):
|
||||
temp_all = []
|
||||
for key in [
|
||||
string
|
||||
for string in data["stats"][0]["STATS"][0].keys()
|
||||
if "MM ID" in string
|
||||
]:
|
||||
for value in [
|
||||
string
|
||||
for string in data["stats"][0]["STATS"][0][key].split(
|
||||
" "
|
||||
)
|
||||
if "TMax" in string
|
||||
]:
|
||||
temp_all.append(
|
||||
int(value.split("[")[1].replace("]", ""))
|
||||
)
|
||||
miner_temp_list.append(round(sum(temp_all) / len(temp_all)))
|
||||
|
||||
if "stats" in data.keys() and not miner_fans:
|
||||
miner_stats = data["stats"][0]
|
||||
miner_fans = {"FANS": []}
|
||||
for item in ["fan1", "fan2", "fan3", "fan4"]:
|
||||
if item in miner_stats["STATS"][1].keys():
|
||||
miner_fans["FANS"].append(
|
||||
{"RPM": miner_stats["STATS"][1][item]}
|
||||
)
|
||||
|
||||
if miner_summary:
|
||||
if "MHS av" in miner_summary["SUMMARY"][0].keys():
|
||||
hashrate = float(
|
||||
format(
|
||||
round(
|
||||
miner_summary["SUMMARY"][0]["MHS av"] / 1000000, 2
|
||||
),
|
||||
".2f",
|
||||
)
|
||||
)
|
||||
elif "GHS av" in miner_summary["SUMMARY"][0].keys():
|
||||
hashrate = float(
|
||||
format(
|
||||
round(miner_summary["SUMMARY"][0]["GHS av"] / 1000, 2),
|
||||
".2f",
|
||||
)
|
||||
)
|
||||
else:
|
||||
hashrate = 0
|
||||
else:
|
||||
hashrate = 0
|
||||
|
||||
fan_speeds = []
|
||||
if miner_fans:
|
||||
for fan in miner_fans["FANS"]:
|
||||
fan_speeds.append(fan["RPM"])
|
||||
|
||||
while len(fan_speeds) < 4:
|
||||
fan_speeds.append(0)
|
||||
|
||||
if len(miner_temp_list) == 0:
|
||||
miner_temp_list = [0]
|
||||
|
||||
data = {
|
||||
"hashrate": hashrate,
|
||||
"fans": fan_speeds,
|
||||
"temp": round(sum(miner_temp_list) / len(miner_temp_list), 2),
|
||||
"datetime": datetime.datetime.now().isoformat(),
|
||||
"model": miner_model,
|
||||
}
|
||||
print(data)
|
||||
await websocket.send_json(data)
|
||||
await asyncio.sleep(settings["graph_data_sleep_time"])
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
data = {"error": "The miner is not responding."}
|
||||
await websocket.send_json(data)
|
||||
await asyncio.sleep(0.5)
|
||||
except KeyError as e:
|
||||
print(e)
|
||||
data = {"error": "The miner returned unusable/unsupported data."}
|
||||
await websocket.send_json(data)
|
||||
await asyncio.sleep(0.5)
|
||||
except WebSocketDisconnect:
|
||||
print("Websocket disconnected.")
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
pass
|
||||
BIN
tools/web_monitor/requirements.txt
Normal file
BIN
tools/web_monitor/requirements.txt
Normal file
Binary file not shown.
25
tools/web_monitor/scan/__init__.py
Normal file
25
tools/web_monitor/scan/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from fastapi import Request, APIRouter
|
||||
|
||||
from tools.web_monitor.template import templates
|
||||
from tools.web_monitor.func import get_current_miner_list
|
||||
|
||||
from .ws import router as ws_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(ws_router)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def scan(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
"scan.html", {"request": request, "cur_miners": get_current_miner_list()}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/add_miners")
|
||||
async def add_miners_scan(request: Request):
|
||||
miners = await request.json()
|
||||
with open("miner_list.txt", "a+") as file:
|
||||
for miner_ip in miners["miners"]:
|
||||
file.write(miner_ip + "\n")
|
||||
return scan
|
||||
39
tools/web_monitor/scan/func.py
Normal file
39
tools/web_monitor/scan/func.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import asyncio
|
||||
|
||||
from fastapi import WebSocket
|
||||
|
||||
from network import MinerNetwork
|
||||
from tools.web_monitor.func import get_current_miner_list
|
||||
from miners.miner_factory import MinerFactory
|
||||
|
||||
|
||||
async def do_websocket_scan(websocket: WebSocket, network_ip: str):
|
||||
cur_miners = get_current_miner_list()
|
||||
try:
|
||||
if "/" in network_ip:
|
||||
network_ip, network_subnet = network_ip.split("/")
|
||||
network = MinerNetwork(network_ip, mask=network_subnet)
|
||||
else:
|
||||
network = MinerNetwork(network_ip)
|
||||
miner_generator = network.scan_network_generator()
|
||||
miners = []
|
||||
async for miner_ip in miner_generator:
|
||||
if miner_ip and str(miner_ip) not in cur_miners:
|
||||
miners.append(miner_ip)
|
||||
|
||||
get_miner_generator = MinerFactory().get_miner_generator(miners)
|
||||
all_miners = []
|
||||
async for found_miner in get_miner_generator:
|
||||
all_miners.append(
|
||||
{"ip": found_miner.ip, "model": await found_miner.get_model()}
|
||||
)
|
||||
all_miners.sort(key=lambda x: x["ip"])
|
||||
send_miners = []
|
||||
for miner_ip in all_miners:
|
||||
send_miners.append(
|
||||
{"ip": str(miner_ip["ip"]), "model": miner_ip["model"]}
|
||||
)
|
||||
await websocket.send_json(send_miners)
|
||||
await websocket.send_text("Done")
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
35
tools/web_monitor/scan/ws.py
Normal file
35
tools/web_monitor/scan/ws.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import asyncio
|
||||
|
||||
import websockets.exceptions
|
||||
from fastapi import WebSocket, WebSocketDisconnect, APIRouter
|
||||
|
||||
from tools.web_monitor.scan.func import do_websocket_scan
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def websocket_scan(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
cur_task = None
|
||||
try:
|
||||
while True:
|
||||
ws_data = await websocket.receive_text()
|
||||
if "-Cancel-" in ws_data:
|
||||
if cur_task:
|
||||
cur_task.cancel()
|
||||
print("Cancelling scan...")
|
||||
try:
|
||||
await cur_task
|
||||
except asyncio.CancelledError:
|
||||
cur_task = None
|
||||
await websocket.send_text("Cancelled")
|
||||
else:
|
||||
cur_task = asyncio.create_task(do_websocket_scan(websocket, ws_data))
|
||||
if cur_task and cur_task.done():
|
||||
cur_task = None
|
||||
except WebSocketDisconnect:
|
||||
print("Websocket disconnected.")
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
pass
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user