Compare commits

...

50 Commits

Author SHA1 Message Date
UpstreamData
36ff5e96a4 Merge remote-tracking branch 'origin/master' 2022-02-14 10:02:16 -07:00
UpstreamData
9bf9f8342a added export csv button to export all data from the tool 2022-02-14 10:01:43 -07:00
UpstreamData
f3660c1f68 Update README.md 2022-02-11 11:11:42 -07:00
UpstreamData
d58aa871b5 updated CGMiner and BMMiner docstrings 2022-02-11 11:06:35 -07:00
UpstreamData
4f90eb65ad updated btminer docstrings 2022-02-11 10:50:04 -07:00
UpstreamData
b50da98322 fixed some issues in CFG-Util-README.md, and started reformatting docstrings and cleaning API code. 2022-01-31 15:35:08 -07:00
UpstreamData
ca47f2817f added bad board util files, and fixed imports in README.md 2022-01-31 09:13:49 -07:00
UpstreamData
c489a4bed9 refactored location of config utility, and changed sizing of some items 2022-01-31 09:10:23 -07:00
UpstreamData
54c7e996db fixed a bug with the config export not converting 2022-01-27 15:02:55 -07:00
UpstreamData
0426bb289e fixed a bug with tags not getting assigned to second and third boards with multiple chains 2022-01-27 14:04:08 -07:00
UpstreamData
8e253ffa05 fixed a bug with tags not getting assigned to second and third boards 2022-01-27 12:38:59 -07:00
UpstreamData
102f365003 added tags to board util bad miners, so when sorting they stay the same 2022-01-27 12:30:14 -07:00
UpstreamData
48d2f6ec07 fixed board util progress bar sizing 2022-01-26 15:37:14 -07:00
UpstreamData
58f0ce8e2d reformatted the board util slightly 2022-01-26 15:35:44 -07:00
UpstreamData
3178083533 added whatsminer get bad boards 2022-01-26 14:53:51 -07:00
UpstreamData
516075db6d reformatted many base commands, and moved them to the BaseMiner class 2022-01-26 08:57:14 -07:00
UpstreamData
d6c8335162 changed board_util copy format 2022-01-25 16:11:15 -07:00
UpstreamData
e7a45efe15 changed board util copy/paste to copy the whole line instead of just the IP 2022-01-25 16:00:22 -07:00
UpstreamData
1c0b5e6441 tracks boards by left/center/right now in lieu of board numbers, and works with Hive T9s and BOS S9s and X17s 2022-01-25 15:53:36 -07:00
UpstreamData
66792e1ab9 added chip count and fixed refreshing data 2022-01-25 15:20:21 -07:00
UpstreamData
6fd631df5b added red highlight to miners with bad boards 2022-01-25 15:14:33 -07:00
UpstreamData
dcf1a805c5 fixed a bug with the board utility 2022-01-24 16:39:11 -07:00
UpstreamData
8edfde96dc cleaned the board utility a bit 2022-01-24 16:31:51 -07:00
UpstreamData
ae911ec775 started adding the board utility 2022-01-24 16:29:21 -07:00
UpstreamData
465d0e6f1c fixed formatting on getting bad boards 2022-01-24 11:15:14 -07:00
UpstreamData
6d9de87fb8 changed bos get_bad_boards to be consistent with hive 2022-01-21 16:30:26 -07:00
UpstreamData
a93027369e added Hive get bad boards, and started on a bad board utility 2022-01-21 16:15:46 -07:00
UpstreamData
a1839aae46 added T9s 2022-01-21 14:42:01 -07:00
UpstreamData
d5fc7650ef fixed some small bugs with miner factory 2022-01-12 11:50:43 -07:00
UpstreamData
cdc6c898ae reformatted, added a bunch of comments to improve readability, and added a whatsminer admin password in settings 2022-01-12 09:04:15 -07:00
UpstreamData
574432ec0d Merge remote-tracking branch 'origin/master' 2022-01-12 08:23:58 -07:00
UpstreamData
a89486d6ad refactored cgf_util_sg to its own folder 2022-01-12 08:23:47 -07:00
Dewey Cox
56d7234ccb moved pysg files into a directory inside cfg_util, and refactored imports. 2022-01-12 08:17:43 -07:00
UpstreamData
c7f1b00e13 Update CFG-Util-README.md 2022-01-12 08:12:46 -07:00
Dewey Cox
60f5137115 fixed progress bar width on different screens 2022-01-11 15:23:54 -07:00
UpstreamData
a90239e3c5 added updates after changing progress bar length to not get ridiculously over length progress bars 2022-01-11 14:02:59 -07:00
UpstreamData
a105429d99 changed refresh_data to only refresh data of selected miners if miners are selected 2022-01-11 13:49:54 -07:00
Dewey Cox
8e73b8f7a1 fixed a bug with getting model when it gets empty bytes 2022-01-11 13:33:29 -07:00
Dewey Cox
53af55a87d fixed a bug with model not reading if there are no hashboards in the miner 2022-01-11 12:58:20 -07:00
UpstreamData
0711bcb259 added misc folder with bos get_bad_tuners, to get tuner errors from all miners on a network 2022-01-11 10:18:31 -07:00
UpstreamData
282e00f93a change get data to refresh data and made scan do both scanning and getting data, and refresh data only refreshes whats currently in the IP list or selected 2022-01-11 09:04:00 -07:00
UpstreamData
2e11527416 improved README.md 2022-01-10 15:04:59 -07:00
UpstreamData
01a64e63c6 fixed more bugs with avalonminers 2022-01-10 13:48:25 -07:00
UpstreamData
2610d642fa fixed a bug with getting data not filling in total hashrate because of a key error on data points with no data 2022-01-10 09:00:45 -07:00
UpstreamData
e1e93aea66 added screeninfo and a resized progress bar because for some reason it is too big on some screens 2022-01-10 08:38:17 -07:00
UpstreamData
ab208d0d2f added rebooting and restarting backend to the GUI 2022-01-08 19:55:26 -07:00
UpstreamData
e9210eb37d added rebooting, still need to add the buttons 2022-01-08 19:49:12 -07:00
UpstreamData
93665772c3 added the avalonminer estats pattern for parsing their garbage output 2022-01-08 15:51:30 -07:00
UpstreamData
44bcc30130 fixed more bugs with avalonminers, and added temps 2022-01-08 15:25:43 -07:00
UpstreamData
d8bccbccaa fixed some bugs with canaan miners not responding properly and returning empty bytes 2022-01-08 14:33:05 -07:00
52 changed files with 2439 additions and 823 deletions

