Compare commits
40 Commits
first-rele
...
cfg_util-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88539650ca | ||
|
|
3cf0162892 | ||
|
|
51e9e19409 | ||
|
|
c93d99b27c | ||
|
|
770b17c86b | ||
|
|
4e8ff9ea74 | ||
|
|
8ec8c57e31 | ||
|
|
48aa7232b1 | ||
|
|
1f3ffe96a1 | ||
|
|
e0505a31ca | ||
|
|
3ecc27b3f9 | ||
|
|
5d66c539d4 | ||
|
|
c751d53398 | ||
|
|
ea1e8abeac | ||
|
|
8d3f6a3c06 | ||
|
|
f35adf0ae4 | ||
|
|
848ac6ef7c | ||
|
|
6db7cd4a1f | ||
|
|
23d465a733 | ||
|
|
1148946a29 | ||
|
|
e77cbc5415 | ||
|
|
5ecb87ec63 | ||
|
|
8ef135dfd7 | ||
|
|
c26a2cc99e | ||
|
|
e0d8078bf1 | ||
|
|
4528060fd0 | ||
|
|
595467487b | ||
|
|
8cba08a900 | ||
|
|
89c009ab11 | ||
|
|
38f93fa212 | ||
|
|
eac2d64468 | ||
|
|
8a2cef15b2 | ||
|
|
c075f3f66a | ||
|
|
d138778f0a | ||
|
|
cf3aefc201 | ||
|
|
d974be5329 | ||
|
|
8c147283ba | ||
|
|
f72ba6582d | ||
|
|
b65badf097 | ||
|
|
cea71d8ca1 |
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Miner Information (If applicable):**
|
||||||
|
- Manufacturer: [e.g. Bitmain, MicroBT]
|
||||||
|
- Type: [e.g. S9, M20]
|
||||||
|
- Firmware Type: [e.g. Stock, BraiinsOS]
|
||||||
|
- Firmware Version:
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
class APIError(Exception):
|
class APIError(Exception):
|
||||||
@@ -17,6 +18,20 @@ class APIError(Exception):
|
|||||||
return "Incorrect API parameters."
|
return "Incorrect API parameters."
|
||||||
|
|
||||||
|
|
||||||
|
class APIWarning(Warning):
|
||||||
|
def __init__(self, *args):
|
||||||
|
if args:
|
||||||
|
self.message = args[0]
|
||||||
|
else:
|
||||||
|
self.message = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.message:
|
||||||
|
return f"{self.message}"
|
||||||
|
else:
|
||||||
|
return "Incorrect API parameters."
|
||||||
|
|
||||||
|
|
||||||
class BaseMinerAPI:
|
class BaseMinerAPI:
|
||||||
def __init__(self, ip: str, port: int = 4028) -> None:
|
def __init__(self, ip: str, port: int = 4028) -> None:
|
||||||
# api port, should be 4028
|
# api port, should be 4028
|
||||||
@@ -41,19 +56,33 @@ class BaseMinerAPI:
|
|||||||
async def multicommand(self, *commands: str) -> dict:
|
async def multicommand(self, *commands: str) -> dict:
|
||||||
"""Creates and sends multiple commands as one command to the miner."""
|
"""Creates and sends multiple commands as one command to the miner."""
|
||||||
# split the commands into a proper list
|
# split the commands into a proper list
|
||||||
commands = [*commands]
|
user_commands = [*commands]
|
||||||
|
allowed_commands = self.get_commands()
|
||||||
for item in commands:
|
|
||||||
# make sure we can actually run the command, otherwise it will fail
|
# make sure we can actually run the command, otherwise it will fail
|
||||||
if item not in self.get_commands():
|
commands = [command for command in user_commands if command in allowed_commands]
|
||||||
# if the command isnt allowed, remove it
|
for item in list(set(user_commands) - set(commands)):
|
||||||
print(f"Removing incorrect command: {item}")
|
warnings.warn(f"""Removing incorrect command: {item}
|
||||||
commands.remove(item)
|
If you are sure you want to use this command please use API.send_command("{item}") instead.""",
|
||||||
|
APIWarning)
|
||||||
# standard multicommand format is "command1+command2"
|
# standard multicommand format is "command1+command2"
|
||||||
# doesnt work for S19 which is dealt with in the send command function
|
# doesnt work for S19 which is dealt with in the send command function
|
||||||
command = "+".join(commands)
|
command = "+".join(commands)
|
||||||
return await self.send_command(command)
|
data = None
|
||||||
|
try:
|
||||||
|
data = await self.send_command(command)
|
||||||
|
except APIError:
|
||||||
|
try:
|
||||||
|
data = {}
|
||||||
|
# S19 handler, try again
|
||||||
|
for cmd in command.split("+"):
|
||||||
|
data[cmd] = []
|
||||||
|
data[cmd].append(await self.send_command(cmd))
|
||||||
|
except APIError as e:
|
||||||
|
raise APIError(e)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
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) -> dict:
|
||||||
"""Send an API command to the miner and return the result."""
|
"""Send an API command to the miner and return the result."""
|
||||||
@@ -94,26 +123,15 @@ class BaseMinerAPI:
|
|||||||
writer.close()
|
writer.close()
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
|
|
||||||
# validate the command suceeded
|
# validate the command succeeded
|
||||||
# also handle for S19 not liking "command1+command2" format
|
validation = self.validate_command_output(data)
|
||||||
if not self.validate_command_output(data):
|
if not validation[0]:
|
||||||
try:
|
raise APIError(validation[1])
|
||||||
data = {}
|
|
||||||
# S19 handler, try again
|
|
||||||
for cmd in command.split("+"):
|
|
||||||
data[cmd] = []
|
|
||||||
data[cmd].append(await self.send_command(cmd))
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
# check again after second try
|
|
||||||
if not self.validate_command_output(data):
|
|
||||||
raise APIError(data["STATUS"][0]["Msg"])
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_command_output(data: dict) -> bool:
|
def validate_command_output(data: dict) -> tuple[bool, str | None]:
|
||||||
"""Check if the returned command output is correctly formatted."""
|
"""Check if the returned command output is correctly formatted."""
|
||||||
# check if the data returned is correct or an error
|
# check if the data returned is correct or an error
|
||||||
# if status isn't a key, it is a multicommand
|
# if status isn't a key, it is a multicommand
|
||||||
@@ -122,20 +140,20 @@ class BaseMinerAPI:
|
|||||||
# make sure not to try to turn id into a dict
|
# make sure not to try to turn id into a dict
|
||||||
if not key == "id":
|
if not key == "id":
|
||||||
# make sure they succeeded
|
# make sure they succeeded
|
||||||
if "STATUS" in data.keys():
|
if "STATUS" in data[key][0].keys():
|
||||||
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
|
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
|
||||||
# this is an error
|
# this is an error
|
||||||
return False
|
return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"]
|
||||||
elif "id" not in data.keys():
|
elif "id" not in data.keys():
|
||||||
if data["STATUS"] not in ["S", "I"]:
|
if data["STATUS"] not in ["S", "I"]:
|
||||||
return False
|
return False, data["Msg"]
|
||||||
else:
|
else:
|
||||||
# make sure the command succeeded
|
# make sure the command succeeded
|
||||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||||
# this is an error
|
# this is an error
|
||||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||||
return False
|
return False, data["STATUS"][0]["Msg"]
|
||||||
return True
|
return True, None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_api_data(data: bytes) -> dict:
|
def load_api_data(data: bytes) -> dict:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class BMMinerAPI(BaseMinerAPI):
|
|||||||
"""
|
"""
|
||||||
API 'config' command.
|
API 'config' command.
|
||||||
|
|
||||||
Returns some miner configuration information:
|
Returns a dict containing some miner configuration information:
|
||||||
ASC Count <- the number of ASCs
|
ASC Count <- the number of ASCs
|
||||||
PGA Count <- the number of PGAs
|
PGA Count <- the number of PGAs
|
||||||
Pool Count <- the number of Pools
|
Pool Count <- the number of Pools
|
||||||
@@ -446,7 +446,6 @@ class BMMinerAPI(BaseMinerAPI):
|
|||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
n: the number of the ASC to disable.
|
n: the number of the ASC to disable.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return await self.send_command("ascdisable", parameters=n)
|
return await self.send_command("ascdisable", parameters=n)
|
||||||
|
|
||||||
|
|||||||
132
API/bosminer.py
132
API/bosminer.py
@@ -2,49 +2,143 @@ from API import BaseMinerAPI
|
|||||||
|
|
||||||
|
|
||||||
class BOSMinerAPI(BaseMinerAPI):
|
class BOSMinerAPI(BaseMinerAPI):
|
||||||
|
"""
|
||||||
|
A class that abstracts the BOSMiner API in the miners.
|
||||||
|
|
||||||
|
Each method corresponds to an API command in BOSMiner.
|
||||||
|
|
||||||
|
BOSMiner API documentation:
|
||||||
|
https://docs.braiins.com/os/plus-en/Development/1_api.html
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ip: the IP address of the miner.
|
||||||
|
port (optional): the port of the API on the miner (standard is 4028)
|
||||||
|
"""
|
||||||
def __init__(self, ip, port=4028):
|
def __init__(self, ip, port=4028):
|
||||||
super().__init__(ip, port)
|
super().__init__(ip, port)
|
||||||
|
|
||||||
async def asccount(self) -> dict:
|
async def asccount(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'asccount' command.
|
||||||
|
|
||||||
|
Returns a dict containing the number of ASC devices.
|
||||||
|
"""
|
||||||
return await self.send_command("asccount")
|
return await self.send_command("asccount")
|
||||||
|
|
||||||
async def asc(self, n: int) -> dict:
|
async def asc(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'asc' command.
|
||||||
|
|
||||||
|
Returns a dict containing the details of a single ASC of number N.
|
||||||
|
|
||||||
|
n: the ASC device to get details of.
|
||||||
|
"""
|
||||||
return await self.send_command("asc", parameters=n)
|
return await self.send_command("asc", parameters=n)
|
||||||
|
|
||||||
async def devdetails(self) -> dict:
|
async def devdetails(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'devdetails' command.
|
||||||
|
|
||||||
|
Returns a dict containing all devices with their static details.
|
||||||
|
"""
|
||||||
return await self.send_command("devdetails")
|
return await self.send_command("devdetails")
|
||||||
|
|
||||||
async def devs(self) -> dict:
|
async def devs(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'devs' command.
|
||||||
|
|
||||||
|
Returns a dict containing each PGA/ASC with their details.
|
||||||
|
"""
|
||||||
return await self.send_command("devs")
|
return await self.send_command("devs")
|
||||||
|
|
||||||
async def edevs(self, old: bool = False) -> dict:
|
async def edevs(self, old: bool = False) -> dict:
|
||||||
|
"""
|
||||||
|
API 'edevs' command.
|
||||||
|
|
||||||
|
Returns a dict containing each PGA/ASC with their details,
|
||||||
|
ignoring blacklisted devices and zombie devices.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old (optional): include zombie devices that became zombies less than 'old' seconds ago
|
||||||
|
"""
|
||||||
if old:
|
if old:
|
||||||
return await self.send_command("edevs", parameters="old")
|
return await self.send_command("edevs", parameters="old")
|
||||||
else:
|
else:
|
||||||
return await self.send_command("edevs")
|
return await self.send_command("edevs")
|
||||||
|
|
||||||
async def pools(self) -> dict:
|
async def pools(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'pools' command.
|
||||||
|
|
||||||
|
Returns a dict containing the status of each pool.
|
||||||
|
"""
|
||||||
return await self.send_command("pools")
|
return await self.send_command("pools")
|
||||||
|
|
||||||
async def summary(self) -> dict:
|
async def summary(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'summary' command.
|
||||||
|
|
||||||
|
Returns a dict containing the status summary of the miner.
|
||||||
|
"""
|
||||||
return await self.send_command("summary")
|
return await self.send_command("summary")
|
||||||
|
|
||||||
async def stats(self) -> dict:
|
async def stats(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'stats' command.
|
||||||
|
|
||||||
|
Returns a dict containing stats for all device/pool with more than 1 getwork.
|
||||||
|
"""
|
||||||
return await self.send_command("stats")
|
return await self.send_command("stats")
|
||||||
|
|
||||||
async def version(self) -> dict:
|
async def version(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'version' command.
|
||||||
|
|
||||||
|
Returns a dict containing version information.
|
||||||
|
"""
|
||||||
return await self.send_command("version")
|
return await self.send_command("version")
|
||||||
|
|
||||||
async def estats(self) -> dict:
|
async def estats(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'estats' command.
|
||||||
|
|
||||||
|
Returns a dict containing stats for all device/pool with more than 1 getwork,
|
||||||
|
ignoring zombie devices.
|
||||||
|
"""
|
||||||
return await self.send_command("estats")
|
return await self.send_command("estats")
|
||||||
|
|
||||||
async def check(self) -> dict:
|
async def check(self, command: str) -> dict:
|
||||||
return await self.send_command("check")
|
"""
|
||||||
|
API 'check' command.
|
||||||
|
|
||||||
|
Returns information about a command:
|
||||||
|
Exists (Y/N) <- the command exists in this version
|
||||||
|
Access (Y/N) <- you have access to use the command
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
command: the command to get information about.
|
||||||
|
"""
|
||||||
|
return await self.send_command("check", parameters=command)
|
||||||
|
|
||||||
async def coin(self) -> dict:
|
async def coin(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'coin' command.
|
||||||
|
|
||||||
|
Returns information about the current coin being mined:
|
||||||
|
Hash Method <- the hashing algorithm
|
||||||
|
Current Block Time <- blocktime as a float, 0 means none
|
||||||
|
Current Block Hash <- the hash of the current block, blank means none
|
||||||
|
LP <- whether LP is in use on at least 1 pool
|
||||||
|
Network Difficulty: the current network difficulty
|
||||||
|
"""
|
||||||
return await self.send_command("coin")
|
return await self.send_command("coin")
|
||||||
|
|
||||||
async def lcd(self) -> dict:
|
async def lcd(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'lcd' command.
|
||||||
|
|
||||||
|
Returns a dict containing an all in one status summary of the miner.
|
||||||
|
"""
|
||||||
return await self.send_command("lcd")
|
return await self.send_command("lcd")
|
||||||
|
|
||||||
async def switchpool(self, n: int) -> dict:
|
async def switchpool(self, n: int) -> dict:
|
||||||
@@ -73,19 +167,53 @@ class BOSMinerAPI(BaseMinerAPI):
|
|||||||
# return await self.send_command("removepool", parameters=n)
|
# return await self.send_command("removepool", parameters=n)
|
||||||
|
|
||||||
async def fans(self) -> dict:
|
async def fans(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'fans' command.
|
||||||
|
|
||||||
|
Returns a dict containing information on fans and fan speeds.
|
||||||
|
"""
|
||||||
return await self.send_command("fans")
|
return await self.send_command("fans")
|
||||||
|
|
||||||
async def tempctrl(self) -> dict:
|
async def tempctrl(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'tempctrl' command.
|
||||||
|
|
||||||
|
Returns a dict containing temp control configuration.
|
||||||
|
"""
|
||||||
return await self.send_command("tempctrl")
|
return await self.send_command("tempctrl")
|
||||||
|
|
||||||
async def temps(self) -> dict:
|
async def temps(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'temps' command.
|
||||||
|
|
||||||
|
Returns a dict containing temperature information.
|
||||||
|
"""
|
||||||
return await self.send_command("temps")
|
return await self.send_command("temps")
|
||||||
|
|
||||||
async def tunerstatus(self) -> dict:
|
async def tunerstatus(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'tunerstatus' command.
|
||||||
|
|
||||||
|
Returns a dict containing tuning stats.
|
||||||
|
"""
|
||||||
return await self.send_command("tunerstatus")
|
return await self.send_command("tunerstatus")
|
||||||
|
|
||||||
async def pause(self) -> dict:
|
async def pause(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'pause' command.
|
||||||
|
|
||||||
|
Pauses mining and stops power consumption and waits for resume command.
|
||||||
|
|
||||||
|
Returns a dict stating that the miner paused mining.
|
||||||
|
"""
|
||||||
return await self.send_command("pause")
|
return await self.send_command("pause")
|
||||||
|
|
||||||
async def resume(self) -> dict:
|
async def resume(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'pause' command.
|
||||||
|
|
||||||
|
Resumes mining on the miner.
|
||||||
|
|
||||||
|
Returns a dict stating that the miner resumed mining.
|
||||||
|
"""
|
||||||
return await self.send_command("resume")
|
return await self.send_command("resume")
|
||||||
|
|||||||
247
API/btminer.py
247
API/btminer.py
@@ -1,12 +1,12 @@
|
|||||||
from API import BaseMinerAPI, APIError
|
from API import BaseMinerAPI, APIError
|
||||||
|
|
||||||
from passlib.hash import md5_crypt
|
from passlib.handlers.md5_crypt import md5_crypt
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import binascii
|
import binascii
|
||||||
from Crypto.Cipher import AES
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
|
||||||
@@ -42,13 +42,19 @@ def _add_to_16(s: str) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def parse_btminer_priviledge_data(token_data, data):
|
def parse_btminer_priviledge_data(token_data, data):
|
||||||
|
# get the encoded data from the dict
|
||||||
enc_data = data['enc']
|
enc_data = data['enc']
|
||||||
|
# get the aes key from the token data
|
||||||
aeskey = hashlib.sha256(token_data['host_passwd_md5'].encode()).hexdigest()
|
aeskey = hashlib.sha256(token_data['host_passwd_md5'].encode()).hexdigest()
|
||||||
|
# unhexlify the aes key
|
||||||
aeskey = binascii.unhexlify(aeskey.encode())
|
aeskey = binascii.unhexlify(aeskey.encode())
|
||||||
aes = AES.new(aeskey, AES.MODE_ECB)
|
# create the required decryptor
|
||||||
ret_msg = json.loads(str(
|
aes = Cipher(algorithms.AES(aeskey), modes.ECB())
|
||||||
aes.decrypt(base64.decodebytes(bytes(
|
decryptor = aes.decryptor()
|
||||||
enc_data, encoding='utf8'))).rstrip(b'\0').decode("utf8")))
|
# decode the message with the decryptor
|
||||||
|
ret_msg = json.loads(decryptor.update(
|
||||||
|
base64.decodebytes(bytes(enc_data, encoding='utf8'))
|
||||||
|
).rstrip(b'\0').decode("utf8"))
|
||||||
return ret_msg
|
return ret_msg
|
||||||
|
|
||||||
|
|
||||||
@@ -60,13 +66,12 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
|
|||||||
# unhexlify the encoded host_passwd
|
# unhexlify the encoded host_passwd
|
||||||
aeskey = binascii.unhexlify(aeskey.encode())
|
aeskey = binascii.unhexlify(aeskey.encode())
|
||||||
# create a new AES key
|
# create a new AES key
|
||||||
aes = AES.new(aeskey, AES.MODE_ECB)
|
aes = Cipher(algorithms.AES(aeskey), modes.ECB())
|
||||||
|
encryptor = aes.encryptor()
|
||||||
# dump the command to json
|
# dump the command to json
|
||||||
api_json_str = json.dumps(command)
|
api_json_str = json.dumps(command)
|
||||||
# encode the json command with the aes key
|
# encode the json command with the aes key
|
||||||
api_json_str_enc = str(base64.encodebytes(
|
api_json_str_enc = base64.encodebytes(encryptor.update(_add_to_16(api_json_str))).decode("utf-8").replace("\n", "")
|
||||||
aes.encrypt(_add_to_16(api_json_str))),
|
|
||||||
encoding='utf8').replace('\n', '')
|
|
||||||
# label the data as being encoded
|
# label the data as being encoded
|
||||||
data_enc = {'enc': 1, 'data': api_json_str_enc}
|
data_enc = {'enc': 1, 'data': api_json_str_enc}
|
||||||
# dump the labeled data to json
|
# dump the labeled data to json
|
||||||
@@ -82,7 +87,9 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
|
|
||||||
async def send_command(self, command: str | bytes, **kwargs) -> dict:
|
async def send_command(self, command: str | bytes, **kwargs) -> dict:
|
||||||
"""Send an API command to the miner and return the result."""
|
"""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
|
||||||
if isinstance(command, str):
|
if isinstance(command, str):
|
||||||
|
# if it is a string, put it into the standard command format
|
||||||
command = json.dumps({"command": command}).encode("utf-8")
|
command = json.dumps({"command": command}).encode("utf-8")
|
||||||
try:
|
try:
|
||||||
# get reader and writer streams
|
# get reader and writer streams
|
||||||
@@ -116,18 +123,28 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
writer.close()
|
writer.close()
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
|
|
||||||
|
# check if th returned data is encoded
|
||||||
if 'enc' in data.keys():
|
if 'enc' in data.keys():
|
||||||
|
# try to parse the encoded data
|
||||||
try:
|
try:
|
||||||
data = parse_btminer_priviledge_data(self.current_token, data)
|
data = parse_btminer_priviledge_data(self.current_token, data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
if not self.validate_command_output(data):
|
# if it fails to validate, it is likely an error
|
||||||
raise APIError(data["Msg"])
|
validation = self.validate_command_output(data)
|
||||||
|
if not validation[0]:
|
||||||
|
raise APIError(validation[1])
|
||||||
|
|
||||||
|
# return the parsed json as a dict
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def get_token(self):
|
async def get_token(self):
|
||||||
|
"""
|
||||||
|
API 'get_token' command.
|
||||||
|
|
||||||
|
Returns an encoded token and md5 password, which are used for the privileged API.
|
||||||
|
"""
|
||||||
data = await self.send_command("get_token")
|
data = await self.send_command("get_token")
|
||||||
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + '$')
|
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + '$')
|
||||||
pwd = pwd.split('$')
|
pwd = pwd.split('$')
|
||||||
@@ -138,7 +155,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
self.current_token = {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
|
self.current_token = {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
|
||||||
return {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
|
return {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
|
||||||
|
|
||||||
#### privileged COMMANDS ####
|
#### PRIVILEGED COMMANDS ####
|
||||||
# Please read the top of this file to learn
|
# 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.
|
# use these commands.
|
||||||
@@ -147,8 +164,13 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
pool_1: str, worker_1: str, passwd_1: str,
|
pool_1: str, worker_1: str, passwd_1: str,
|
||||||
pool_2: str = None, worker_2: str = None, passwd_2: str = None,
|
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_3: str = None, worker_3: str = None, passwd_3: str = None):
|
||||||
|
# get the token and password from the miner
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
if pool_2 and pool_3:
|
|
||||||
|
# parse pool data
|
||||||
|
if not pool_1:
|
||||||
|
raise APIError("No pools set.")
|
||||||
|
elif pool_2 and pool_3:
|
||||||
command = {
|
command = {
|
||||||
"cmd": "update_pools",
|
"cmd": "update_pools",
|
||||||
"pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1,
|
"pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1,
|
||||||
@@ -166,16 +188,33 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
"cmd": "update_pools",
|
"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)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
|
# send the command
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def restart_btminer(self):
|
async def restart(self):
|
||||||
|
"""
|
||||||
|
API 'restart_btminer' command
|
||||||
|
|
||||||
|
Returns a reply informing of the restart and restarts BTMiner.
|
||||||
|
"""
|
||||||
command = {"cmd": "restart_btminer"}
|
command = {"cmd": "restart_btminer"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def power_off(self, respbefore: bool = True):
|
async def power_off(self, respbefore: bool = True):
|
||||||
|
"""
|
||||||
|
API 'power_off' command.
|
||||||
|
|
||||||
|
Powers off the mining of the miner.
|
||||||
|
|
||||||
|
Returns info on the power off.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
respbefore (optional): respond before powering off.
|
||||||
|
"""
|
||||||
if respbefore:
|
if respbefore:
|
||||||
command = {"cmd": "power_off", "respbefore": "true"}
|
command = {"cmd": "power_off", "respbefore": "true"}
|
||||||
else:
|
else:
|
||||||
@@ -185,24 +224,58 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def power_on(self):
|
async def power_on(self):
|
||||||
|
"""
|
||||||
|
API 'power_on' command.
|
||||||
|
|
||||||
|
Powers on the mining of the miner.
|
||||||
|
|
||||||
|
Returns info on the power on.
|
||||||
|
"""
|
||||||
command = {"cmd": "power_on"}
|
command = {"cmd": "power_on"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def reset_led(self):
|
async def reset_led(self):
|
||||||
|
"""
|
||||||
|
API 'reset_led' command.
|
||||||
|
|
||||||
|
Resets the LED flashing to normal.
|
||||||
|
|
||||||
|
Returns a confirmation of resetting the LED.
|
||||||
|
"""
|
||||||
command = {"cmd": "set_led", "param": "auto"}
|
command = {"cmd": "set_led", "param": "auto"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_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):
|
async def set_led(self, color: str = "red", period: int = 2000, duration: int = 1000, start: int = 0):
|
||||||
|
"""
|
||||||
|
API 'set_led' command.
|
||||||
|
|
||||||
|
Sets the LED to do some pattern set with parameters.
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
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()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def set_low_power(self):
|
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.
|
||||||
|
"""
|
||||||
command = {"cmd": "set_low_power"}
|
command = {"cmd": "set_low_power"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
@@ -214,18 +287,43 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
return NotImplementedError
|
return NotImplementedError
|
||||||
|
|
||||||
async def reboot(self):
|
async def reboot(self):
|
||||||
|
"""
|
||||||
|
API 'reboot' command.
|
||||||
|
|
||||||
|
Reboots the miner.
|
||||||
|
|
||||||
|
Returns the status of the command then reboots.
|
||||||
|
"""
|
||||||
command = {"cmd": "reboot"}
|
command = {"cmd": "reboot"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def factory_reset(self):
|
async def factory_reset(self):
|
||||||
|
"""
|
||||||
|
API 'factory_reset' command.
|
||||||
|
|
||||||
|
Resets the miner to factory defaults.
|
||||||
|
|
||||||
|
Returns the status of the command then resets.
|
||||||
|
"""
|
||||||
command = {"cmd": "factory_reset"}
|
command = {"cmd": "factory_reset"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def update_pwd(self, old_pwd: str, new_pwd: str):
|
async def update_pwd(self, old_pwd: str, new_pwd: str):
|
||||||
|
"""
|
||||||
|
API 'update_pwd' command.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
# check if password length is greater than 8 bytes
|
# check if password length is greater than 8 bytes
|
||||||
if len(new_pwd.encode('utf-8')) > 8:
|
if len(new_pwd.encode('utf-8')) > 8:
|
||||||
return APIError(
|
return APIError(
|
||||||
@@ -236,6 +334,13 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def set_target_freq(self, percent: int):
|
async def set_target_freq(self, percent: int):
|
||||||
|
"""
|
||||||
|
API 'set_target_freq' command.
|
||||||
|
|
||||||
|
Sets the frequency for the miner ot use.
|
||||||
|
|
||||||
|
Returns the status of setting the frequency.
|
||||||
|
"""
|
||||||
if not -10 < percent < 100:
|
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 range. Please set a % between -10 and 100")
|
||||||
command = {"cmd": "set_target_freq", "percent": str(percent)}
|
command = {"cmd": "set_target_freq", "percent": str(percent)}
|
||||||
@@ -244,36 +349,82 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def enable_fast_boot(self):
|
async def enable_fast_boot(self):
|
||||||
|
"""
|
||||||
|
API 'enable_fast_boot' command.
|
||||||
|
|
||||||
|
Turns on the fast boot feature on the miner.
|
||||||
|
|
||||||
|
Returns the status of setting the fast boot to on.
|
||||||
|
"""
|
||||||
command = {"cmd": "enable_btminer_fast_boot"}
|
command = {"cmd": "enable_btminer_fast_boot"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def disable_fast_boot(self):
|
async def disable_fast_boot(self):
|
||||||
|
"""
|
||||||
|
API 'disable'_fast_boot' command.
|
||||||
|
|
||||||
|
Turns off the fast boot feature on the miner.
|
||||||
|
|
||||||
|
Returns the status of setting the fast boot to off.
|
||||||
|
"""
|
||||||
command = {"cmd": "disable_btminer_fast_boot"}
|
command = {"cmd": "disable_btminer_fast_boot"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def enable_web_pools(self):
|
async def enable_web_pools(self):
|
||||||
|
"""
|
||||||
|
API 'enable_web_pools' command.
|
||||||
|
|
||||||
|
Turns on the ability to change the pools through the web interface.
|
||||||
|
|
||||||
|
Returns the status of setting the web pools to enabled.
|
||||||
|
"""
|
||||||
command = {"cmd": "enable_web_pools"}
|
command = {"cmd": "enable_web_pools"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def disable_web_pools(self):
|
async def disable_web_pools(self):
|
||||||
|
"""
|
||||||
|
API 'disable_web_pools' command.
|
||||||
|
|
||||||
|
Turns off the ability to change the pools through the web interface.
|
||||||
|
|
||||||
|
Returns the status of setting the web pools to disabled.
|
||||||
|
"""
|
||||||
command = {"cmd": "disable_web_pools"}
|
command = {"cmd": "disable_web_pools"}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def set_hostname(self, hostname: str):
|
async def set_hostname(self, hostname: str):
|
||||||
|
"""
|
||||||
|
API 'set_hostname' command.
|
||||||
|
|
||||||
|
Sets the hostname of the miner.
|
||||||
|
|
||||||
|
Returns the status of setting the hostname.
|
||||||
|
"""
|
||||||
command = {"cmd": "set_hostname", "hostname": hostname}
|
command = {"cmd": "set_hostname", "hostname": hostname}
|
||||||
token_data = await self.get_token()
|
token_data = await self.get_token()
|
||||||
enc_command = create_privileged_cmd(token_data, command)
|
enc_command = create_privileged_cmd(token_data, command)
|
||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def set_power_pct(self, percent: int):
|
async def set_power_pct(self, percent: int):
|
||||||
|
"""
|
||||||
|
API 'set_power_pct' command.
|
||||||
|
|
||||||
|
Sets the percent of power the miner should use.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
if not 0 < percent < 100:
|
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 range. Please set a % between 0 and 100")
|
||||||
command = {"cmd": "set_power_pct", "percent": str(percent)}
|
command = {"cmd": "set_power_pct", "percent": str(percent)}
|
||||||
@@ -282,6 +433,18 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
return await self.send_command(enc_command)
|
return await self.send_command(enc_command)
|
||||||
|
|
||||||
async def pre_power_on(self, complete: bool, msg: str):
|
async def pre_power_on(self, complete: bool, msg: str):
|
||||||
|
"""
|
||||||
|
API 'pre_power_on' command.
|
||||||
|
|
||||||
|
Preheats the miner for the 'power_on' command. Can also be used to query the status of pre powering on.
|
||||||
|
|
||||||
|
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"
|
||||||
|
"""
|
||||||
|
|
||||||
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(
|
return APIError(
|
||||||
'Message is incorrect, please choose one of '
|
'Message is incorrect, please choose one of '
|
||||||
@@ -299,28 +462,82 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
#### END privileged COMMANDS ####
|
#### END privileged COMMANDS ####
|
||||||
|
|
||||||
async def summary(self):
|
async def summary(self):
|
||||||
|
"""
|
||||||
|
API 'summary' command.
|
||||||
|
|
||||||
|
Returns a dict containing the status summary of the miner.
|
||||||
|
"""
|
||||||
return await self.send_command("summary")
|
return await self.send_command("summary")
|
||||||
|
|
||||||
async def pools(self):
|
async def pools(self):
|
||||||
|
"""
|
||||||
|
API 'pools' command.
|
||||||
|
|
||||||
|
Returns a dict containing the status of each pool.
|
||||||
|
"""
|
||||||
return await self.send_command("pools")
|
return await self.send_command("pools")
|
||||||
|
|
||||||
async def devs(self):
|
async def devs(self):
|
||||||
|
"""
|
||||||
|
API 'devs' command.
|
||||||
|
|
||||||
|
Returns a dict containing each PGA/ASC with their details.
|
||||||
|
"""
|
||||||
return await self.send_command("devs")
|
return await self.send_command("devs")
|
||||||
|
|
||||||
async def edevs(self):
|
async def edevs(self):
|
||||||
|
"""
|
||||||
|
API 'edevs' command.
|
||||||
|
|
||||||
|
Returns a dict containing each PGA/ASC with their details,
|
||||||
|
ignoring blacklisted devices and zombie devices.
|
||||||
|
"""
|
||||||
return await self.send_command("edevs")
|
return await self.send_command("edevs")
|
||||||
|
|
||||||
async def devdetails(self):
|
async def devdetails(self):
|
||||||
|
"""
|
||||||
|
API 'devdetails' command.
|
||||||
|
|
||||||
|
Returns a dict containing all devices with their static details.
|
||||||
|
"""
|
||||||
return await self.send_command("devdetails")
|
return await self.send_command("devdetails")
|
||||||
|
|
||||||
async def get_psu(self):
|
async def get_psu(self):
|
||||||
|
"""
|
||||||
|
API 'get_psu' command.
|
||||||
|
|
||||||
|
Returns a dict containing PSU and power information.
|
||||||
|
"""
|
||||||
return await self.send_command("get_psu")
|
return await self.send_command("get_psu")
|
||||||
|
|
||||||
async def version(self):
|
async def version(self):
|
||||||
|
"""
|
||||||
|
API 'get_version' command.
|
||||||
|
|
||||||
|
Returns a dict containing version information.
|
||||||
|
"""
|
||||||
|
return await self.get_version()
|
||||||
|
|
||||||
|
async def get_version(self):
|
||||||
|
"""
|
||||||
|
API 'get_version' command.
|
||||||
|
|
||||||
|
Returns a dict containing version information.
|
||||||
|
"""
|
||||||
return await self.send_command("get_version")
|
return await self.send_command("get_version")
|
||||||
|
|
||||||
async def status(self):
|
async def status(self):
|
||||||
|
"""
|
||||||
|
API 'status' command.
|
||||||
|
|
||||||
|
Returns a dict containing BTMiner status and firmware version.
|
||||||
|
"""
|
||||||
return await self.send_command("status")
|
return await self.send_command("status")
|
||||||
|
|
||||||
async def get_miner_info(self):
|
async def get_miner_info(self):
|
||||||
|
"""
|
||||||
|
API 'get_miner_info' command.
|
||||||
|
|
||||||
|
Returns a dict containing general miner info.
|
||||||
|
"""
|
||||||
return await self.send_command("get_miner_info")
|
return await self.send_command("get_miner_info")
|
||||||
|
|||||||
369
API/cgminer.py
369
API/cgminer.py
@@ -2,6 +2,18 @@ from API import BaseMinerAPI
|
|||||||
|
|
||||||
|
|
||||||
class CGMinerAPI(BaseMinerAPI):
|
class CGMinerAPI(BaseMinerAPI):
|
||||||
|
"""
|
||||||
|
A class that abstracts the CGMiner API in the miners.
|
||||||
|
|
||||||
|
Each method corresponds to an API command in CGMiner.
|
||||||
|
|
||||||
|
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)
|
||||||
|
"""
|
||||||
def __init__(self, ip, port=4028):
|
def __init__(self, ip, port=4028):
|
||||||
super().__init__(ip, port)
|
super().__init__(ip, port)
|
||||||
|
|
||||||
@@ -29,139 +41,492 @@ class CGMinerAPI(BaseMinerAPI):
|
|||||||
return await self.send_command("config")
|
return await self.send_command("config")
|
||||||
|
|
||||||
async def summary(self) -> dict:
|
async def summary(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'summary' command.
|
||||||
|
|
||||||
|
Returns a dict containing the status summary of the miner.
|
||||||
|
"""
|
||||||
return await self.send_command("summary")
|
return await self.send_command("summary")
|
||||||
|
|
||||||
async def pools(self) -> dict:
|
async def pools(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'pools' command.
|
||||||
|
|
||||||
|
Returns a dict containing the status of each pool.
|
||||||
|
"""
|
||||||
return await self.send_command("pools")
|
return await self.send_command("pools")
|
||||||
|
|
||||||
async def devs(self) -> dict:
|
async def devs(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'devs' command.
|
||||||
|
|
||||||
|
Returns a dict containing each PGA/ASC with their details.
|
||||||
|
"""
|
||||||
return await self.send_command("devs")
|
return await self.send_command("devs")
|
||||||
|
|
||||||
async def edevs(self, old: bool = False) -> dict:
|
async def edevs(self, old: bool = False) -> dict:
|
||||||
|
"""
|
||||||
|
API 'edevs' command.
|
||||||
|
|
||||||
|
Returns a dict containing each PGA/ASC with their details,
|
||||||
|
ignoring blacklisted devices and zombie devices.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old (optional): include zombie devices that became zombies less than 'old' seconds ago
|
||||||
|
"""
|
||||||
if old:
|
if old:
|
||||||
return await self.send_command("edevs", parameters="old")
|
return await self.send_command("edevs", parameters="old")
|
||||||
else:
|
else:
|
||||||
return await self.send_command("edevs")
|
return await self.send_command("edevs")
|
||||||
|
|
||||||
async def pga(self, n: int) -> dict:
|
async def pga(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'pga' command.
|
||||||
|
|
||||||
|
Returns a dict containing the details of a single PGA of number N.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the PGA to get details of.
|
||||||
|
"""
|
||||||
return await self.send_command("pga", parameters=n)
|
return await self.send_command("pga", parameters=n)
|
||||||
|
|
||||||
async def pgacount(self) -> dict:
|
async def pgacount(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'pgacount' command.
|
||||||
|
|
||||||
|
Returns a dict containing the number of PGA devices.
|
||||||
|
"""
|
||||||
return await self.send_command("pgacount")
|
return await self.send_command("pgacount")
|
||||||
|
|
||||||
async def switchpool(self, n: int) -> dict:
|
async def switchpool(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'switchpool' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of switching pools.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the pool to switch to.
|
||||||
|
"""
|
||||||
return await self.send_command("switchpool", parameters=n)
|
return await self.send_command("switchpool", parameters=n)
|
||||||
|
|
||||||
async def enablepool(self, n: int) -> dict:
|
async def enablepool(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'enablepool' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of enabling the pool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the pool to enable.
|
||||||
|
"""
|
||||||
return await self.send_command("enablepool", parameters=n)
|
return await self.send_command("enablepool", parameters=n)
|
||||||
|
|
||||||
async def addpool(self, url: str, username: str, password: str) -> dict:
|
async def addpool(self, url: str, username: str, password: str) -> dict:
|
||||||
|
"""
|
||||||
|
API 'addpool' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of adding the 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 await self.send_command("addpool", parameters=f"{url}, {username}, {password}")
|
return await self.send_command("addpool", parameters=f"{url}, {username}, {password}")
|
||||||
|
|
||||||
async def poolpriority(self, *n: int) -> dict:
|
async def poolpriority(self, *n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'poolpriority' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of setting pool priority.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: pool numbers in order of priority.
|
||||||
|
"""
|
||||||
return await self.send_command("poolpriority", parameters=f"{','.join([str(item) for item in n])}")
|
return await self.send_command("poolpriority", parameters=f"{','.join([str(item) for item in n])}")
|
||||||
|
|
||||||
async def poolquota(self, n: int, q: int) -> dict:
|
async def poolquota(self, n: int, q: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'poolquota' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of setting pool quota.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: pool number to set quota on.
|
||||||
|
q: quota to set the pool to.
|
||||||
|
"""
|
||||||
return await self.send_command("poolquota", parameters=f"{n}, {q}")
|
return await self.send_command("poolquota", parameters=f"{n}, {q}")
|
||||||
|
|
||||||
async def disablepool(self, n: int) -> dict:
|
async def disablepool(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'disablepool' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of disabling the pool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the pool to disable.
|
||||||
|
"""
|
||||||
return await self.send_command("disablepool", parameters=n)
|
return await self.send_command("disablepool", parameters=n)
|
||||||
|
|
||||||
async def removepool(self, n: int) -> dict:
|
async def removepool(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'removepool' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of removing the pool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the pool to remove.
|
||||||
|
"""
|
||||||
return await self.send_command("removepool", parameters=n)
|
return await self.send_command("removepool", parameters=n)
|
||||||
|
|
||||||
async def save(self, filename: str = None) -> dict:
|
async def save(self, filename: str = None) -> dict:
|
||||||
|
"""
|
||||||
|
API 'save' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of saving the config file..
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
filename (optional): the filename to save the config as.
|
||||||
|
"""
|
||||||
if filename:
|
if filename:
|
||||||
return await self.send_command("save", parameters=filename)
|
return await self.send_command("save", parameters=filename)
|
||||||
else:
|
else:
|
||||||
return await self.send_command("save")
|
return await self.send_command("save")
|
||||||
|
|
||||||
async def quit(self) -> dict:
|
async def quit(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'quit' command.
|
||||||
|
|
||||||
|
Returns a single "BYE" before CGMiner quits.
|
||||||
|
"""
|
||||||
return await self.send_command("quit")
|
return await self.send_command("quit")
|
||||||
|
|
||||||
async def notify(self) -> dict:
|
async def notify(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'notify' command.
|
||||||
|
|
||||||
|
Returns a dict containing the last status and count of each devices problem(s).
|
||||||
|
"""
|
||||||
return await self.send_command("notify")
|
return await self.send_command("notify")
|
||||||
|
|
||||||
async def privileged(self) -> dict:
|
async def privileged(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'privileged' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with an error if you have no privileged access.
|
||||||
|
"""
|
||||||
return await self.send_command("privileged")
|
return await self.send_command("privileged")
|
||||||
|
|
||||||
async def pgaenable(self, n: int) -> dict:
|
async def pgaenable(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'pgaenable' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of enabling the PGA device N.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the PGA to enable.
|
||||||
|
"""
|
||||||
return await self.send_command("pgaenable", parameters=n)
|
return await self.send_command("pgaenable", parameters=n)
|
||||||
|
|
||||||
async def pgadisable(self, n: int) -> dict:
|
async def pgadisable(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'pgadisable' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of disabling the PGA device N.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the PGA to disable.
|
||||||
|
"""
|
||||||
return await self.send_command("pgadisable", parameters=n)
|
return await self.send_command("pgadisable", parameters=n)
|
||||||
|
|
||||||
async def pgaidentify(self, n: int) -> dict:
|
async def pgaidentify(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'pgaidentify' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of identifying the PGA device N.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the PGA to identify.
|
||||||
|
"""
|
||||||
return await self.send_command("pgaidentify", parameters=n)
|
return await self.send_command("pgaidentify", parameters=n)
|
||||||
|
|
||||||
async def devdetails(self) -> dict:
|
async def devdetails(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'devdetails' command.
|
||||||
|
|
||||||
|
Returns a dict containing all devices with their static details.
|
||||||
|
"""
|
||||||
return await self.send_command("devdetails")
|
return await self.send_command("devdetails")
|
||||||
|
|
||||||
async def restart(self) -> dict:
|
async def restart(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'restart' command.
|
||||||
|
|
||||||
|
Returns a single "RESTART" before CGMiner restarts.
|
||||||
|
"""
|
||||||
return await self.send_command("restart")
|
return await self.send_command("restart")
|
||||||
|
|
||||||
async def stats(self) -> dict:
|
async def stats(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'stats' command.
|
||||||
|
|
||||||
|
Returns a dict containing stats for all device/pool with more than 1 getwork.
|
||||||
|
"""
|
||||||
return await self.send_command("stats")
|
return await self.send_command("stats")
|
||||||
|
|
||||||
async def estats(self, old: bool = False) -> dict:
|
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,
|
||||||
|
ignoring zombie devices.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
old (optional): include zombie devices that became zombies less than 'old' seconds ago.
|
||||||
|
"""
|
||||||
if old:
|
if old:
|
||||||
return await self.send_command("estats", parameters="old")
|
return await self.send_command("estats", parameters="old")
|
||||||
else:
|
else:
|
||||||
return await self.send_command("estats")
|
return await self.send_command("estats")
|
||||||
|
|
||||||
async def check(self, command) -> dict:
|
async def check(self, command) -> dict:
|
||||||
|
"""
|
||||||
|
API 'check' command.
|
||||||
|
|
||||||
|
Returns 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)
|
return await self.send_command("check", parameters=command)
|
||||||
|
|
||||||
async def failover_only(self, failover: bool) -> dict:
|
async def failover_only(self, failover: bool) -> dict:
|
||||||
|
"""
|
||||||
|
API 'failover-only' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with what failover-only was set to.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
failover: what to set failover-only to.
|
||||||
|
"""
|
||||||
return await self.send_command("failover-only", parameters=failover)
|
return await self.send_command("failover-only", parameters=failover)
|
||||||
|
|
||||||
async def coin(self) -> dict:
|
async def coin(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'coin' command.
|
||||||
|
|
||||||
|
Returns information about the current coin being mined:
|
||||||
|
Hash Method <- the hashing algorithm
|
||||||
|
Current Block Time <- blocktime as a float, 0 means none
|
||||||
|
Current Block Hash <- the hash of the current block, blank means none
|
||||||
|
LP <- whether LP is in use on at least 1 pool
|
||||||
|
Network Difficulty: the current network difficulty
|
||||||
|
"""
|
||||||
return await self.send_command("coin")
|
return await self.send_command("coin")
|
||||||
|
|
||||||
async def debug(self, setting: str) -> dict:
|
async def debug(self, setting: str) -> dict:
|
||||||
|
"""
|
||||||
|
API 'debug' command.
|
||||||
|
|
||||||
|
Returns which debug setting was enabled or disabled.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
setting: which setting to switch to. Options are:
|
||||||
|
Silent,
|
||||||
|
Quiet,
|
||||||
|
Verbose,
|
||||||
|
Debug,
|
||||||
|
RPCProto,
|
||||||
|
PerDevice,
|
||||||
|
WorkTime,
|
||||||
|
Normal.
|
||||||
|
"""
|
||||||
return await self.send_command("debug", parameters=setting)
|
return await self.send_command("debug", parameters=setting)
|
||||||
|
|
||||||
async def setconfig(self, name: str, n: int) -> dict:
|
async def setconfig(self, name: str, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'setconfig' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of setting 'name' to N.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name: name of the config setting to set. Options are:
|
||||||
|
queue,
|
||||||
|
scantime,
|
||||||
|
expiry.
|
||||||
|
n: the value to set the 'name' setting to.
|
||||||
|
"""
|
||||||
return await self.send_command("setconfig", parameters=f"{name}, {n}")
|
return await self.send_command("setconfig", parameters=f"{name}, {n}")
|
||||||
|
|
||||||
async def usbstats(self) -> dict:
|
async def usbstats(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'usbstats' command.
|
||||||
|
|
||||||
|
Returns a dict containing the stats of all USB devices except ztex.
|
||||||
|
"""
|
||||||
return await self.send_command("usbstats")
|
return await self.send_command("usbstats")
|
||||||
|
|
||||||
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
|
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.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
MMQ -
|
||||||
|
opt: clock
|
||||||
|
val: 160 - 230 (multiple of 2)
|
||||||
|
CMR -
|
||||||
|
opt: clock
|
||||||
|
val: 100 - 220
|
||||||
|
"""
|
||||||
if val:
|
if val:
|
||||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}, {val}")
|
return await self.send_command("pgaset", parameters=f"{n}, {opt}, {val}")
|
||||||
else:
|
else:
|
||||||
return await self.send_command("pgaset", parameters=f"{n}, {opt}")
|
return await self.send_command("pgaset", parameters=f"{n}, {opt}")
|
||||||
|
|
||||||
async def zero(self, which: str, value: bool) -> dict:
|
async def zero(self, which: str, summary: bool) -> dict:
|
||||||
return await self.send_command("zero", parameters=f"{which}, {value}")
|
"""
|
||||||
|
API 'zero' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with info on the zero and optional 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 await self.send_command("zero", parameters=f"{which}, {summary}")
|
||||||
|
|
||||||
async def hotplug(self, n: int) -> dict:
|
async def hotplug(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'hotplug' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with whether or not hotplug was enabled.
|
||||||
|
"""
|
||||||
return await self.send_command("hotplug", parameters=n)
|
return await self.send_command("hotplug", parameters=n)
|
||||||
|
|
||||||
async def asc(self, n: int) -> dict:
|
async def asc(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'asc' command.
|
||||||
|
|
||||||
|
Returns a dict containing the details of a single ASC of number N.
|
||||||
|
|
||||||
|
n: the ASC device to get details of.
|
||||||
|
"""
|
||||||
return await self.send_command("asc", parameters=n)
|
return await self.send_command("asc", parameters=n)
|
||||||
|
|
||||||
async def ascenable(self, n: int) -> dict:
|
async def ascenable(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'ascenable' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of enabling the ASC device N.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the ASC to enable.
|
||||||
|
"""
|
||||||
return await self.send_command("ascenable", parameters=n)
|
return await self.send_command("ascenable", parameters=n)
|
||||||
|
|
||||||
async def ascdisable(self, n: int) -> dict:
|
async def ascdisable(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'ascdisable' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of disabling the ASC device N.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the ASC to disable.
|
||||||
|
"""
|
||||||
return await self.send_command("ascdisable", parameters=n)
|
return await self.send_command("ascdisable", parameters=n)
|
||||||
|
|
||||||
async def ascidentify(self, n: int) -> dict:
|
async def ascidentify(self, n: int) -> dict:
|
||||||
|
"""
|
||||||
|
API 'ascidentify' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the results of identifying the ASC device N.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
n: the number of the PGA to identify.
|
||||||
|
"""
|
||||||
return await self.send_command("ascidentify", parameters=n)
|
return await self.send_command("ascidentify", parameters=n)
|
||||||
|
|
||||||
async def asccount(self) -> dict:
|
async def asccount(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'asccount' command.
|
||||||
|
|
||||||
|
Returns a dict containing the number of ASC devices.
|
||||||
|
"""
|
||||||
return await self.send_command("asccount")
|
return await self.send_command("asccount")
|
||||||
|
|
||||||
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
|
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
|
||||||
|
"""
|
||||||
|
API 'ascset' command.
|
||||||
|
|
||||||
|
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:
|
||||||
|
AVA+BTB -
|
||||||
|
opt: freq
|
||||||
|
val: 256 - 1024 (chip frequency)
|
||||||
|
BTB -
|
||||||
|
opt: millivolts
|
||||||
|
val: 1000 - 1400 (core voltage)
|
||||||
|
MBA -
|
||||||
|
opt: reset
|
||||||
|
val: 0 - # of chips (reset a chip)
|
||||||
|
|
||||||
|
opt: freq
|
||||||
|
val: 0 - # of chips, 100 - 1400 (chip frequency)
|
||||||
|
|
||||||
|
opt: ledcount
|
||||||
|
val: 0 - 100 (chip count for LED)
|
||||||
|
|
||||||
|
opt: ledlimit
|
||||||
|
val: 0 - 200 (LED off below GH/s)
|
||||||
|
|
||||||
|
opt: spidelay
|
||||||
|
val: 0 - 9999 (SPI per I/O delay)
|
||||||
|
|
||||||
|
opt: spireset
|
||||||
|
val: i or s, 0 - 9999 (SPI regular reset)
|
||||||
|
|
||||||
|
opt: spisleep
|
||||||
|
val: 0 - 9999 (SPI reset sleep in ms)
|
||||||
|
BMA -
|
||||||
|
opt: volt
|
||||||
|
val: 0 - 9
|
||||||
|
|
||||||
|
opt: clock
|
||||||
|
val: 0 - 15
|
||||||
|
"""
|
||||||
if val:
|
if val:
|
||||||
return await self.send_command("ascset", parameters=f"{n}, {opt}, {val}")
|
return await self.send_command("ascset", parameters=f"{n}, {opt}, {val}")
|
||||||
else:
|
else:
|
||||||
return await self.send_command("ascset", parameters=f"{n}, {opt}")
|
return await self.send_command("ascset", parameters=f"{n}, {opt}")
|
||||||
|
|
||||||
async def lcd(self) -> dict:
|
async def lcd(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'lcd' command.
|
||||||
|
|
||||||
|
Returns a dict containing an all in one status summary of the miner.
|
||||||
|
"""
|
||||||
return await self.send_command("lcd")
|
return await self.send_command("lcd")
|
||||||
|
|
||||||
async def lockstats(self) -> dict:
|
async def lockstats(self) -> dict:
|
||||||
|
"""
|
||||||
|
API 'lockstats' command.
|
||||||
|
|
||||||
|
Returns the STATUS section with the result of writing the lock stats to STDERR.
|
||||||
|
"""
|
||||||
return await self.send_command("lockstats")
|
return await self.send_command("lockstats")
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -21,6 +21,21 @@ if __name__ == '__main__':
|
|||||||
2. Navigate to this directory, and run ```make_cfg_tool_exe.py build``` on Windows or ```python3 make_cfg_tool_exe.py``` on Mac or UNIX.
|
2. Navigate to this directory, and run ```make_cfg_tool_exe.py build``` on Windows or ```python3 make_cfg_tool_exe.py``` on Mac or UNIX.
|
||||||
|
|
||||||
### Interfacing with miners programmatically
|
### Interfacing with miners programmatically
|
||||||
|
<br>
|
||||||
|
|
||||||
|
##### Note: If you are trying to interface with Whatsminers, there is a bug in the way they are interacted with on Windows, so to fix that you need to change the event loop policy using this code:
|
||||||
|
```python
|
||||||
|
# need to import these 2 libraries, you need asyncio anyway so make sure you have sys imported
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# if the computer is windows, set the event loop policy to a WindowsSelector policy
|
||||||
|
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
|
```
|
||||||
|
|
||||||
|
##### It is likely a good idea to use this code in your program anyway to be preventative.
|
||||||
|
<br>
|
||||||
|
|
||||||
To write your own custom programs with this repo, you have many options.
|
To write your own custom programs with this repo, you have many options.
|
||||||
|
|
||||||
@@ -31,7 +46,7 @@ A basic script to find all miners on the network and get the hashrate from them
|
|||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
from network import MinerNetwork
|
from network import MinerNetwork
|
||||||
from cfg_util.func import safe_parse_api_data
|
from cfg_util.func.parse_data import safe_parse_api_data
|
||||||
|
|
||||||
async def get_hashrate():
|
async def get_hashrate():
|
||||||
# Miner Network class allows for easy scanning of a network
|
# Miner Network class allows for easy scanning of a network
|
||||||
@@ -65,7 +80,7 @@ You can also create your own miner without scanning if you know the IP:
|
|||||||
import asyncio
|
import asyncio
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from miners.miner_factory import MinerFactory
|
from miners.miner_factory import MinerFactory
|
||||||
from cfg_util.func import safe_parse_api_data
|
from cfg_util.func.parse_data import safe_parse_api_data
|
||||||
|
|
||||||
async def get_miner_hashrate(ip: str):
|
async def get_miner_hashrate(ip: str):
|
||||||
# Instantiate a Miner Factory to generate miners from their IP
|
# Instantiate a Miner Factory to generate miners from their IP
|
||||||
@@ -91,7 +106,7 @@ Or generate a miner directly without the factory:
|
|||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
from miners.bosminer import BOSminer
|
from miners.bosminer import BOSminer
|
||||||
from cfg_util.func import safe_parse_api_data
|
from cfg_util.func.parse_data import safe_parse_api_data
|
||||||
|
|
||||||
async def get_miner_hashrate(ip: str):
|
async def get_miner_hashrate(ip: str):
|
||||||
# Create a BOSminer miner object
|
# Create a BOSminer miner object
|
||||||
@@ -113,7 +128,7 @@ Or finally, just get the API directly:
|
|||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
from API.bosminer import BOSMinerAPI
|
from API.bosminer import BOSMinerAPI
|
||||||
from cfg_util.func import safe_parse_api_data
|
from cfg_util.func.parse_data import safe_parse_api_data
|
||||||
|
|
||||||
async def get_miner_hashrate(ip: str):
|
async def get_miner_hashrate(ip: str):
|
||||||
# Create a BOSminerAPI object
|
# Create a BOSminerAPI object
|
||||||
|
|||||||
@@ -1,323 +0,0 @@
|
|||||||
import ipaddress
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from operator import itemgetter
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import aiofiles
|
|
||||||
import toml
|
|
||||||
|
|
||||||
from cfg_util.miner_factory import miner_factory
|
|
||||||
from cfg_util.layout import window
|
|
||||||
from cfg_util.func.data import safe_parse_api_data
|
|
||||||
|
|
||||||
from config.bos import bos_config_convert, general_config_convert_bos
|
|
||||||
|
|
||||||
from API import APIError
|
|
||||||
|
|
||||||
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
|
|
||||||
|
|
||||||
|
|
||||||
async def update_ui_with_data(key, message, append=False):
|
|
||||||
if append:
|
|
||||||
message = window[key].get_text() + message
|
|
||||||
window[key].update(message)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_prog_bar(amount):
|
|
||||||
window["progress"].Update(amount)
|
|
||||||
percent_done = 100 * (amount / window['progress'].maxlen)
|
|
||||||
window["progress_percent"].Update(f"{round(percent_done, 2)} %")
|
|
||||||
if percent_done == 100:
|
|
||||||
window["progress_percent"].Update("")
|
|
||||||
|
|
||||||
|
|
||||||
async def set_progress_bar_len(amount):
|
|
||||||
window["progress"].Update(0, max=amount)
|
|
||||||
window["progress"].maxlen = amount
|
|
||||||
|
|
||||||
|
|
||||||
async def scan_network(network):
|
|
||||||
await update_ui_with_data("status", "Scanning")
|
|
||||||
network_size = len(network)
|
|
||||||
miner_generator = network.scan_network_generator()
|
|
||||||
await set_progress_bar_len(2 * network_size)
|
|
||||||
progress_bar_len = 0
|
|
||||||
miners = []
|
|
||||||
async for miner in miner_generator:
|
|
||||||
if miner:
|
|
||||||
miners.append(miner)
|
|
||||||
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)
|
|
||||||
progress_bar_len += 1
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
all_miners.sort(key=lambda x: x.ip)
|
|
||||||
window["ip_list"].update([str(miner.ip) for miner in all_miners])
|
|
||||||
await update_ui_with_data("ip_count", str(len(all_miners)))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def miner_light(ips: list):
|
|
||||||
await asyncio.gather(*[flip_light(ip) for ip in ips])
|
|
||||||
|
|
||||||
|
|
||||||
async def flip_light(ip):
|
|
||||||
listbox = window['ip_list'].Widget
|
|
||||||
miner = await miner_factory.get_miner(ip)
|
|
||||||
if ip in window["ip_list"].Values:
|
|
||||||
index = window["ip_list"].Values.index(ip)
|
|
||||||
if listbox.itemcget(index, "background") == 'red':
|
|
||||||
listbox.itemconfigure(index, bg='#f0f3f7', fg='#000000')
|
|
||||||
await miner.fault_light_off()
|
|
||||||
else:
|
|
||||||
listbox.itemconfigure(index, bg='red', fg='white')
|
|
||||||
await miner.fault_light_on()
|
|
||||||
|
|
||||||
|
|
||||||
async def import_config(ip):
|
|
||||||
await update_ui_with_data("status", "Importing")
|
|
||||||
miner = await miner_factory.get_miner(ipaddress.ip_address(*ip))
|
|
||||||
await miner.get_config()
|
|
||||||
config = miner.config
|
|
||||||
await update_ui_with_data("config", str(config))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def import_iplist(file_location):
|
|
||||||
await update_ui_with_data("status", "Importing")
|
|
||||||
if not os.path.exists(file_location):
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
ip_list = []
|
|
||||||
async with aiofiles.open(file_location, mode='r') as file:
|
|
||||||
async for line in file:
|
|
||||||
ips = [x.group() for x in re.finditer(
|
|
||||||
"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", line)]
|
|
||||||
for ip in ips:
|
|
||||||
if ip not in ip_list:
|
|
||||||
ip_list.append(ipaddress.ip_address(ip))
|
|
||||||
ip_list.sort()
|
|
||||||
window["ip_list"].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_list'].Values:
|
|
||||||
await file.write(str(item) + "\n")
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def send_config_generator(miners: list, config):
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
config_tasks = []
|
|
||||||
for miner in miners:
|
|
||||||
if len(config_tasks) >= CONFIG_THREADS:
|
|
||||||
configured = asyncio.as_completed(config_tasks)
|
|
||||||
config_tasks = []
|
|
||||||
for sent_config in configured:
|
|
||||||
yield await sent_config
|
|
||||||
config_tasks.append(loop.create_task(miner.send_config(config)))
|
|
||||||
configured = asyncio.as_completed(config_tasks)
|
|
||||||
for sent_config in configured:
|
|
||||||
yield await sent_config
|
|
||||||
|
|
||||||
|
|
||||||
async def send_config(ips: list, config):
|
|
||||||
await update_ui_with_data("status", "Configuring")
|
|
||||||
await set_progress_bar_len(2 * len(ips))
|
|
||||||
progress_bar_len = 0
|
|
||||||
get_miner_genenerator = miner_factory.get_miner_generator(ips)
|
|
||||||
all_miners = []
|
|
||||||
async for miner in get_miner_genenerator:
|
|
||||||
all_miners.append(miner)
|
|
||||||
progress_bar_len += 1
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
|
|
||||||
config_sender_generator = send_config_generator(all_miners, config)
|
|
||||||
async for _config_sender in config_sender_generator:
|
|
||||||
progress_bar_len += 1
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def import_config_file(file_location):
|
|
||||||
await update_ui_with_data("status", "Importing")
|
|
||||||
if not os.path.exists(file_location):
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
async with aiofiles.open(file_location, mode='r') as file:
|
|
||||||
config = await file.read()
|
|
||||||
await update_ui_with_data("config", await bos_config_convert(toml.loads(config)))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def export_config_file(file_location, config):
|
|
||||||
await update_ui_with_data("status", "Exporting")
|
|
||||||
config = toml.loads(config)
|
|
||||||
config['format']['generator'] = 'upstream_config_util'
|
|
||||||
config['format']['timestamp'] = int(time.time())
|
|
||||||
config = toml.dumps(config)
|
|
||||||
async with aiofiles.open(file_location, mode='w+') as file:
|
|
||||||
await file.write(await general_config_convert_bos(config))
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def get_data(ip_list: list):
|
|
||||||
await update_ui_with_data("status", "Getting Data")
|
|
||||||
ips = [ipaddress.ip_address(ip) for ip in ip_list]
|
|
||||||
await set_progress_bar_len(len(ips))
|
|
||||||
progress_bar_len = 0
|
|
||||||
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in ips])
|
|
||||||
miner_data = []
|
|
||||||
for all_data in data_gen:
|
|
||||||
miner_data.append(await all_data)
|
|
||||||
progress_bar_len += 1
|
|
||||||
asyncio.create_task(update_prog_bar(progress_bar_len))
|
|
||||||
|
|
||||||
miner_data.sort(key=lambda x: ipaddress.ip_address(x['IP']))
|
|
||||||
|
|
||||||
total_hr = round(sum(d.get('TH/s', 0) for d in miner_data), 2)
|
|
||||||
window["hr_total"].update(f"{total_hr} TH/s")
|
|
||||||
window["hr_list"].update(disabled=False)
|
|
||||||
window["hr_list"].update([item['IP'] + " | "
|
|
||||||
+ item['host'] + " | "
|
|
||||||
+ str(item['TH/s']) + " TH/s | "
|
|
||||||
+ item['user'] + " | "
|
|
||||||
+ str(item['wattage']) + " W"
|
|
||||||
for item in miner_data])
|
|
||||||
window["hr_list"].update(disabled=True)
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|
||||||
|
|
||||||
async def get_formatted_data(ip: ipaddress.ip_address):
|
|
||||||
miner = await miner_factory.get_miner(ip)
|
|
||||||
try:
|
|
||||||
miner_data = await miner.api.multicommand("summary", "pools", "tunerstatus")
|
|
||||||
except APIError:
|
|
||||||
return {'TH/s': "Unknown", 'IP': str(miner.ip), 'host': "Unknown", 'user': "Unknown", 'wattage': 0}
|
|
||||||
host = await miner.get_hostname()
|
|
||||||
if "tunerstatus" in miner_data.keys():
|
|
||||||
wattage = await safe_parse_api_data(miner_data, "tunerstatus", 0, 'TUNERSTATUS', 0, "PowerLimit")
|
|
||||||
# data['tunerstatus'][0]['TUNERSTATUS'][0]['PowerLimit']
|
|
||||||
else:
|
|
||||||
wattage = 0
|
|
||||||
if "summary" in miner_data.keys():
|
|
||||||
if 'MHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
|
||||||
th5s = round(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'MHS 5s') / 1000000, 2)
|
|
||||||
elif 'GHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
|
||||||
if not miner_data['summary'][0]['SUMMARY'][0]['GHS 5s'] == "":
|
|
||||||
th5s = round(float(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'GHS 5s')) / 1000, 2)
|
|
||||||
else:
|
|
||||||
th5s = 0
|
|
||||||
else:
|
|
||||||
th5s = 0
|
|
||||||
else:
|
|
||||||
th5s = 0
|
|
||||||
if "pools" not in miner_data.keys():
|
|
||||||
user = "?"
|
|
||||||
elif not miner_data['pools'][0]['POOLS'] == []:
|
|
||||||
user = await safe_parse_api_data(miner_data, 'pools', 0, 'POOLS', 0, 'User')
|
|
||||||
else:
|
|
||||||
user = "Blank"
|
|
||||||
return {'TH/s': th5s, 'IP': str(miner.ip), 'host': host, 'user': user, 'wattage': wattage}
|
|
||||||
|
|
||||||
|
|
||||||
async def generate_config():
|
|
||||||
config = {'group': [{
|
|
||||||
'name': 'group',
|
|
||||||
'quota': 1,
|
|
||||||
'pool': [{
|
|
||||||
'url': 'stratum2+tcp://us-east.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt',
|
|
||||||
'user': 'UpstreamDataInc.test',
|
|
||||||
'password': '123'
|
|
||||||
}, {
|
|
||||||
'url': 'stratum2+tcp://stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt',
|
|
||||||
'user': 'UpstreamDataInc.test',
|
|
||||||
'password': '123'
|
|
||||||
}, {
|
|
||||||
'url': 'stratum+tcp://stratum.slushpool.com:3333',
|
|
||||||
'user': 'UpstreamDataInc.test',
|
|
||||||
'password': '123'
|
|
||||||
}]
|
|
||||||
}],
|
|
||||||
'format': {
|
|
||||||
'version': '1.2+',
|
|
||||||
'model': 'Antminer S9',
|
|
||||||
'generator': 'upstream_config_util',
|
|
||||||
'timestamp': int(time.time())
|
|
||||||
},
|
|
||||||
'temp_control': {
|
|
||||||
'target_temp': 80.0,
|
|
||||||
'hot_temp': 90.0,
|
|
||||||
'dangerous_temp': 120.0
|
|
||||||
},
|
|
||||||
'autotuning': {
|
|
||||||
'enabled': True,
|
|
||||||
'psu_power_limit': 900
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window['config'].update(await bos_config_convert(config))
|
|
||||||
|
|
||||||
|
|
||||||
async def sort_data(index: int or str):
|
|
||||||
await update_ui_with_data("status", "Sorting Data")
|
|
||||||
data_list = window['hr_list'].Values
|
|
||||||
new_list = []
|
|
||||||
indexes = {}
|
|
||||||
for item in data_list:
|
|
||||||
item_data = [part.strip() for part in item.split("|")]
|
|
||||||
for idx, part in enumerate(item_data):
|
|
||||||
if re.match("[0-9]* W", part):
|
|
||||||
item_data[idx] = item_data[idx].replace(" W", "")
|
|
||||||
indexes['wattage'] = idx
|
|
||||||
elif re.match("[0-9]*\.?[0-9]* TH\/s", part):
|
|
||||||
item_data[idx] = item_data[idx].replace(" TH/s", "")
|
|
||||||
indexes['hr'] = idx
|
|
||||||
elif re.match("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", part):
|
|
||||||
item_data[idx] = ipaddress.ip_address(item_data[idx])
|
|
||||||
indexes['ip'] = idx
|
|
||||||
new_list.append(item_data)
|
|
||||||
if not isinstance(index, str):
|
|
||||||
if index == indexes['hr']:
|
|
||||||
new_data_list = sorted(new_list, key=lambda x: float(x[index]))
|
|
||||||
else:
|
|
||||||
new_data_list = sorted(new_list, key=itemgetter(index))
|
|
||||||
else:
|
|
||||||
if index.lower() not in indexes.keys():
|
|
||||||
return
|
|
||||||
elif index.lower() == 'hr':
|
|
||||||
new_data_list = sorted(new_list, key=lambda x: float(x[indexes[index]]))
|
|
||||||
else:
|
|
||||||
new_data_list = sorted(new_list, key=itemgetter(indexes[index]))
|
|
||||||
new_ip_list = []
|
|
||||||
for item in new_data_list:
|
|
||||||
new_ip_list.append(item[indexes['ip']])
|
|
||||||
new_data_list = [str(item[indexes['ip']]) + " | "
|
|
||||||
+ item[1] + " | "
|
|
||||||
+ item[indexes['hr']] + " TH/s | "
|
|
||||||
+ item[3] + " | "
|
|
||||||
+ str(item[indexes['wattage']]) + " W"
|
|
||||||
for item in new_data_list]
|
|
||||||
window["hr_list"].update(disabled=False)
|
|
||||||
window["hr_list"].update(new_data_list)
|
|
||||||
window['ip_list'].update(new_ip_list)
|
|
||||||
window["hr_list"].update(disabled=True)
|
|
||||||
await update_ui_with_data("status", "")
|
|
||||||
|
|||||||
68
cfg_util/func/files.py
Normal file
68
cfg_util/func/files.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import ipaddress
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import toml
|
||||||
|
|
||||||
|
from cfg_util.func.ui import update_ui_with_data
|
||||||
|
from cfg_util.layout import window
|
||||||
|
from config.bos import bos_config_convert, general_config_convert_bos
|
||||||
|
|
||||||
|
|
||||||
|
async def import_iplist(file_location):
|
||||||
|
await update_ui_with_data("status", "Importing")
|
||||||
|
if not os.path.exists(file_location):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
ip_list = []
|
||||||
|
async with aiofiles.open(file_location, mode='r') as file:
|
||||||
|
async for line in file:
|
||||||
|
ips = [x.group() for x in re.finditer(
|
||||||
|
"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", line)]
|
||||||
|
for ip in ips:
|
||||||
|
if ip not in ip_list:
|
||||||
|
ip_list.append(ipaddress.ip_address(ip))
|
||||||
|
ip_list.sort()
|
||||||
|
window["ip_table"].update([[str(ip), "", "", "", ""] for ip in ip_list])
|
||||||
|
await update_ui_with_data("ip_count", str(len(ip_list)))
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def export_iplist(file_location, ip_list_selected):
|
||||||
|
await update_ui_with_data("status", "Exporting")
|
||||||
|
if not os.path.exists(file_location):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if ip_list_selected is not None and not ip_list_selected == []:
|
||||||
|
async with aiofiles.open(file_location, mode='w') as file:
|
||||||
|
for item in ip_list_selected:
|
||||||
|
await file.write(str(item) + "\n")
|
||||||
|
else:
|
||||||
|
async with aiofiles.open(file_location, mode='w') as file:
|
||||||
|
for item in window['ip_table'].Values:
|
||||||
|
await file.write(str(item[0]) + "\n")
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def import_config_file(file_location):
|
||||||
|
await update_ui_with_data("status", "Importing")
|
||||||
|
if not os.path.exists(file_location):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
async with aiofiles.open(file_location, mode='r') as file:
|
||||||
|
config = await file.read()
|
||||||
|
await update_ui_with_data("config", await bos_config_convert(toml.loads(config)))
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def export_config_file(file_location, config):
|
||||||
|
await update_ui_with_data("status", "Exporting")
|
||||||
|
config = toml.loads(config)
|
||||||
|
config['format']['generator'] = 'upstream_config_util'
|
||||||
|
config['format']['timestamp'] = int(time.time())
|
||||||
|
config = toml.dumps(config)
|
||||||
|
async with aiofiles.open(file_location, mode='w+') as file:
|
||||||
|
await file.write(await general_config_convert_bos(config))
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
301
cfg_util/func/miners.py
Normal file
301
cfg_util/func/miners.py
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import asyncio
|
||||||
|
import ipaddress
|
||||||
|
import time
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from API import APIError, APIWarning
|
||||||
|
from cfg_util.func.parse_data import safe_parse_api_data
|
||||||
|
from cfg_util.func.ui import update_ui_with_data, update_prog_bar, set_progress_bar_len
|
||||||
|
from cfg_util.layout import window
|
||||||
|
from cfg_util.miner_factory import miner_factory
|
||||||
|
from config.bos import bos_config_convert
|
||||||
|
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
|
||||||
|
|
||||||
|
|
||||||
|
async def import_config(idx):
|
||||||
|
await update_ui_with_data("status", "Importing")
|
||||||
|
miner = await miner_factory.get_miner(ipaddress.ip_address(window["ip_table"].Values[idx[0]][0]))
|
||||||
|
await miner.get_config()
|
||||||
|
config = miner.config
|
||||||
|
await update_ui_with_data("config", str(config))
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def scan_network(network):
|
||||||
|
await update_ui_with_data("status", "Scanning")
|
||||||
|
await update_ui_with_data("ip_count", "")
|
||||||
|
await update_ui_with_data("hr_total", "")
|
||||||
|
window["ip_table"].update([])
|
||||||
|
network_size = len(network)
|
||||||
|
miner_generator = network.scan_network_generator()
|
||||||
|
await set_progress_bar_len(2 * network_size)
|
||||||
|
progress_bar_len = 0
|
||||||
|
miners = []
|
||||||
|
async for miner in miner_generator:
|
||||||
|
if miner:
|
||||||
|
miners.append(miner)
|
||||||
|
# can output "Identifying" for each found item, but it gets a bit cluttered
|
||||||
|
# and could possibly be confusing for the end user because of timing on
|
||||||
|
# adding the IPs
|
||||||
|
# window["ip_table"].update([["Identifying...", "", "", "", ""] for miner in miners])
|
||||||
|
progress_bar_len += 1
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
progress_bar_len += network_size - len(miners)
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
get_miner_genenerator = miner_factory.get_miner_generator(miners)
|
||||||
|
all_miners = []
|
||||||
|
async for found_miner in get_miner_genenerator:
|
||||||
|
all_miners.append(found_miner)
|
||||||
|
all_miners.sort(key=lambda x: x.ip)
|
||||||
|
window["ip_table"].update([[str(miner.ip)] for miner in all_miners])
|
||||||
|
progress_bar_len += 1
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
await update_ui_with_data("ip_count", str(len(all_miners)))
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def miner_light(ips: list):
|
||||||
|
await asyncio.gather(*[flip_light(ip) for ip in ips])
|
||||||
|
|
||||||
|
|
||||||
|
async def flip_light(ip):
|
||||||
|
ip_list = window['ip_table'].Widget
|
||||||
|
miner = await miner_factory.get_miner(ip)
|
||||||
|
index = [item[0] for item in window["ip_table"].Values].index(ip)
|
||||||
|
index_tags = ip_list.item(index)['tags']
|
||||||
|
if "light" not in index_tags:
|
||||||
|
ip_list.item(index, tags=([*index_tags, "light"]))
|
||||||
|
window['ip_table'].update(row_colors=[(index, "white", "red")])
|
||||||
|
await miner.fault_light_on()
|
||||||
|
else:
|
||||||
|
index_tags.remove("light")
|
||||||
|
ip_list.item(index, tags=index_tags)
|
||||||
|
window['ip_table'].update(row_colors=[(index, "black", "white")])
|
||||||
|
await miner.fault_light_off()
|
||||||
|
|
||||||
|
|
||||||
|
async def send_config_generator(miners: list, config):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
config_tasks = []
|
||||||
|
for miner in miners:
|
||||||
|
if len(config_tasks) >= CONFIG_THREADS:
|
||||||
|
configured = asyncio.as_completed(config_tasks)
|
||||||
|
config_tasks = []
|
||||||
|
for sent_config in configured:
|
||||||
|
yield await sent_config
|
||||||
|
config_tasks.append(loop.create_task(miner.send_config(config)))
|
||||||
|
configured = asyncio.as_completed(config_tasks)
|
||||||
|
for sent_config in configured:
|
||||||
|
yield await sent_config
|
||||||
|
|
||||||
|
|
||||||
|
async def send_config(ips: list, config):
|
||||||
|
await update_ui_with_data("status", "Configuring")
|
||||||
|
await set_progress_bar_len(2 * len(ips))
|
||||||
|
progress_bar_len = 0
|
||||||
|
get_miner_genenerator = miner_factory.get_miner_generator(ips)
|
||||||
|
all_miners = []
|
||||||
|
async for miner in get_miner_genenerator:
|
||||||
|
all_miners.append(miner)
|
||||||
|
progress_bar_len += 1
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
|
||||||
|
config_sender_generator = send_config_generator(all_miners, config)
|
||||||
|
async for _config_sender in config_sender_generator:
|
||||||
|
progress_bar_len += 1
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_data(ip_list: list):
|
||||||
|
await update_ui_with_data("status", "Getting Data")
|
||||||
|
ips = [ipaddress.ip_address(ip) for ip in ip_list]
|
||||||
|
if len(ips) == 0:
|
||||||
|
ips = [ipaddress.ip_address(ip) for ip in [item[0] for item in window["ip_table"].Values]]
|
||||||
|
await set_progress_bar_len(len(ips))
|
||||||
|
progress_bar_len = 0
|
||||||
|
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in ips])
|
||||||
|
ip_table_data = window["ip_table"].Values
|
||||||
|
ordered_all_ips = [item[0] for item in ip_table_data]
|
||||||
|
for all_data in data_gen:
|
||||||
|
data_point = await all_data
|
||||||
|
if data_point["IP"] in ordered_all_ips:
|
||||||
|
ip_table_index = ordered_all_ips.index(data_point["IP"])
|
||||||
|
ip_table_data[ip_table_index] = [
|
||||||
|
data_point["IP"], data_point["model"], data_point["host"], str(data_point['TH/s']) + " TH/s", data_point["temp"],
|
||||||
|
data_point['user'], str(data_point['wattage']) + " W"
|
||||||
|
]
|
||||||
|
window["ip_table"].update(ip_table_data)
|
||||||
|
progress_bar_len += 1
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
|
||||||
|
hashrate_list = [float(item[3].replace(" TH/s", "")) for item in window["ip_table"].Values if not item[3] == '']
|
||||||
|
total_hr = round(sum(hashrate_list), 2)
|
||||||
|
window["hr_total"].update(f"{total_hr} TH/s")
|
||||||
|
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def scan_and_get_data(network):
|
||||||
|
await update_ui_with_data("status", "Scanning")
|
||||||
|
network_size = len(network)
|
||||||
|
miner_generator = network.scan_network_generator()
|
||||||
|
await set_progress_bar_len(3 * network_size)
|
||||||
|
progress_bar_len = 0
|
||||||
|
miners = []
|
||||||
|
async for miner in miner_generator:
|
||||||
|
if miner:
|
||||||
|
miners.append(miner)
|
||||||
|
# can output "Identifying" for each found item, but it gets a bit cluttered
|
||||||
|
# and could possibly be confusing for the end user because of timing on
|
||||||
|
# adding the IPs
|
||||||
|
# window["ip_table"].update([["Identifying..."] for miner in miners])
|
||||||
|
progress_bar_len += 1
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
progress_bar_len += network_size - len(miners)
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
get_miner_genenerator = miner_factory.get_miner_generator(miners)
|
||||||
|
all_miners = []
|
||||||
|
async for found_miner in get_miner_genenerator:
|
||||||
|
all_miners.append(found_miner)
|
||||||
|
all_miners.sort(key=lambda x: x.ip)
|
||||||
|
window["ip_table"].update([[str(miner.ip)] for miner in all_miners])
|
||||||
|
progress_bar_len += 1
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
await update_ui_with_data("ip_count", str(len(all_miners)))
|
||||||
|
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in miners])
|
||||||
|
ip_table_data = window["ip_table"].Values
|
||||||
|
ordered_all_ips = [item[0] for item in ip_table_data]
|
||||||
|
progress_bar_len += (network_size - len(miners))
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
await update_ui_with_data("status", "Getting Data")
|
||||||
|
for all_data in data_gen:
|
||||||
|
data_point = await all_data
|
||||||
|
if data_point["IP"] in ordered_all_ips:
|
||||||
|
ip_table_index = ordered_all_ips.index(data_point["IP"])
|
||||||
|
ip_table_data[ip_table_index] = [
|
||||||
|
data_point["IP"], data_point["model"], data_point["host"], str(data_point['TH/s']) + " TH/s", data_point["temp"],
|
||||||
|
data_point['user'], str(data_point['wattage']) + " W"
|
||||||
|
]
|
||||||
|
window["ip_table"].update(ip_table_data)
|
||||||
|
progress_bar_len += 1
|
||||||
|
asyncio.create_task(update_prog_bar(progress_bar_len))
|
||||||
|
hashrate_list = [float(item[3].replace(" TH/s", "")) for item in window["ip_table"].Values if not item[3] == '']
|
||||||
|
total_hr = round(sum(hashrate_list), 2)
|
||||||
|
await update_ui_with_data("hr_total", f"{total_hr} TH/s")
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_formatted_data(ip: ipaddress.ip_address):
|
||||||
|
miner = await miner_factory.get_miner(ip)
|
||||||
|
warnings.filterwarnings('ignore')
|
||||||
|
try:
|
||||||
|
miner_data = await miner.api.multicommand("summary", "devs", "temps", "tunerstatus", "pools", "stats")
|
||||||
|
except APIError:
|
||||||
|
return {'TH/s': "Unknown", 'IP': str(miner.ip), 'host': "Unknown", 'user': "Unknown", 'wattage': 0}
|
||||||
|
host = await miner.get_hostname()
|
||||||
|
model = await miner.get_model()
|
||||||
|
temps = 0
|
||||||
|
if "summary" in miner_data.keys():
|
||||||
|
if "Temperature" in miner_data['summary'][0]['SUMMARY'][0].keys():
|
||||||
|
if not round(miner_data['summary'][0]['SUMMARY'][0]["Temperature"]) == 0:
|
||||||
|
temps = miner_data['summary'][0]['SUMMARY'][0]["Temperature"]
|
||||||
|
if 'MHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
||||||
|
th5s = round(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'MHS 5s') / 1000000, 2)
|
||||||
|
elif 'GHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
|
||||||
|
if not miner_data['summary'][0]['SUMMARY'][0]['GHS 5s'] == "":
|
||||||
|
th5s = round(float(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'GHS 5s')) / 1000,
|
||||||
|
2)
|
||||||
|
else:
|
||||||
|
th5s = 0
|
||||||
|
else:
|
||||||
|
th5s = 0
|
||||||
|
else:
|
||||||
|
th5s = 0
|
||||||
|
if "temps" in miner_data.keys() and not miner_data["temps"][0]['TEMPS'] == []:
|
||||||
|
if "Chip" in miner_data["temps"][0]['TEMPS'][0].keys():
|
||||||
|
for board in miner_data["temps"][0]['TEMPS']:
|
||||||
|
if board["Chip"] is not None and not board["Chip"] == 0.0:
|
||||||
|
temps = board["Chip"]
|
||||||
|
if "devs" in miner_data.keys() and not miner_data["devs"][0]['DEVS'] == []:
|
||||||
|
if "Chip Temp Avg" in miner_data["devs"][0]['DEVS'][0].keys():
|
||||||
|
for board in miner_data["devs"][0]['DEVS']:
|
||||||
|
if board['Chip Temp Avg'] is not None and not board['Chip Temp Avg'] == 0.0:
|
||||||
|
temps = board['Chip Temp Avg']
|
||||||
|
|
||||||
|
if "stats" in miner_data.keys() and not miner_data["stats"][0]['STATS'] == []:
|
||||||
|
for temp in ["temp2", "temp1", "temp3"]:
|
||||||
|
if temp in miner_data["stats"][0]['STATS'][1].keys():
|
||||||
|
if miner_data["stats"][0]['STATS'][1][temp] is not None and not miner_data["stats"][0]['STATS'][1][
|
||||||
|
temp] == 0.0:
|
||||||
|
temps = miner_data["stats"][0]['STATS'][1][temp]
|
||||||
|
|
||||||
|
if "pools" not in miner_data.keys():
|
||||||
|
user = "?"
|
||||||
|
elif not miner_data['pools'][0]['POOLS'] == []:
|
||||||
|
user = await safe_parse_api_data(miner_data, 'pools', 0, 'POOLS', 0, 'User')
|
||||||
|
else:
|
||||||
|
user = "Blank"
|
||||||
|
|
||||||
|
if "tunerstatus" in miner_data.keys():
|
||||||
|
wattage = await safe_parse_api_data(miner_data, "tunerstatus", 0, 'TUNERSTATUS', 0, "PowerLimit")
|
||||||
|
elif "Power" in miner_data["summary"][0]["SUMMARY"][0].keys():
|
||||||
|
wattage = await safe_parse_api_data(miner_data, "summary", 0, 'SUMMARY', 0, "Power")
|
||||||
|
else:
|
||||||
|
wattage = 0
|
||||||
|
return {'TH/s': th5s, 'IP': str(miner.ip), 'model': model,
|
||||||
|
'temp': round(temps), 'host': host, 'user': user,
|
||||||
|
'wattage': wattage}
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_config(username, workername, v2_allowed):
|
||||||
|
if username and workername:
|
||||||
|
user = f"{username}.{workername}"
|
||||||
|
elif username and not workername:
|
||||||
|
user = username
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if v2_allowed:
|
||||||
|
url_1 = 'stratum2+tcp://v2.us-east.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt'
|
||||||
|
url_2 = 'stratum2+tcp://v2.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt'
|
||||||
|
url_3 = 'stratum+tcp://stratum.slushpool.com:3333'
|
||||||
|
else:
|
||||||
|
url_1 = 'stratum+tcp://ca.stratum.slushpool.com:3333'
|
||||||
|
url_2 = 'stratum+tcp://us-east.stratum.slushpool.com:3333'
|
||||||
|
url_3 = 'stratum+tcp://stratum.slushpool.com:3333'
|
||||||
|
|
||||||
|
config = {'group': [{
|
||||||
|
'name': 'group',
|
||||||
|
'quota': 1,
|
||||||
|
'pool': [{
|
||||||
|
'url': url_1,
|
||||||
|
'user': user,
|
||||||
|
'password': '123'
|
||||||
|
}, {
|
||||||
|
'url': url_2,
|
||||||
|
'user': user,
|
||||||
|
'password': '123'
|
||||||
|
}, {
|
||||||
|
'url': url_3,
|
||||||
|
'user': user,
|
||||||
|
'password': '123'
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
'format': {
|
||||||
|
'version': '1.2+',
|
||||||
|
'model': 'Antminer S9',
|
||||||
|
'generator': 'upstream_config_util',
|
||||||
|
'timestamp': int(time.time())
|
||||||
|
},
|
||||||
|
'temp_control': {
|
||||||
|
'target_temp': 80.0,
|
||||||
|
'hot_temp': 90.0,
|
||||||
|
'dangerous_temp': 120.0
|
||||||
|
},
|
||||||
|
'autotuning': {
|
||||||
|
'enabled': True,
|
||||||
|
'psu_power_limit': 900
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window['config'].update(await bos_config_convert(config))
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from API import APIError
|
from API import APIError
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyPep8
|
||||||
async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int = 0):
|
async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int = 0):
|
||||||
path = [*path]
|
path = [*path]
|
||||||
if len(path) == idx+1:
|
if len(path) == idx+1:
|
||||||
@@ -18,6 +19,7 @@ async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int =
|
|||||||
if path[idx] in data.keys():
|
if path[idx] in data.keys():
|
||||||
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
|
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
|
||||||
# has to be == None, or else it fails on 0.0 hashrates
|
# has to be == None, or else it fails on 0.0 hashrates
|
||||||
|
# noinspection PyPep8
|
||||||
if parsed_data == None:
|
if parsed_data == None:
|
||||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||||
return parsed_data
|
return parsed_data
|
||||||
@@ -34,6 +36,7 @@ async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int =
|
|||||||
if len(data) > path[idx]:
|
if len(data) > path[idx]:
|
||||||
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
|
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
|
||||||
# has to be == None, or else it fails on 0.0 hashrates
|
# has to be == None, or else it fails on 0.0 hashrates
|
||||||
|
# noinspection PyPep8
|
||||||
if parsed_data == None:
|
if parsed_data == None:
|
||||||
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
|
||||||
return parsed_data
|
return parsed_data
|
||||||
72
cfg_util/func/ui.py
Normal file
72
cfg_util/func/ui.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import ipaddress
|
||||||
|
import re
|
||||||
|
|
||||||
|
from cfg_util.layout import window
|
||||||
|
|
||||||
|
import pyperclip
|
||||||
|
|
||||||
|
|
||||||
|
def copy_from_table(table):
|
||||||
|
selection = table.selection()
|
||||||
|
copy_values = []
|
||||||
|
for each in selection:
|
||||||
|
try:
|
||||||
|
value = table.item(each)["values"][0]
|
||||||
|
copy_values.append(str(value))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
copy_string = "\n".join(copy_values)
|
||||||
|
pyperclip.copy(copy_string)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_ui_with_data(key, message, append=False):
|
||||||
|
if append:
|
||||||
|
message = window[key].get_text() + message
|
||||||
|
window[key].update(message)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_prog_bar(amount):
|
||||||
|
window["progress"].Update(amount)
|
||||||
|
percent_done = 100 * (amount / window['progress'].maxlen)
|
||||||
|
window["progress_percent"].Update(f"{round(percent_done, 2)} %")
|
||||||
|
if percent_done == 100:
|
||||||
|
window["progress_percent"].Update("")
|
||||||
|
|
||||||
|
|
||||||
|
async def set_progress_bar_len(amount):
|
||||||
|
window["progress"].Update(0, max=amount)
|
||||||
|
window["progress"].maxlen = amount
|
||||||
|
window["progress_percent"].Update("0.0 %")
|
||||||
|
|
||||||
|
|
||||||
|
async def sort_data(index: int or str):
|
||||||
|
await update_ui_with_data("status", "Sorting Data")
|
||||||
|
data_list = window['ip_table'].Values
|
||||||
|
|
||||||
|
# wattage
|
||||||
|
if re.match("[0-9]* W", str(data_list[0][index])):
|
||||||
|
new_list = sorted(data_list, key=lambda x: int(x[index].replace(" W", "")))
|
||||||
|
if data_list == new_list:
|
||||||
|
new_list = sorted(data_list, reverse=True, key=lambda x: int(x[index].replace(" W", "")))
|
||||||
|
|
||||||
|
# hashrate
|
||||||
|
elif re.match("[0-9]*\.?[0-9]* TH\/s", str(data_list[0][index])):
|
||||||
|
new_list = sorted(data_list, key=lambda x: float(x[index].replace(" TH/s", "")))
|
||||||
|
if data_list == new_list:
|
||||||
|
new_list = sorted(data_list, reverse=True, key=lambda x: float(x[index].replace(" TH/s", "")))
|
||||||
|
|
||||||
|
# ip addresses
|
||||||
|
elif re.match("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",
|
||||||
|
str(data_list[0][index])):
|
||||||
|
new_list = sorted(data_list, key=lambda x: ipaddress.ip_address(x[index]))
|
||||||
|
if data_list == new_list:
|
||||||
|
new_list = sorted(data_list, reverse=True, key=lambda x: ipaddress.ip_address(x[index]))
|
||||||
|
|
||||||
|
# everything else, hostname, temp, and user
|
||||||
|
else:
|
||||||
|
new_list = sorted(data_list, key=lambda x: x[index])
|
||||||
|
if data_list == new_list:
|
||||||
|
new_list = sorted(data_list, reverse=True, key=lambda x: x[index])
|
||||||
|
|
||||||
|
await update_ui_with_data("ip_table", new_list)
|
||||||
|
await update_ui_with_data("status", "")
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
This file stores the MinerFactory instance used by the ConfigUtility for use in other files.
|
||||||
|
"""
|
||||||
|
|
||||||
from miners.miner_factory import MinerFactory
|
from miners.miner_factory import MinerFactory
|
||||||
|
|
||||||
miner_factory = MinerFactory()
|
miner_factory = MinerFactory()
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
import PySimpleGUI as sg
|
||||||
|
|
||||||
from cfg_util.layout import window
|
from cfg_util.layout import window, generate_config_layout
|
||||||
from cfg_util.func import scan_network, sort_data, send_config, miner_light, get_data, export_config_file, \
|
from cfg_util.func.miners import scan_network, send_config, miner_light, get_data, generate_config, import_config, \
|
||||||
generate_config, import_config, import_iplist, import_config_file, export_iplist
|
scan_and_get_data
|
||||||
|
from cfg_util.func.files import import_iplist, import_config_file, export_iplist, export_config_file
|
||||||
|
from cfg_util.func.ui import sort_data, copy_from_table
|
||||||
|
|
||||||
from network import MinerNetwork
|
from network import MinerNetwork
|
||||||
|
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
|
||||||
async def ui():
|
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:
|
while True:
|
||||||
event, value = window.read(timeout=10)
|
event, value = window.read(timeout=10)
|
||||||
if event in (None, 'Close'):
|
if event in (None, 'Close', sg.WIN_CLOSED):
|
||||||
sys.exit()
|
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 event == 'scan':
|
||||||
if len(value['miner_network'].split("/")) > 1:
|
if len(value['miner_network'].split("/")) > 1:
|
||||||
network = value['miner_network'].split("/")
|
network = value['miner_network'].split("/")
|
||||||
@@ -21,36 +37,51 @@ async def ui():
|
|||||||
miner_network = MinerNetwork(value['miner_network'])
|
miner_network = MinerNetwork(value['miner_network'])
|
||||||
asyncio.create_task(scan_network(miner_network))
|
asyncio.create_task(scan_network(miner_network))
|
||||||
if event == 'select_all_ips':
|
if event == 'select_all_ips':
|
||||||
if value['ip_list'] == window['ip_list'].Values:
|
if len(value["ip_table"]) == len(window["ip_table"].Values):
|
||||||
window['ip_list'].set_value([])
|
window["ip_table"].update(select_rows=())
|
||||||
else:
|
else:
|
||||||
window['ip_list'].set_value(window['ip_list'].Values)
|
window["ip_table"].update(select_rows=([row for row in range(len(window["ip_table"].Values))]))
|
||||||
if event == 'import_config':
|
if event == 'import_config':
|
||||||
if 2 > len(value['ip_list']) > 0:
|
if 2 > len(value['ip_table']) > 0:
|
||||||
asyncio.create_task(import_config(value['ip_list']))
|
asyncio.create_task(import_config(value['ip_table']))
|
||||||
if event == 'light':
|
if event == 'light':
|
||||||
asyncio.create_task(miner_light(value['ip_list']))
|
asyncio.create_task(miner_light([window['ip_table'].Values[item][0] for item in value['ip_table']]))
|
||||||
if event == "import_iplist":
|
if event == "import_iplist":
|
||||||
asyncio.create_task(import_iplist(value["file_iplist"]))
|
asyncio.create_task(import_iplist(value["file_iplist"]))
|
||||||
if event == "export_iplist":
|
if event == "export_iplist":
|
||||||
asyncio.create_task(export_iplist(value["file_iplist"], value['ip_list']))
|
asyncio.create_task(export_iplist(value["file_iplist"], [window['ip_table'].Values[item][0] for item in value['ip_table']]))
|
||||||
if event == "send_config":
|
if event == "send_config":
|
||||||
asyncio.create_task(send_config(value['ip_list'], value['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":
|
if event == "import_file_config":
|
||||||
asyncio.create_task(import_config_file(value['file_config']))
|
asyncio.create_task(import_config_file(value['file_config']))
|
||||||
if event == "export_file_config":
|
if event == "export_file_config":
|
||||||
asyncio.create_task(export_config_file(value['file_config'], value["config"]))
|
asyncio.create_task(export_config_file(value['file_config'], value["config"]))
|
||||||
if event == "get_data":
|
if event == "get_data":
|
||||||
asyncio.create_task(get_data(value['ip_list']))
|
if len(window["ip_table"].Values) == 0:
|
||||||
|
if len(value['miner_network'].split("/")) > 1:
|
||||||
|
network = value['miner_network'].split("/")
|
||||||
|
miner_network = MinerNetwork(ip_addr=network[0], mask=network[1])
|
||||||
|
else:
|
||||||
|
miner_network = MinerNetwork(value['miner_network'])
|
||||||
|
asyncio.create_task(scan_and_get_data(miner_network))
|
||||||
|
else:
|
||||||
|
asyncio.create_task(get_data([window["ip_table"].Values[item][0] for item in value["ip_table"]]))
|
||||||
if event == "generate_config":
|
if event == "generate_config":
|
||||||
asyncio.create_task(generate_config())
|
await generate_config_ui()
|
||||||
if event == "sort_data_ip":
|
|
||||||
asyncio.create_task(sort_data('ip'))
|
|
||||||
if event == "sort_data_hr":
|
|
||||||
asyncio.create_task(sort_data('hr'))
|
|
||||||
if event == "sort_data_user":
|
|
||||||
asyncio.create_task(sort_data(3))
|
|
||||||
if event == "sort_data_w":
|
|
||||||
asyncio.create_task(sort_data('wattage'))
|
|
||||||
if event == "__TIMEOUT__":
|
if event == "__TIMEOUT__":
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_config_ui():
|
||||||
|
generate_config_window = sg.Window("Generate Config", generate_config_layout(), modal=True)
|
||||||
|
while True:
|
||||||
|
event, values = generate_config_window.read()
|
||||||
|
if event in (None, 'Close', sg.WIN_CLOSED):
|
||||||
|
break
|
||||||
|
if event == "generate_config_window_generate":
|
||||||
|
if values['generate_config_window_username']:
|
||||||
|
await generate_config(values['generate_config_window_username'],
|
||||||
|
values['generate_config_window_workername'],
|
||||||
|
values['generate_config_window_allow_v2'])
|
||||||
|
generate_config_window.close()
|
||||||
|
break
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ The build will show up in the build directory.
|
|||||||
import datetime
|
import datetime
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from cx_Freeze import setup, Executable
|
from cx_Freeze import setup, Executable
|
||||||
|
|
||||||
base = None
|
base = None
|
||||||
@@ -18,9 +17,15 @@ if sys.platform == "win32":
|
|||||||
version = datetime.datetime.now()
|
version = datetime.datetime.now()
|
||||||
version = version.strftime("%y.%m.%d")
|
version = version.strftime("%y.%m.%d")
|
||||||
print(version)
|
print(version)
|
||||||
|
|
||||||
|
|
||||||
setup(name="UpstreamCFGUtil.exe",
|
setup(name="UpstreamCFGUtil.exe",
|
||||||
version=version,
|
version=version,
|
||||||
description="Upstream Data Config Utility Build",
|
description="Upstream Data Config Utility Build",
|
||||||
options={"build_exe": {"build_exe": f"{os.getcwd()}\\build\\UpstreamCFGUtil-{version}-{sys.platform}\\"}},
|
options={"build_exe": {"build_exe": f"{os.getcwd()}\\build\\UpstreamCFGUtil-{version}-{sys.platform}\\",
|
||||||
|
"include_files": [os.path.join(os.getcwd(), "settings/settings.toml"),
|
||||||
|
os.path.join(os.getcwd(), "static/CFG-Util-README.md")],
|
||||||
|
},
|
||||||
|
},
|
||||||
executables=[Executable("config_tool.py", base=base, icon="icon.ico", target_name="UpstreamCFGUtil.exe")]
|
executables=[Executable("config_tool.py", base=base, icon="icon.ico", target_name="UpstreamCFGUtil.exe")]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ class BaseMiner:
|
|||||||
def __init__(self, ip: str, api: BMMinerAPI | BOSMinerAPI | CGMinerAPI | BTMinerAPI | UnknownAPI) -> None:
|
def __init__(self, ip: str, api: BMMinerAPI | BOSMinerAPI | CGMinerAPI | BTMinerAPI | UnknownAPI) -> None:
|
||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ipaddress.ip_address(ip)
|
||||||
self.api = api
|
self.api = api
|
||||||
|
self.api_type = None
|
||||||
|
self.model = None
|
||||||
|
|||||||
11
miners/antminer/S9/bmminer.py
Normal file
11
miners/antminer/S9/bmminer.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from miners.bmminer import BMMiner
|
||||||
|
|
||||||
|
|
||||||
|
class BMMinerS9(BMMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.model = "S9"
|
||||||
|
self.api_type = "BMMiner"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"BMMinerS9: {str(self.ip)}"
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
from miners import BaseMiner
|
|
||||||
from API.bosminer import BOSMinerAPI
|
|
||||||
import asyncssh
|
|
||||||
import toml
|
|
||||||
from config.bos import bos_config_convert, general_config_convert_bos
|
|
||||||
|
|
||||||
|
|
||||||
class BOSMinerS9(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BOSMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
self.config = None
|
|
||||||
self.uname = 'root'
|
|
||||||
self.pwd = 'admin'
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"S9 - BOSminer: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
|
||||||
"""Create a new asyncssh connection"""
|
|
||||||
conn = await asyncssh.connect(str(self.ip), known_hosts=None, username=self.uname, password=self.pwd,
|
|
||||||
server_host_key_algs=['ssh-rsa'])
|
|
||||||
# return created connection
|
|
||||||
return conn
|
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd: str) -> None:
|
|
||||||
"""Sends SSH command to miner."""
|
|
||||||
# creates result variable
|
|
||||||
result = None
|
|
||||||
|
|
||||||
# runs the command on the miner
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
# attempt to run command up to 3 times
|
|
||||||
for i in range(3):
|
|
||||||
try:
|
|
||||||
# save result of the command
|
|
||||||
result = await conn.run(cmd)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{cmd} error: {e}")
|
|
||||||
if i == 3:
|
|
||||||
return
|
|
||||||
continue
|
|
||||||
|
|
||||||
# let the user know the result of the command
|
|
||||||
if result is not None:
|
|
||||||
if result.stdout != "":
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
elif result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
else:
|
|
||||||
print(cmd)
|
|
||||||
|
|
||||||
async def fault_light_on(self) -> None:
|
|
||||||
"""Sends command to turn on fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light on')
|
|
||||||
|
|
||||||
async def fault_light_off(self) -> None:
|
|
||||||
"""Sends command to turn off fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light off')
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
"""Restart bosminer hashing process."""
|
|
||||||
await self.send_ssh_command('/etc/init.d/bosminer restart')
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
"""Reboots power to the physical miner."""
|
|
||||||
await self.send_ssh_command('/sbin/reboot')
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml') as file:
|
|
||||||
toml_data = toml.loads(await file.read())
|
|
||||||
cfg = await bos_config_convert(toml_data)
|
|
||||||
self.config = cfg
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
"""Attempts to get hostname from miner."""
|
|
||||||
try:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
|
||||||
return data.stdout.strip()
|
|
||||||
except Exception as e:
|
|
||||||
print(self.ip, e)
|
|
||||||
return "BOSMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self, yaml_config) -> None:
|
|
||||||
"""Configures miner with yaml config."""
|
|
||||||
toml_conf = await general_config_convert_bos(yaml_config)
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
|
|
||||||
await file.write(toml_conf)
|
|
||||||
await conn.run("/etc/init.d/bosminer restart")
|
|
||||||
|
|
||||||
async def get_bad_boards(self) -> list:
|
|
||||||
"""Checks for and provides list of non working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if bad > 0:
|
|
||||||
return [str(self.ip), bad]
|
|
||||||
|
|
||||||
async def check_good_boards(self) -> str:
|
|
||||||
"""Checks for and provides list for working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if not bad > 0:
|
|
||||||
return str(self.ip)
|
|
||||||
11
miners/antminer/S9/bosminer.py
Normal file
11
miners/antminer/S9/bosminer.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from miners.bosminer import BOSminer
|
||||||
|
|
||||||
|
|
||||||
|
class BOSMinerS9(BOSminer):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.model = "S9"
|
||||||
|
self.api_type = "BOSMiner"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"BOSminerS9: {str(self.ip)}"
|
||||||
11
miners/antminer/S9/cgminer.py
Normal file
11
miners/antminer/S9/cgminer.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from miners.cgminer import CGMiner
|
||||||
|
|
||||||
|
|
||||||
|
class CGMinerS9(CGMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.model = "S9"
|
||||||
|
self.api_type = "CGMiner"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"CGMinerS9: {str(self.ip)}"
|
||||||
9
miners/antminer/X17/bmminer.py
Normal file
9
miners/antminer/X17/bmminer.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from miners.bmminer import BMMiner
|
||||||
|
|
||||||
|
|
||||||
|
class BMMinerX17(BMMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"CGMinerX17: {str(self.ip)}"
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
from miners import BaseMiner
|
|
||||||
from API.bosminer import BOSMinerAPI
|
|
||||||
import asyncssh
|
|
||||||
import toml
|
|
||||||
from config.bos import bos_config_convert, general_config_convert_bos
|
|
||||||
|
|
||||||
|
|
||||||
class BOSminerX17(BaseMiner):
|
|
||||||
def __init__(self, ip: str) -> None:
|
|
||||||
api = BOSMinerAPI(ip)
|
|
||||||
super().__init__(ip, api)
|
|
||||||
self.config = None
|
|
||||||
self.uname = 'root'
|
|
||||||
self.pwd = 'admin'
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"X17 - BOSminer: {str(self.ip)}"
|
|
||||||
|
|
||||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
|
||||||
"""Create a new asyncssh connection"""
|
|
||||||
conn = await asyncssh.connect(str(self.ip), known_hosts=None, username=self.uname, password=self.pwd,
|
|
||||||
server_host_key_algs=['ssh-rsa'])
|
|
||||||
# return created connection
|
|
||||||
return conn
|
|
||||||
|
|
||||||
async def send_ssh_command(self, cmd: str) -> None:
|
|
||||||
"""Sends SSH command to miner."""
|
|
||||||
# creates result variable
|
|
||||||
result = None
|
|
||||||
|
|
||||||
# runs the command on the miner
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
# attempt to run command up to 3 times
|
|
||||||
for i in range(3):
|
|
||||||
try:
|
|
||||||
# save result of the command
|
|
||||||
result = await conn.run(cmd)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{cmd} error: {e}")
|
|
||||||
if i == 3:
|
|
||||||
return
|
|
||||||
continue
|
|
||||||
|
|
||||||
# let the user know the result of the command
|
|
||||||
if result is not None:
|
|
||||||
if result.stdout != "":
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
elif result.stderr != "":
|
|
||||||
print("ERROR: " + result.stderr)
|
|
||||||
else:
|
|
||||||
print(cmd)
|
|
||||||
|
|
||||||
async def fault_light_on(self) -> None:
|
|
||||||
"""Sends command to turn on fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light on')
|
|
||||||
|
|
||||||
async def fault_light_off(self) -> None:
|
|
||||||
"""Sends command to turn off fault light on the miner."""
|
|
||||||
await self.send_ssh_command('miner fault_light off')
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
"""Restart bosminer hashing process."""
|
|
||||||
await self.send_ssh_command('/etc/init.d/bosminer restart')
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
"""Reboots power to the physical miner."""
|
|
||||||
await self.send_ssh_command('/sbin/reboot')
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml') as file:
|
|
||||||
toml_data = toml.loads(await file.read())
|
|
||||||
cfg = await bos_config_convert(toml_data)
|
|
||||||
self.config = cfg
|
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
"""Attempts to get hostname from miner."""
|
|
||||||
try:
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
|
||||||
return data.stdout.strip()
|
|
||||||
except Exception as e:
|
|
||||||
print(self.ip, e)
|
|
||||||
return "BOSMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self, yaml_config) -> None:
|
|
||||||
"""Configures miner with yaml config."""
|
|
||||||
toml_conf = await general_config_convert_bos(yaml_config)
|
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
|
||||||
async with conn.start_sftp_client() as sftp:
|
|
||||||
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
|
|
||||||
await file.write(toml_conf)
|
|
||||||
await conn.run("/etc/init.d/bosminer restart")
|
|
||||||
|
|
||||||
async def get_bad_boards(self) -> list:
|
|
||||||
"""Checks for and provides list of non working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if bad > 0:
|
|
||||||
return [str(self.ip), bad]
|
|
||||||
|
|
||||||
async def check_good_boards(self) -> str:
|
|
||||||
"""Checks for and provides list for working boards."""
|
|
||||||
devs = await self.api.devdetails()
|
|
||||||
bad = 0
|
|
||||||
chains = devs['DEVDETAILS']
|
|
||||||
for chain in chains:
|
|
||||||
if chain['Chips'] == 0:
|
|
||||||
bad += 1
|
|
||||||
if not bad > 0:
|
|
||||||
return str(self.ip)
|
|
||||||
10
miners/antminer/X17/bosminer.py
Normal file
10
miners/antminer/X17/bosminer.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from miners.bosminer import BOSminer
|
||||||
|
|
||||||
|
|
||||||
|
class BOSMinerX17(BOSminer):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.api_type = "BOSMiner"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"BOSminerX17: {str(self.ip)}"
|
||||||
10
miners/antminer/X17/cgminer.py
Normal file
10
miners/antminer/X17/cgminer.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from miners.cgminer import CGMiner
|
||||||
|
|
||||||
|
|
||||||
|
class CGMinerX17(CGMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.api_type = "CGMiner"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"CGMinerX17: {str(self.ip)}"
|
||||||
18
miners/antminer/X19/bmminer.py
Normal file
18
miners/antminer/X19/bmminer.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from miners.bmminer import BMMiner
|
||||||
|
|
||||||
|
|
||||||
|
class BMMinerX19(BMMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"BMMinerX19: {str(self.ip)}"
|
||||||
|
|
||||||
|
async def get_model(self):
|
||||||
|
if self.model:
|
||||||
|
return self.model
|
||||||
|
version_data = await self.api.version()
|
||||||
|
if version_data:
|
||||||
|
self.model = version_data["VERSION"][0]["Type"].replace("Antminer ", "")
|
||||||
|
return self.model
|
||||||
|
return None
|
||||||
19
miners/antminer/X19/cgminer.py
Normal file
19
miners/antminer/X19/cgminer.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from miners.cgminer import CGMiner
|
||||||
|
|
||||||
|
|
||||||
|
class CGMinerX19(CGMiner):
|
||||||
|
def __init__(self, ip: str) -> None:
|
||||||
|
super().__init__(ip)
|
||||||
|
self.api_type = "CGMiner"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"CGMinerX19: {str(self.ip)}"
|
||||||
|
|
||||||
|
async def get_model(self):
|
||||||
|
if self.model:
|
||||||
|
return self.model
|
||||||
|
version_data = await self.api.version()
|
||||||
|
if version_data:
|
||||||
|
self.model = version_data["VERSION"][0]["Type"].replace("Antminer ", "")
|
||||||
|
return self.model
|
||||||
|
return None
|
||||||
@@ -5,13 +5,23 @@ from miners import BaseMiner
|
|||||||
class BMMiner(BaseMiner):
|
class BMMiner(BaseMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
api = BMMinerAPI(ip)
|
api = BMMinerAPI(ip)
|
||||||
|
self.model = None
|
||||||
super().__init__(ip, api)
|
super().__init__(ip, api)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"BMMiner: {str(self.ip)}"
|
return f"BMMiner: {str(self.ip)}"
|
||||||
|
|
||||||
|
async def get_model(self):
|
||||||
|
if self.model:
|
||||||
|
return self.model
|
||||||
|
version_data = await self.api.devdetails()
|
||||||
|
if version_data:
|
||||||
|
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
|
||||||
|
return self.model
|
||||||
|
return None
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
async def get_hostname(self) -> str:
|
||||||
return "BMMiner Unknown"
|
return "?"
|
||||||
|
|
||||||
async def send_config(self, _):
|
async def send_config(self, _):
|
||||||
return None # ignore for now
|
return None # ignore for now
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class BOSminer(BaseMiner):
|
|||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
api = BOSMinerAPI(ip)
|
api = BOSMinerAPI(ip)
|
||||||
super().__init__(ip, api)
|
super().__init__(ip, api)
|
||||||
|
self.model = None
|
||||||
self.config = None
|
self.config = None
|
||||||
self.uname = 'root'
|
self.uname = 'root'
|
||||||
self.pwd = 'admin'
|
self.pwd = 'admin'
|
||||||
@@ -86,6 +87,15 @@ class BOSminer(BaseMiner):
|
|||||||
print(self.ip, e)
|
print(self.ip, e)
|
||||||
return "BOSMiner Unknown"
|
return "BOSMiner Unknown"
|
||||||
|
|
||||||
|
async def get_model(self):
|
||||||
|
if self.model:
|
||||||
|
return self.model
|
||||||
|
version_data = await self.api.devdetails()
|
||||||
|
if version_data:
|
||||||
|
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
|
||||||
|
return self.model
|
||||||
|
return None
|
||||||
|
|
||||||
async def send_config(self, yaml_config) -> None:
|
async def send_config(self, yaml_config) -> None:
|
||||||
"""Configures miner with yaml config."""
|
"""Configures miner with yaml config."""
|
||||||
toml_conf = await general_config_convert_bos(yaml_config)
|
toml_conf = await general_config_convert_bos(yaml_config)
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
from API.btminer import BTMinerAPI
|
from API.btminer import BTMinerAPI
|
||||||
from miners import BaseMiner
|
from miners import BaseMiner
|
||||||
|
from API import APIError
|
||||||
|
|
||||||
|
|
||||||
class BTMiner(BaseMiner):
|
class BTMiner(BaseMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
api = BTMinerAPI(ip)
|
api = BTMinerAPI(ip)
|
||||||
|
self.model = None
|
||||||
super().__init__(ip, api)
|
super().__init__(ip, api)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"BTMiner: {str(self.ip)}"
|
return f"BTMiner: {str(self.ip)}"
|
||||||
|
|
||||||
|
async def get_model(self):
|
||||||
|
if self.model:
|
||||||
|
return self.model
|
||||||
|
version_data = await self.api.devdetails()
|
||||||
|
if version_data:
|
||||||
|
self.model = version_data["DEVDETAILS"][0]["Model"].split("V")[0]
|
||||||
|
return self.model
|
||||||
|
return None
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
async def get_hostname(self) -> str:
|
||||||
return "BTMiner Unknown"
|
try:
|
||||||
|
host_data = await self.api.get_miner_info()
|
||||||
|
if host_data:
|
||||||
|
return host_data["Msg"]["hostname"]
|
||||||
|
except APIError:
|
||||||
|
return "?"
|
||||||
|
|
||||||
async def send_config(self, _):
|
async def send_config(self, _):
|
||||||
return None # ignore for now
|
return None # ignore for now
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class CGMiner(BaseMiner):
|
|||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
api = CGMinerAPI(ip)
|
api = CGMinerAPI(ip)
|
||||||
super().__init__(ip, api)
|
super().__init__(ip, api)
|
||||||
|
self.model = None
|
||||||
self.config = None
|
self.config = None
|
||||||
self.uname = 'root'
|
self.uname = 'root'
|
||||||
self.pwd = 'admin'
|
self.pwd = 'admin'
|
||||||
@@ -14,6 +15,15 @@ class CGMiner(BaseMiner):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"CGMiner: {str(self.ip)}"
|
return f"CGMiner: {str(self.ip)}"
|
||||||
|
|
||||||
|
async def get_model(self):
|
||||||
|
if self.model:
|
||||||
|
return self.model
|
||||||
|
version_data = await self.api.devdetails()
|
||||||
|
if version_data:
|
||||||
|
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
|
||||||
|
return self.model
|
||||||
|
return None
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
async def get_hostname(self) -> str:
|
||||||
try:
|
try:
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
@@ -21,9 +31,9 @@ class CGMiner(BaseMiner):
|
|||||||
data = await conn.run('cat /proc/sys/kernel/hostname')
|
data = await conn.run('cat /proc/sys/kernel/hostname')
|
||||||
return data.stdout.strip()
|
return data.stdout.strip()
|
||||||
else:
|
else:
|
||||||
return "CGMiner Unknown"
|
return "?"
|
||||||
except Exception:
|
except Exception:
|
||||||
return "CGMiner Unknown"
|
return "?"
|
||||||
|
|
||||||
async def send_config(self, _):
|
async def send_config(self, _):
|
||||||
return None # ignore for now
|
return None # ignore for now
|
||||||
@@ -67,10 +77,15 @@ class CGMiner(BaseMiner):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _result_handler(result: asyncssh.process.SSHCompletedProcess) -> None:
|
def _result_handler(result: asyncssh.process.SSHCompletedProcess) -> None:
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
if len(result.stdout) > 0:
|
if len(result.stdout) > 0:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
print("ssh stdout: \n" + result.stdout)
|
print("ssh stdout: \n" + result.stdout)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
if len(result.stderr) > 0:
|
if len(result.stderr) > 0:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
print("ssh stderr: \n" + result.stderrr)
|
print("ssh stderr: \n" + result.stderrr)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
if len(result.stdout) <= 0 and len(result.stderr) <= 0:
|
if len(result.stdout) <= 0 and len(result.stderr) <= 0:
|
||||||
print("ssh stdout stderr empty")
|
print("ssh stdout stderr empty")
|
||||||
# if result.stdout != "":
|
# if result.stdout != "":
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
from miners.bosminer import BOSminer
|
from miners.antminer.S9.bosminer import BOSMinerS9
|
||||||
|
from miners.antminer.S9.bmminer import BMMinerS9
|
||||||
|
from miners.antminer.S9.cgminer import CGMinerS9
|
||||||
|
|
||||||
|
|
||||||
|
from miners.antminer.X17.bosminer import BOSMinerX17
|
||||||
|
from miners.antminer.X17.bmminer import BMMinerX17
|
||||||
|
from miners.antminer.X17.cgminer import CGMinerX17
|
||||||
|
|
||||||
|
from miners.antminer.X19.bmminer import BMMinerX19
|
||||||
|
from miners.antminer.X19.cgminer import CGMinerX19
|
||||||
|
|
||||||
|
from miners.whatsminer.M20 import BTMinerM20
|
||||||
|
from miners.whatsminer.M21 import BTMinerM21
|
||||||
|
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.bmminer import BMMiner
|
||||||
from miners.cgminer import CGMiner
|
from miners.cgminer import CGMiner
|
||||||
from miners.btminer import BTMiner
|
|
||||||
from miners.unknown import UnknownMiner
|
from miners.unknown import UnknownMiner
|
||||||
from API import APIError
|
from API import APIError
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -16,6 +32,14 @@ class MinerFactory:
|
|||||||
self.miners = {}
|
self.miners = {}
|
||||||
|
|
||||||
async def get_miner_generator(self, ips: list):
|
async def get_miner_generator(self, ips: list):
|
||||||
|
"""
|
||||||
|
Get Miner objects from ip addresses using an async generator.
|
||||||
|
|
||||||
|
Returns an asynchronous generator containing Miners.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ips: a list of ip addresses to get miners for.
|
||||||
|
"""
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
scan_tasks = []
|
scan_tasks = []
|
||||||
for miner in ips:
|
for miner in ips:
|
||||||
@@ -24,37 +48,53 @@ class MinerFactory:
|
|||||||
for miner in scanned:
|
for miner in scanned:
|
||||||
yield await miner
|
yield await miner
|
||||||
|
|
||||||
async def get_miner(self, ip: ipaddress.ip_address) -> BOSminer or CGMiner or BMMiner or UnknownMiner:
|
async def get_miner(self, ip: ipaddress.ip_address):
|
||||||
"""Decide a miner type using the IP address of the miner."""
|
"""Decide a miner type using the IP address of the miner."""
|
||||||
# check if the miner already exists in cache
|
# check if the miner already exists in cache
|
||||||
if ip in self.miners:
|
if ip in self.miners:
|
||||||
return self.miners[ip]
|
return self.miners[ip]
|
||||||
# get the version data
|
miner = UnknownMiner(str(ip))
|
||||||
version = None
|
api = None
|
||||||
for i in range(GET_VERSION_RETRIES):
|
for i in range(GET_VERSION_RETRIES):
|
||||||
version_data = await self._get_version_data(ip)
|
api = await self._get_api_type(ip)
|
||||||
if version_data:
|
if api:
|
||||||
# if we got version data, get a list of the keys so we can check type of miner
|
|
||||||
version = list(version_data['VERSION'][0].keys())
|
|
||||||
break
|
break
|
||||||
if version:
|
model = None
|
||||||
# check version against different return miner types
|
for i in range(GET_VERSION_RETRIES):
|
||||||
if "BOSminer" in version or "BOSminer+" in version:
|
model = await self._get_miner_model(ip)
|
||||||
miner = BOSminer(str(ip))
|
if model:
|
||||||
elif "CGMiner" in version:
|
break
|
||||||
miner = CGMiner(str(ip))
|
if model:
|
||||||
elif "BMMiner" in version:
|
if "Antminer" in model:
|
||||||
miner = BMMiner(str(ip))
|
if model == "Antminer S9":
|
||||||
elif "BTMiner" in version:
|
if "BOSMiner" in api:
|
||||||
miner = BTMiner(str(ip))
|
miner = BOSMinerS9(str(ip))
|
||||||
else:
|
elif "CGMiner" in api:
|
||||||
print(f"Bad API response: {version}")
|
miner = CGMinerS9(str(ip))
|
||||||
miner = UnknownMiner(str(ip))
|
elif "BMMiner" in api:
|
||||||
else:
|
miner = BMMinerS9(str(ip))
|
||||||
# if we don't get version, miner type is unknown
|
elif "17" in model:
|
||||||
print(f"No API response: {str(ip)}")
|
if "BOSMiner" in api:
|
||||||
miner = UnknownMiner(str(ip))
|
miner = BOSMinerX17(str(ip))
|
||||||
# save the miner in cache
|
elif "CGMiner" in api:
|
||||||
|
miner = CGMinerX17(str(ip))
|
||||||
|
elif "BMMiner" in api:
|
||||||
|
miner = BMMinerX17(str(ip))
|
||||||
|
elif "19" in model:
|
||||||
|
if "CGMiner" in api:
|
||||||
|
miner = CGMinerX19(str(ip))
|
||||||
|
elif "BMMiner" in api:
|
||||||
|
miner = BMMinerX19(str(ip))
|
||||||
|
elif "M20" in model:
|
||||||
|
miner = BTMinerM20(str(ip))
|
||||||
|
elif "M21" in model:
|
||||||
|
miner = BTMinerM21(str(ip))
|
||||||
|
elif "M30" in model:
|
||||||
|
miner = BTMinerM30(str(ip))
|
||||||
|
elif "M31" in model:
|
||||||
|
miner = BTMinerM31(str(ip))
|
||||||
|
elif "M32" in model:
|
||||||
|
miner = BTMinerM32(str(ip))
|
||||||
self.miners[ip] = miner
|
self.miners[ip] = miner
|
||||||
return miner
|
return miner
|
||||||
|
|
||||||
@@ -62,101 +102,107 @@ class MinerFactory:
|
|||||||
"""Clear the miner factory cache."""
|
"""Clear the miner factory cache."""
|
||||||
self.miners = {}
|
self.miners = {}
|
||||||
|
|
||||||
@staticmethod
|
async def _get_miner_model(self, ip: ipaddress.ip_address or str) -> dict or None:
|
||||||
async def _get_version_data(ip: ipaddress.ip_address) -> dict or None:
|
model = None
|
||||||
"""Get data on the version of the miner to return the right miner."""
|
|
||||||
for i in range(3):
|
|
||||||
try:
|
try:
|
||||||
# open a connection to the miner
|
data = await self._send_api_command(str(ip), "devdetails")
|
||||||
fut = asyncio.open_connection(str(ip), 4028)
|
if data.get("STATUS"):
|
||||||
# get reader and writer streams
|
if data["STATUS"][0].get("STATUS") not in ["I", "S"]:
|
||||||
try:
|
try:
|
||||||
reader, writer = await asyncio.wait_for(fut, timeout=7)
|
data = await self._send_api_command(str(ip), "version")
|
||||||
except asyncio.exceptions.TimeoutError:
|
model = data["VERSION"][0]["Type"]
|
||||||
return None
|
except:
|
||||||
|
print(f"Get Model Exception: {ip}")
|
||||||
# create the command
|
|
||||||
cmd = {"command": "version"}
|
|
||||||
|
|
||||||
# send the command
|
|
||||||
writer.write(json.dumps(cmd).encode('utf-8'))
|
|
||||||
await writer.drain()
|
|
||||||
|
|
||||||
# instantiate data
|
|
||||||
data = b""
|
|
||||||
|
|
||||||
# loop to receive all the data
|
|
||||||
while True:
|
|
||||||
d = await reader.read(4096)
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
data += d
|
|
||||||
|
|
||||||
if data.endswith(b"\x00"):
|
|
||||||
data = json.loads(data.decode('utf-8')[:-1])
|
|
||||||
else:
|
else:
|
||||||
# some stupid whatsminers need a different command
|
model = data["DEVDETAILS"][0]["Model"]
|
||||||
fut = asyncio.open_connection(str(ip), 4028)
|
if model:
|
||||||
# get reader and writer streams
|
return model
|
||||||
try:
|
except OSError as e:
|
||||||
reader, writer = await asyncio.wait_for(fut, timeout=7)
|
if e.winerror == 121:
|
||||||
except asyncio.exceptions.TimeoutError:
|
return None
|
||||||
return None
|
else:
|
||||||
|
print(ip, e)
|
||||||
# create the command
|
return None
|
||||||
cmd = {"command": "get_version"}
|
|
||||||
|
async def _send_api_command(self, ip: ipaddress.ip_address or str, command: str):
|
||||||
# send the command
|
try:
|
||||||
writer.write(json.dumps(cmd).encode('utf-8'))
|
# get reader and writer streams
|
||||||
await writer.drain()
|
reader, writer = await asyncio.open_connection(str(ip), 4028)
|
||||||
|
# handle OSError 121
|
||||||
# instantiate data
|
except OSError as e:
|
||||||
data = b""
|
if e.winerror == "121":
|
||||||
|
print("Semaphore Timeout has Expired.")
|
||||||
# loop to receive all the data
|
return {}
|
||||||
while True:
|
|
||||||
d = await reader.read(4096)
|
# create the command
|
||||||
if not d:
|
cmd = {"command": command}
|
||||||
break
|
|
||||||
data += d
|
# send the command
|
||||||
|
writer.write(json.dumps(cmd).encode('utf-8'))
|
||||||
data = data.decode('utf-8').replace("\n", "")
|
await writer.drain()
|
||||||
data = json.loads(data)
|
|
||||||
|
# instantiate data
|
||||||
# close the connection
|
data = b""
|
||||||
writer.close()
|
|
||||||
await writer.wait_closed()
|
# loop to receive all the data
|
||||||
# check if the data returned is correct or an error
|
try:
|
||||||
# if status isn't a key, it is a multicommand
|
while True:
|
||||||
if "STATUS" not in data.keys():
|
d = await reader.read(4096)
|
||||||
for key in data.keys():
|
if not d:
|
||||||
# make sure not to try to turn id into a dict
|
break
|
||||||
if not key == "id":
|
data += d
|
||||||
# make sure they succeeded
|
except Exception as e:
|
||||||
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
|
print(e)
|
||||||
# this is an error
|
|
||||||
raise APIError(data["STATUS"][0]["Msg"])
|
try:
|
||||||
else:
|
# some json from the API returns with a null byte (\x00) on the end
|
||||||
# check for stupid whatsminer formatting
|
if data.endswith(b"\x00"):
|
||||||
if not isinstance(data["STATUS"], list):
|
# handle the null byte
|
||||||
if data["STATUS"] not in ("S", "I"):
|
str_data = data.decode('utf-8')[:-1]
|
||||||
raise APIError(data["Msg"])
|
else:
|
||||||
else:
|
# no null byte
|
||||||
if "whatsminer" in data["Description"]:
|
str_data = data.decode('utf-8')
|
||||||
return {"VERSION": [{"BTMiner": data["Description"]}]}
|
# fix an error with a btminer return having an extra comma that breaks json.loads()
|
||||||
# make sure the command succeeded
|
str_data = str_data.replace(",}", "}")
|
||||||
elif data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
# fix an error with a btminer return having a newline that breaks json.loads()
|
||||||
# this is an error
|
str_data = str_data.replace("\n", "")
|
||||||
raise APIError(data["STATUS"][0]["Msg"])
|
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
|
||||||
# return the data
|
str_data = str_data.replace("}{", "},{")
|
||||||
return data
|
# 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}")
|
||||||
|
data = parsed_data
|
||||||
|
|
||||||
|
# close the connection
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_api_type(self, ip: ipaddress.ip_address or str) -> dict or None:
|
||||||
|
"""Get data on the version of the miner to return the right miner."""
|
||||||
|
api = None
|
||||||
|
try:
|
||||||
|
data = await self._send_api_command(str(ip), "version")
|
||||||
|
if data.get("STATUS") and not data.get("STATUS") == "E":
|
||||||
|
if data["STATUS"][0].get("STATUS") in ["I", "S"]:
|
||||||
|
if "BMMiner" in data["VERSION"][0].keys():
|
||||||
|
api = "BMMiner"
|
||||||
|
elif "CGMiner" in data["VERSION"][0].keys():
|
||||||
|
api = "CGMiner"
|
||||||
|
elif "BOSminer" in data["VERSION"][0].keys() or "BOSminer+" in data["VERSION"][0].keys():
|
||||||
|
api = "BOSMiner"
|
||||||
|
elif data.get("Description") and "whatsminer" in data.get("Description"):
|
||||||
|
api = "BTMiner"
|
||||||
|
if api:
|
||||||
|
return api
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.winerror == 121:
|
if e.winerror == 121:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
print(ip, e)
|
print(ip, e)
|
||||||
# except json.decoder.JSONDecodeError:
|
|
||||||
# print("Decode Error @ " + str(ip) + str(data))
|
|
||||||
# except Exception as e:
|
|
||||||
# print(ip, e)
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ class UnknownMiner(BaseMiner):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Unknown: {str(self.ip)}"
|
return f"Unknown: {str(self.ip)}"
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
async def send_config(self, _):
|
async def send_config(self, _):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,9 @@
|
|||||||
from API.btminer import BTMinerAPI
|
from miners.btminer import BTMiner
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM20(BaseMiner):
|
class BTMinerM20(BTMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
api = BTMinerAPI(ip)
|
super().__init__(ip)
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"M20 - BTMiner: {str(self.ip)}"
|
return f"M20 - BTMiner: {str(self.ip)}"
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -1,26 +1,9 @@
|
|||||||
from API.btminer import BTMinerAPI
|
from miners.btminer import BTMiner
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM21(BaseMiner):
|
class BTMinerM21(BTMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
api = BTMinerAPI(ip)
|
super().__init__(ip)
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"M21 - BTMiner: {str(self.ip)}"
|
return f"M21 - BTMiner: {str(self.ip)}"
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -1,26 +1,9 @@
|
|||||||
from API.btminer import BTMinerAPI
|
from miners.btminer import BTMiner
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM30(BaseMiner):
|
class BTMinerM30(BTMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
api = BTMinerAPI(ip)
|
super().__init__(ip)
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"M30- BTMiner: {str(self.ip)}"
|
return f"M30- BTMiner: {str(self.ip)}"
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -1,26 +1,9 @@
|
|||||||
from API.btminer import BTMinerAPI
|
from miners.btminer import BTMiner
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM31(BaseMiner):
|
class BTMinerM31(BTMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
api = BTMinerAPI(ip)
|
super().__init__(ip)
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"M31 - BTMiner: {str(self.ip)}"
|
return f"M31 - BTMiner: {str(self.ip)}"
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -1,26 +1,9 @@
|
|||||||
from API.btminer import BTMinerAPI
|
from miners.btminer import BTMiner
|
||||||
from miners import BaseMiner
|
|
||||||
|
|
||||||
|
|
||||||
class BTMinerM32(BaseMiner):
|
class BTMinerM32(BTMiner):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
api = BTMinerAPI(ip)
|
super().__init__(ip)
|
||||||
super().__init__(ip, api)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"M32 - BTMiner: {str(self.ip)}"
|
return f"M32 - BTMiner: {str(self.ip)}"
|
||||||
|
|
||||||
async def get_hostname(self) -> str:
|
|
||||||
return "BTMiner Unknown"
|
|
||||||
|
|
||||||
async def send_config(self):
|
|
||||||
return None # ignore for now
|
|
||||||
|
|
||||||
async def restart_backend(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_config(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class MinerNetwork:
|
|||||||
return ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False)
|
return ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False)
|
||||||
|
|
||||||
async def scan_network_for_miners(self) -> None or list:
|
async def scan_network_for_miners(self) -> None or list:
|
||||||
"""Scan the network for miners, and """
|
"""Scan the network for miners, and return found miners as a list."""
|
||||||
local_network = self.get_network()
|
local_network = self.get_network()
|
||||||
print(f"Scanning {local_network} for miners...")
|
print(f"Scanning {local_network} for miners...")
|
||||||
scan_tasks = []
|
scan_tasks = []
|
||||||
@@ -55,6 +55,11 @@ class MinerNetwork:
|
|||||||
return miners
|
return miners
|
||||||
|
|
||||||
async def scan_network_generator(self):
|
async def scan_network_generator(self):
|
||||||
|
"""
|
||||||
|
Scan the network for miners using an async generator.
|
||||||
|
|
||||||
|
Returns an asynchronous generator containing found miners.
|
||||||
|
"""
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
local_network = self.get_network()
|
local_network = self.get_network()
|
||||||
scan_tasks = []
|
scan_tasks = []
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
64
static/CFG-Util-README.md
Normal file
64
static/CFG-Util-README.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# CFG-Util
|
||||||
|
|
||||||
|
## Interact with bitcoin mining ASICs using a simple GUI.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Input Fields
|
||||||
|
### Network IP:
|
||||||
|
* Defaults to 192.168.1.0/24 (192.168.1.0 - 192.168.1.255)
|
||||||
|
* Enter any IP on your local network, and it will automatically load your entire network with a /24 subnet (255 IP addresses)
|
||||||
|
* You can also add a subnet mask by adding a / after the IP and entering the subnet mask
|
||||||
|
* Press Scan to scan the selected network for miners
|
||||||
|
|
||||||
|
### IP List File:
|
||||||
|
* Use the Browse button to select a file
|
||||||
|
* Use the Import button to import all IP addresses from a file, regardless of where they are located in the file
|
||||||
|
* Use the Export button to export all IP addresses (or all selected IP addresses if you select some) to a file, with each 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
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
## 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.
|
||||||
|
* OPEN IN WEB: Opens all currently selected miners web interfaces in your default browser.
|
||||||
|
|
||||||
|
### Table:
|
||||||
|
* Click any header in the table to sort that row.
|
||||||
|
* You can copy (CTRL + C) a list of IP's directly from the rows selected in the table.
|
||||||
|
|
||||||
|
* #### IP:
|
||||||
|
* Contains all the IP's scanned
|
||||||
|
|
||||||
|
* #### Model:
|
||||||
|
* The model of the miners scanned.
|
||||||
|
|
||||||
|
* #### Hostname:
|
||||||
|
* The hostname of the miners scanned.
|
||||||
|
* ? will be displayed if the tool is unable to get it.
|
||||||
|
|
||||||
|
* #### Hashrate:
|
||||||
|
* The hashrate of the miners scanned.
|
||||||
|
|
||||||
|
* #### Temperature:
|
||||||
|
* The average board temperature of the miners scanned.
|
||||||
|
|
||||||
|
* #### Current User:
|
||||||
|
* The current first pool user of the miners scanned.
|
||||||
|
|
||||||
|
* #### Wattage
|
||||||
|
* The current wattage of the miners scanned.
|
||||||
|
* 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
|
||||||
Reference in New Issue
Block a user