View File

@@ -62,7 +62,7 @@ class BaseMinerAPI:
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.""",
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
@@ -84,7 +84,7 @@ If you are sure you want to use this command please use API.send_command("{item}
if 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
@@ -123,10 +123,12 @@ 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]:
raise APIError(validation[1])
return data
@@ -172,10 +174,12 @@ If you are sure you want to use this command please use API.send_command("{item}
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: {str_data}")
return parsed_data

View File

@@ -2,34 +2,36 @@ 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 +47,141 @@ 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.
async def addpool(self,
url: str,
username: str,
password: str
) -> dict:
"""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 +189,130 @@ 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)
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 +321,36 @@ 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 +359,96 @@ 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 +482,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 +497,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")

View File

@@ -1,32 +1,46 @@
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
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\./]*)\$')
# 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,18 +48,37 @@ 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']
# 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
@@ -59,10 +92,23 @@ def parse_btminer_priviledge_data(token_data, data):
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']
# 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,7 +117,13 @@ 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}
# dump the labeled data to json
@@ -80,20 +132,65 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
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 | 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")
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
reader, writer = await asyncio.open_connection(
str(self.ip),
self.port
)
# handle OSError 121
except OSError as e:
if e.winerror == "121":
@@ -123,47 +220,91 @@ class BTMinerAPI(BaseMinerAPI):
writer.close()
await writer.wait_closed()
# check if th returned data is encoded
# 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)
data = parse_btminer_priviledge_data(
self.current_token,
data
)
except Exception as e:
print(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")
# 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"] + '$')
# 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):
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 +314,36 @@ 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 +351,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 +364,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 +381,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 +394,53 @@ 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 +453,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 +463,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 +473,55 @@ 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:
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 +529,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 +542,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 +555,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 +569,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,47 +585,57 @@ 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":
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"]'
'["wait for adjust temp", '
'"adjust complete", '
'"adjust continue"]'
)
if complete:
complete = "true"
else:
complete = "false"
command = {"cmd": "pre_power_on", "complete": complete, "msg": msg}
command = {"cmd": "pre_power_on",
"complete": complete,
"msg": msg
}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
@@ -462,82 +643,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")

View File

@@ -2,34 +2,36 @@ from API import BaseMinerAPI
class CGMinerAPI(BaseMinerAPI):
"""
A class that abstracts the CGMiner API in the miners.
"""An abstraction of the BMMiner API.
Each method corresponds to an API command in CGMiner.
Each method corresponds to an API command in BMMiner.
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 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, 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 +43,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 +78,106 @@ 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.
async def addpool(self,
url: str,
username: str,
password: str
) -> dict:
"""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 +185,130 @@ class CGMinerAPI(BaseMinerAPI):
return await self.send_command("save")
async def quit(self) -> dict:
"""
API 'quit' command.
"""Quit BMMiner.
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 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)
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 +317,36 @@ 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,6 +355,13 @@ 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}")
@@ -392,90 +369,76 @@ class CGMinerAPI(BaseMinerAPI):
return await self.send_command("pgaset", parameters=f"{n}, {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)
@@ -509,6 +472,14 @@ 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}")
@@ -516,17 +487,15 @@ class CGMinerAPI(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")

170
README.md
View File

@@ -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.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.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,6 +101,7 @@ 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")))
```
@@ -105,12 +111,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.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,6 +125,7 @@ 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")))
```
@@ -128,7 +136,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.func.parse_data import safe_parse_api_data
async def get_miner_hashrate(ip: str):
# Create a BOSminerAPI object
@@ -141,6 +150,151 @@ 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")))
```
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.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.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.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.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
View File

@@ -0,0 +1,4 @@
from tools.bad_board_util import main
if __name__ == '__main__':
main()

View File

@@ -1,4 +1,4 @@
from cfg_util import main
from tools.cfg_util import main
if __name__ == '__main__':
main()

29
make_board_tool_exe.py Normal file
View File

@@ -0,0 +1,29 @@
"""
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}\\"
},
},
executables=[Executable("board_util.py", base=base, icon="icon.ico", target_name="UpstreamBoardUtil.exe")]
)

View File

@@ -12,3 +12,28 @@ class BaseMiner:
self.api = api
self.api_type = None
self.model = None
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 None
async def restart_backend(self):
return None
async def send_config(self, yaml_config):
return None

View File

@@ -1,7 +1,7 @@
from miners.bosminer import BOSminer
from miners.bosminer import BOSMiner
class BOSMinerS9(BOSminer):
class BOSMinerS9(BOSMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.model = "S9"

View 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)}"

View 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)}"

View File

@@ -0,0 +1,46 @@
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

View File

@@ -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)}"

View File

@@ -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)}"

View File

@@ -0,0 +1,175 @@
from miners.cgminer import CGMiner
import re
class CGMinerAvalon(CGMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.model = "Avalon"
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"CGMinerAvalon: {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)

View 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)}"

View File

@@ -1,12 +1,16 @@
from API.bmminer import BMMinerAPI
from miners import BaseMiner
import asyncssh
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)}"
@@ -21,16 +25,50 @@ class BMMiner(BaseMiner):
return None
async def get_hostname(self) -> str:
return "?"
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()
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 restart_backend(self) -> None:
return None # Murray
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)
except Exception as e:
print(f"{cmd} error: {e}")
if i == 3:
return
continue
return result
async def reboot(self) -> None:
return None # Murray
async def get_config(self) -> None:
return None # Murray
await self.send_ssh_command("reboot")

View File

@@ -5,7 +5,7 @@ import toml
from config.bos import bos_config_convert, general_config_convert_bos
class BOSminer(BaseMiner):
class BOSMiner(BaseMiner):
def __init__(self, ip: str) -> None:
api = BOSMinerAPI(ip)
super().__init__(ip, api)
@@ -13,6 +13,7 @@ class BOSminer(BaseMiner):
self.config = None
self.uname = 'root'
self.pwd = 'admin'
self.nominal_chips = 63
def __repr__(self) -> str:
return f"BOSminer: {str(self.ip)}"
@@ -61,7 +62,10 @@ class BOSminer(BaseMiner):
"""Sends command to turn off fault light on the miner."""
await self.send_ssh_command('miner fault_light off')
async def restart_backend(self) -> None:
async def restart_backend(self):
await self.restart_bosminer()
async def restart_bosminer(self) -> None:
"""Restart bosminer hashing process."""
await self.send_ssh_command('/etc/init.d/bosminer restart')
@@ -92,8 +96,9 @@ class BOSminer(BaseMiner):
return self.model + " (BOS)"
version_data = await self.api.devdetails()
if version_data:
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
return self.model + " (BOS)"
if not version_data["DEVDETAILS"] == []:
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
return self.model + " (BOS)"
return None
async def send_config(self, yaml_config) -> None:
@@ -105,16 +110,41 @@ class BOSminer(BaseMiner):
await file.write(toml_conf)
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."""
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
})
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."""

View File

@@ -8,6 +8,7 @@ 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)}"
@@ -29,14 +30,30 @@ class BTMiner(BaseMiner):
except APIError:
return "?"
async def send_config(self, _):
return None # ignore for now
async def restart_backend(self) -> None:
return None
async def get_board_info(self) -> dict:
"""Gets data on each board and chain in the miner."""
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:
print(board)
return boards
async def reboot(self) -> None:
return None
async def get_config(self) -> None:
return None

View File

@@ -1,5 +1,6 @@
from miners import BaseMiner
from API.cgminer import CGMinerAPI
from API import APIError
import asyncssh
@@ -18,7 +19,10 @@ 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
@@ -35,9 +39,6 @@ class CGMiner(BaseMiner):
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),
@@ -97,6 +98,9 @@ class CGMiner(BaseMiner):
# else:
# print(cmd)
async def restart_backend(self) -> None:
await self.restart_cgminer()
async def restart_cgminer(self) -> None:
commands = ['cgminer-api restart',
'/usr/bin/cgminer-monitor >/dev/null 2>&1']
@@ -104,9 +108,7 @@ class CGMiner(BaseMiner):
await self.send_ssh_command(commands)
async def reboot(self) -> None:
commands = ['reboot']
commands = ';'.join(commands)
await self.send_ssh_command(commands)
await self.send_ssh_command("reboot")
async def start_cgminer(self) -> None:
commands = ['mkdir -p /etc/tmp/',
@@ -131,3 +133,4 @@ class CGMiner(BaseMiner):
self._result_handler(result)
self.config = result.stdout
print(str(self.config))

View File

@@ -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
@@ -16,10 +19,16 @@ 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 import CGMinerAvalon
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
@@ -40,11 +49,16 @@ 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
@@ -53,38 +67,81 @@ class MinerFactory:
# 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
model = None
# try to get the API multiple times based on retries
for i in range(GET_VERSION_RETRIES):
# get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None
api = await self._get_api_type(ip)
# if we find the API type, dont need to loop anymore
if api:
break
model = None
# try to get the model multiple times based on retries
for i in range(GET_VERSION_RETRIES):
# get the model, should return some miner model type, e.g. Antminer S9
model = await self._get_miner_model(ip)
# if we find the model type, dont need to loop anymore
if model:
break
# 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:
print(ip)
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 "CGMiner" in api:
miner = CGMinerX19(str(ip))
elif "BMMiner" in api:
miner = BMMinerX19(str(ip))
# Avalonminer V8
elif "avalon" in model:
miner = CGMinerAvalon(str(ip))
# Whatsminers
elif "M20" in model:
miner = BTMinerM20(str(ip))
elif "M21" in model:
@@ -95,41 +152,90 @@ 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_model(self, ip: ipaddress.ip_address or str) -> str or None:
# instantiate model as being nothing if getting it fails
model = None
# try block in case of APIError or OSError 121 (Semaphore timeout)
try:
# send the devdetails command to the miner (will fail with no boards/devices)
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:
# sometimes data is b'', check for that
if data:
# status check, make sure the command succeeded
if data.get("STATUS"):
if not isinstance(data["STATUS"], str):
# if status is E, its an error
if data["STATUS"][0].get("STATUS") not in ["I", "S"]:
# try an alternate method if devdetails fails
data = await self._send_api_command(str(ip), "version")
model = data["VERSION"][0]["Type"]
except:
print(f"Get Model Exception: {ip}")
# make sure we have data
if data:
# check the keys are there to get the version
if data.get("VERSION"):
if data["VERSION"][0].get("Type"):
# save the model to be returned later
model = data["VERSION"][0]["Type"]
else:
# make sure devdetails actually contains data, if its empty, there are no devices
if "DEVDETAILS" in data.keys() and not data["DEVDETAILS"] == []:
# check for model, for most miners
if not data["DEVDETAILS"][0]["Model"] == "":
# model of most miners
model = data["DEVDETAILS"][0]["Model"]
# if model fails, try driver
else:
# some avalonminers have model in driver
model = data["DEVDETAILS"][0]["Driver"]
else:
model = data["DEVDETAILS"][0]["Model"]
else:
try:
# if all that fails, try just version
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
if "VERSION" in data.keys():
model = data["VERSION"][0]["Type"]
else:
print(data)
return model
# if there are errors, we just return None
except APIError:
return model
except OSError as e:
if e.winerror == 121:
return None
print(e)
return model
else:
print(ip, e)
return None
return model
async def _send_api_command(self, ip: ipaddress.ip_address or str, command: str):
try:
@@ -176,12 +282,11 @@ 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()
@@ -189,24 +294,43 @@ class MinerFactory:
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."""
# instantiate API as None in case something fails
api = None
# try block to handle OSError 121 (Semaphore timeout)
try:
# try the version command,works on most miners
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 we got data back, try to parse it
if data:
# make sure the command succeeded
if data.get("STATUS") and not data.get("STATUS") == "E":
if data["STATUS"][0].get("STATUS") in ["I", "S"]:
# check if there are any BMMiner strings in any of the dict keys
if any("BMMiner" in string for string in data["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 data["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 data["VERSION"][0].keys()):
api = "BOSMiner"
# if all that fails, check the Description to see if it is a whatsminer
elif data.get("Description") and "whatsminer" in data.get("Description"):
api = "BTMiner"
# return the API if we found it
if api:
return api
# if there are errors, return None
except OSError as e:
if e.winerror == 121:
return None

View File

@@ -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"

View File

@@ -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)}"

View File

@@ -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
View 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}

View File

@@ -18,40 +18,75 @@ class MinerNetwork:
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 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 self.mask:
subnet_mask = str(self.mask)
else:
# if there is no subnet mask passed, default to /24
if not self.mask:
subnet_mask = "24"
return ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False)
# 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...")
# 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 = []
# clear cached miners
self.miner_factory.clear_cached_miners()
# try to get each miner found
for miner_ip in miner_ips:
# append to the list of tasks
create_miners_tasks.append(self.miner_factory.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,16 +95,32 @@ 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

View File

@@ -8,14 +8,20 @@ try:
NETWORK_PING_TIMEOUT: int = settings["ping_timeout"]
NETWORK_SCAN_THREADS: int = settings["scan_threads"]
CFG_UTIL_REBOOT_THREADS: int = settings["reboot_threads"]
CFG_UTIL_CONFIG_THREADS: int = settings["config_threads"]
MINER_FACTORY_GET_VERSION_RETRIES: int = settings["get_version_retries"]
WHATSMINER_PWD: str = settings["whatsminer_pwd"]
except:
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"

View File

@@ -2,4 +2,13 @@ get_version_retries = 3
ping_retries = 3
ping_timeout = 5
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"

View File

@@ -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,27 +15,26 @@
## 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.
### Table:
@@ -34,7 +42,7 @@
* 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 +65,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
View File

View File

@@ -0,0 +1,18 @@
from tools.bad_board_util.miner_factory import miner_factory
from tools.bad_board_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())
if __name__ == '__main__':
main()

View File

View File

@@ -0,0 +1,43 @@
import ipaddress
import os
import re
import aiofiles
from tools.bad_board_util.func.ui import update_ui_with_data
from tools.bad_board_util.layout import window
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", "")

View File

@@ -0,0 +1,197 @@
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 tools.bad_board_util.miner_factory import miner_factory
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 = 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 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", "")
else:
row_colors.append((ip_table_index, "white", "red"))
if 1 in data_point["data"].keys():
board_center = " ".join([chain["chip_status"] for chain in data_point["data"][1]]).replace("o", "")
else:
row_colors.append((ip_table_index, "white", "red"))
if 2 in data_point["data"].keys():
board_right = " ".join([chain["chip_status"] for chain in data_point["data"][2]]).replace("o", "")
else:
row_colors.append((ip_table_index, "white", "red"))
if False in [chain["nominal"] for chain in [data_point["data"][key] for key in data_point["data"].keys()][0]]:
row_colors.append((ip_table_index, "white", "red"))
else:
row_colors.append((ip_table_index, "white", "red"))
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, row_colors=row_colors)
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
await update_ui_with_data("status", "")
async def scan_and_get_data(network):
await update_ui_with_data("status", "Scanning")
await update_ui_with_data("ip_count", "")
await update_ui_with_data("ip_table", [])
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")
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", "")
else:
row_colors.append((ip_table_index, "bad"))
if 1 in data_point["data"].keys():
board_center = " ".join([chain["chip_status"] for chain in data_point["data"][1]]).replace("o", "")
else:
row_colors.append((ip_table_index, "bad"))
if 2 in data_point["data"].keys():
board_right = " ".join([chain["chip_status"] for chain in data_point["data"][2]]).replace("o", "")
else:
row_colors.append((ip_table_index, "bad"))
if False in [chain["nominal"] for board in [data_point["data"][key] for key in data_point["data"].keys()] for chain in board]:
row_colors.append((ip_table_index, "bad"))
else:
row_colors.append((ip_table_index, "bad"))
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))
data = [
data_point["IP"],
data_point["model"],
len(board_left),
board_left_chips,
len(board_center),
board_center_chips,
len(board_right),
board_right_chips
]
ip_table_data[ip_table_index] = data
window["ip_table"].update(ip_table_data)
table = window["ip_table"].Widget
table.tag_configure("bad", foreground="white", background="red")
for row in row_colors:
table.item(row[0] + 1, tags=row[1])
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
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 miner_factory.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

View File

@@ -0,0 +1,79 @@
import ipaddress
import re
from tools.bad_board_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]
table_values = table.item(each)["values"]
ip = table_values[0]
model = table_values[1]
l_brd_chips = str(table_values[2])
c_brd_chips = str(table_values[4])
r_brd_chips = str(table_values[6])
all_values = [ip, model, 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):
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", "")

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
"""
This file stores the MinerFactory instance used by the BadBoardUtility for use in other files.
"""
from miners.miner_factory import MinerFactory
miner_factory = MinerFactory()

View File

@@ -0,0 +1,50 @@
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
from tools.bad_board_util.func.files import import_iplist, export_iplist
from tools.bad_board_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=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_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)

View File

@@ -0,0 +1 @@
from tools.cfg_util.cfg_util_sg import main

View File

@@ -1,6 +1,4 @@
from cfg_util.miner_factory import miner_factory
from cfg_util.layout import window
from cfg_util.ui import ui
from tools.cfg_util.cfg_util_sg.ui import ui
import asyncio
import sys

View File

@@ -6,8 +6,8 @@ import time
import aiofiles
import toml
from cfg_util.func.ui import update_ui_with_data
from cfg_util.layout import window
from tools.cfg_util.cfg_util_sg.func.ui import update_ui_with_data
from tools.cfg_util.cfg_util_sg.layout import window
from config.bos import bos_config_convert, general_config_convert_bos
@@ -30,6 +30,27 @@ async def import_iplist(file_location):
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) 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) 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):
@@ -59,10 +80,11 @@ async def import_config_file(file_location):
async def export_config_file(file_location, config):
await update_ui_with_data("status", "Exporting")
config = await 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(await general_config_convert_bos(config))
await file.write(config)
await update_ui_with_data("status", "")

View File

@@ -4,12 +4,12 @@ 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 tools.cfg_util.cfg_util_sg.func.parse_data import safe_parse_api_data
from tools.cfg_util.cfg_util_sg.func.ui import update_ui_with_data, update_prog_bar, set_progress_bar_len
from tools.cfg_util.cfg_util_sg.layout import window
from tools.cfg_util.cfg_util_sg.miner_factory import miner_factory
from config.bos import bos_config_convert
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS, CFG_UTIL_REBOOT_THREADS as REBOOT_THREADS
async def import_config(idx):
@@ -30,6 +30,7 @@ async def scan_network(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:
@@ -74,6 +75,72 @@ async def flip_light(ip):
await miner.fault_light_off()
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
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 = 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))
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
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 = 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))
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 send_config_generator(miners: list, config):
loop = asyncio.get_event_loop()
config_tasks = []
@@ -93,6 +160,7 @@ 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
asyncio.create_task(update_prog_bar(progress_bar_len))
get_miner_genenerator = miner_factory.get_miner_generator(ips)
all_miners = []
async for miner in get_miner_genenerator:
@@ -104,16 +172,28 @@ async def send_config(ips: list, 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")
await asyncio.sleep(3)
await refresh_data(ips)
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]
@@ -122,14 +202,25 @@ async def get_data(ip_list: list):
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["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]
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")
@@ -138,6 +229,9 @@ async def get_data(ip_list: list):
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()
await set_progress_bar_len(3 * network_size)
@@ -174,7 +268,8 @@ async def scan_and_get_data(network):
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["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)
@@ -189,60 +284,91 @@ async def scan_and_get_data(network):
async def get_formatted_data(ip: ipaddress.ip_address):
miner = await miner_factory.get_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:
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)
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:
print(e)
return {'TH/s': 0, 'IP': str(miner.ip), 'model': 'Unknown', 'temp': 0, 'host': 'Unknown', 'user': 'Unknown',
'wattage': 0}
if miner_data:
# get all data from summary
if "summary" in miner_data.keys():
if not miner_data["summary"][0].get("SUMMARY") == []:
# 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, this is the only place to get this for most miners as far as I know
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)
# alternate temperature data, for BraiinsOS
if "temps" in miner_data.keys():
if 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"]
# 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:
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']
print(miner_data['pools'][0])
user = "Blank"
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]
# 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")
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}

View File

@@ -1,7 +1,7 @@
import ipaddress
import re
from cfg_util.layout import window
from tools.cfg_util.cfg_util_sg.layout import window
import pyperclip

File diff suppressed because one or more lines are too long

View File

@@ -2,11 +2,12 @@ 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 tools.cfg_util.cfg_util_sg.layout import window, generate_config_layout
from tools.cfg_util.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
from tools.cfg_util.cfg_util_sg.func.files import import_iplist, \
import_config_file, export_iplist, export_config_file, export_csv
from tools.cfg_util.cfg_util_sg.func.ui import sort_data, copy_from_table
from network import MinerNetwork
@@ -35,7 +36,7 @@ async def ui():
miner_network = MinerNetwork(ip_addr=network[0], mask=network[1])
else:
miner_network = MinerNetwork(value['miner_network'])
asyncio.create_task(scan_network(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=())
@@ -44,28 +45,26 @@ async def ui():
if event == 'import_config':
if 2 > len(value['ip_table']) > 0:
asyncio.create_task(import_config(value['ip_table']))
if event == "restart_miner_backend":
asyncio.create_task(restart_miners_backend([window['ip_table'].Values[item][0] for item in value['ip_table']]))
if event == "reboot_miners":
asyncio.create_task(reboot_miners([window['ip_table'].Values[item][0] for item in 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 == "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":
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 == "refresh_data":
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__":

View File

View File

@@ -0,0 +1,50 @@
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