Compare commits

..

91 Commits

Author SHA1 Message Date
UpstreamData
15fc27e6fa added configuration for X19 miners 2022-06-07 11:12:26 -06:00
UpstreamData
943ebc77a1 switch braiins miners over to using new config dataclass 2022-06-07 10:49:41 -06:00
UpstreamData
733437ef03 create basic config dataclass to be used to configure miners 2022-06-06 16:05:09 -06:00
UpstreamData
b444245e98 added new whatsminer types to miner factory 2022-06-06 10:09:11 -06:00
UpstreamData
481d31a0f1 added more new whatsminer types 2022-06-06 10:06:17 -06:00
UpstreamData
264db3bdd6 fix a bug with whatsminer M21S missing import 2022-06-06 09:41:10 -06:00
UpstreamData
d292b9c195 improved whatsminer handling, and added VF20 to miner dict 2022-06-06 09:26:38 -06:00
UpstreamData
dce25a679f added new miner type M30S+VF20 2022-06-06 09:17:42 -06:00
UpstreamData
c903631742 improved build process 2022-06-06 09:17:22 -06:00
Colin Crossman
e70bfdc886 Fix indent issue that caused missing MAC addresses (#10) 2022-06-05 15:50:07 -06:00
UpstreamData
8e1803add1 made slight optimizations to get_data and the way the miner gets mac data 2022-06-03 15:30:09 -06:00
UpstreamData
7d61056ea3 added whatsminer M30S+ VE40 2022-06-03 15:00:04 -06:00
UpstreamData
0d497baa45 added mac for M20 series 2022-06-03 14:55:03 -06:00
UpstreamData
d3a71c5a93 added mac addresses to get_data 2022-06-03 14:29:10 -06:00
UpstreamData
895a5b7ac8 fixed more bugs with whatsminers and added more versions 2022-06-03 11:20:34 -06:00
UpstreamData
7a5a0b287c fixed a bug with some versions of whatsminer and improved logging 2022-06-03 09:35:55 -06:00
UpstreamData
c7d73276c8 fixed a small bug with sorting 2022-06-03 08:59:15 -06:00
UpstreamData
4bbb9d0b08 added a basis for configuration of X17 and X19 miners by getting pool info from config file. 2022-06-02 16:06:36 -06:00
UpstreamData
3ee49e6fd7 fixed a warning with ylim being set to 0 2022-06-02 14:52:17 -06:00
UpstreamData
dcd3e99d73 added interval to recording 2022-06-02 14:25:55 -06:00
UpstreamData
64018cdad8 completed basic recording functionality 2022-06-02 14:17:08 -06:00
UpstreamData
e7d269008c added the basics of the recording functionality, just need to write out to file. 2022-06-02 11:08:14 -06:00
UpstreamData
7dfe25e5d2 added base for recording miner data to pdf file. 2022-06-01 16:13:30 -06:00
UpstreamData
382f9cff76 added reboot command for X19 and X17 models on BMMiner 2022-06-01 14:02:34 -06:00
UpstreamData
a5195ff1db fix a bug with testbench where toolbox finds braiins but bench does not 2022-06-01 11:44:07 -06:00
UpstreamData
b1ec726d18 added some docstrings to data 2022-06-01 11:22:30 -06:00
UpstreamData
5ae2cb2b22 fixed a bug with not all table data getting reset on data update 2022-06-01 11:22:12 -06:00
UpstreamData
472a15f4ca added fault light function for X17 BMMiner models 2022-06-01 10:54:45 -06:00
UpstreamData
7cc7973587 fixed a bug with some BOS S17e not returning data frm devdetails and fans 2022-06-01 10:19:58 -06:00
UpstreamData
ab964e4c88 fixed a bug with sorting by chip % 2022-06-01 08:15:35 -06:00
UpstreamData
4087874b4a added get hostname to X19 miners 2022-05-31 17:05:05 -06:00
UpstreamData
844deec0d3 add fault light command to X19 miners 2022-05-31 16:54:56 -06:00
UpstreamData
d36eef4c33 switched to httpx 2022-05-31 16:08:17 -06:00
UpstreamData
69d4ee5570 Revert "add .readthedocs.yaml"
This reverts commit e7b01ccdab.
2022-05-31 13:23:41 -06:00
UpstreamData
e6d3ec01fe Merge remote-tracking branch 'origin/master' 2022-05-31 13:18:59 -06:00
UpstreamData
e7b01ccdab add .readthedocs.yaml 2022-05-31 13:18:52 -06:00
UpstreamData
38506903ea fixed an issue with BMMiner get data and bosminer get data not identifying correct board number. 2022-05-31 08:45:49 -06:00
UpstreamData
c9a1560052 Merge remote-tracking branch 'origin/master' 2022-05-30 14:20:48 -06:00
UpstreamData
88f8ff10b7 fixed a bug with sorting 2022-05-30 14:20:37 -06:00
UpstreamData
11d38c9c3b fixed a bug with sorting 2022-05-30 14:19:57 -06:00
UpstreamData
0082037f45 add dev-requirements and remove cx-freeze from requirements 2022-05-30 13:29:47 -06:00
UpstreamData
dd5ccafa1e added listener function to cfg util 2022-05-30 13:27:56 -06:00
UpstreamData
739126935a fixed some bugs with differing version of BTMiner and different versions of M30S++ having different chip counts 2022-05-30 11:13:37 -06:00
UpstreamData
5c850a43a9 ignore errors with S19 multicommands 2022-05-30 09:46:05 -06:00
UpstreamData
24b037f273 fixed a bug with bmminer stats 2022-05-30 09:40:30 -06:00
UpstreamData
f847700c05 fixed a bug with antminers not reporting type because of fans in testbench, and added a long running get data for long tests 2022-05-27 11:54:51 -06:00
UpstreamData
69820dd9d2 slightly improved getting data from bmminer X9, X17, and X19 with an improvement to finding offset 2022-05-27 11:47:44 -06:00
UpstreamData
ad4b710cb7 miner is no longer cached in miner factory if it is unknown 2022-05-27 11:05:35 -06:00
UpstreamData
c53c18654b improved bmminer with a fan and board offset 2022-05-27 11:01:25 -06:00
UpstreamData
18797f4b56 added S9 data for bmminer 2022-05-27 10:41:41 -06:00
UpstreamData
e86c93e287 fixed refreshing data 2022-05-26 16:19:15 -06:00
UpstreamData
89cfde28f5 added chips for M30S 2022-05-26 16:05:48 -06:00
UpstreamData
0f2a867828 fix wrong import from collections 2022-05-26 15:53:38 -06:00
UpstreamData
4f5aef2d45 update some type hints and comments in miner factory, and remove some uneeded imports 2022-05-26 15:51:57 -06:00
UpstreamData
96801f93d1 made fault lights and async generator to make them much faster 2022-05-26 15:41:41 -06:00
UpstreamData
a8ce73c3d6 fixed an issue with the windows event loop not working with asyncio.create_subprocess_shell 2022-05-26 15:25:04 -06:00
UpstreamData
513dd2b981 fixed abug with testbench not removing miners when there were 0 online 2022-05-26 13:48:49 -06:00
UpstreamData
c35b30e949 fixed formatting issues 2022-05-26 13:23:32 -06:00
UpstreamData
942f2a1c8d improved type hinting and formatting in miner factory 2022-05-26 12:14:28 -06:00
UpstreamData
9078df680e added get_data to web_monitor dashboard 2022-05-26 11:15:01 -06:00
UpstreamData
527997cc58 fixed a bug with refreshing data 2022-05-26 11:13:39 -06:00
UpstreamData
41433bcaf5 change hashrate data to 1m as it seems more consistent, and add get_data to web monitor 2022-05-26 11:12:31 -06:00
UpstreamData
3451b88669 added temps and fans for bmminer and cgminer 2022-05-26 10:57:52 -06:00
UpstreamData
a42af2764e fixed a bug with ideal chips not getting set fast enough 2022-05-26 10:43:10 -06:00
UpstreamData
baaad73eb8 fixed a bug with pool prefix not getting removed when getting data 2022-05-26 10:39:39 -06:00
UpstreamData
34c9f85098 added btminer fan data and per chip temps 2022-05-26 10:36:39 -06:00
UpstreamData
d6638fa4d2 added fan counts to miners, and added more data to bosminer and miner data 2022-05-26 10:26:40 -06:00
UpstreamData
0f51487d3f added miner data to base miner 2022-05-26 08:41:51 -06:00
UpstreamData
3a11b173c3 added chip percent to config tool 2022-05-25 15:02:48 -06:00
UpstreamData
568f86700b improved X19 miner scan speed and implemented miner data in miners 2022-05-25 14:44:23 -06:00
UpstreamData
3b702aac2c improved handling of MinerData by improving dataclass 2022-05-25 14:01:52 -06:00
UpstreamData
6fbd9faffd updated network test to use unittest 2022-05-25 11:49:38 -06:00
UpstreamData
9eb2259aae removed extra print statements and a loop that wasnt needed in miner factory 2022-05-25 09:02:37 -06:00
UpstreamData
149c386a4c fix a bug with X17 not responding 2022-05-25 08:56:02 -06:00
UpstreamData
726e7ff0f0 add support for basic S9is 2022-05-24 14:43:22 -06:00
UpstreamData
87a690eb00 create basic dataclass for miner data 2022-05-20 10:12:51 -06:00
UpstreamData
fd5dba4036 fixed some bugs and improved testbench look 2022-05-19 15:54:29 -06:00
UpstreamData
e54847337a update testbench color palette 2022-05-19 15:31:12 -06:00
UpstreamData
3ff43c3ccd removed old tools that will no longer work 2022-05-19 11:56:12 -06:00
UpstreamData
ec5563f2f0 slightly improved network functionality and added tests for network 2022-05-19 11:55:38 -06:00
UpstreamData
40f14876cc made miner count a fixed position bar 2022-05-19 11:05:40 -06:00
UpstreamData
6abfe8a503 switch testbench to dark mode and add miner count 2022-05-19 10:47:36 -06:00
Dewey Cox
0a4d52ef03 fixed a bug with matplotlib.pyplot.subplots() causing resizing of windows 2022-05-19 09:11:28 -06:00
Colin Crossman
e4207e0120 Allow MinerFactory to take a list of discrete IPs (#7) 2022-05-18 20:16:47 -06:00
UpstreamData
ed89476866 fixed a bug with temp not displaying on the cfg tool 2022-05-18 12:50:44 -06:00
UpstreamData
7f7964526c fixed some bugs with scanning being too fast to get data and killing the tasks 2022-05-18 12:13:20 -06:00
UpstreamData
85b282740a update scanning in web interface 2022-05-18 11:39:14 -06:00
UpstreamData
8cbf3a20a3 make miner ips a true ip address, and allow sorting miners using __lt__ and __gt__ 2022-05-18 11:34:39 -06:00
UpstreamData
8ebcbd3c33 vastly improved scanning in web monitor 2022-05-18 11:12:38 -06:00
UpstreamData
c3e285a9ee fix some bugs in web monitor 2022-05-18 11:06:21 -06:00
UpstreamData
9f19b42de5 fixed some bugs with older versions of braiins OS +, and fixed a bug in testbench 2022-05-17 13:09:10 -06:00
172 changed files with 3469 additions and 3307 deletions

View File

@@ -59,7 +59,9 @@ class BaseMinerAPI:
]
]
async def multicommand(self, *commands: str) -> dict:
async def multicommand(
self, *commands: str, ignore_x19_error: bool = False
) -> dict:
"""Creates and sends multiple commands as one command to the miner."""
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
# split the commands into a proper list
@@ -78,8 +80,8 @@ If you are sure you want to use this command please use API.send_command("{item}
command = "+".join(commands)
data = None
try:
data = await self.send_command(command)
except APIError as e:
data = await self.send_command(command, x19_command=ignore_x19_error)
except APIError:
try:
data = {}
# S19 handler, try again
@@ -96,9 +98,10 @@ If you are sure you want to use this command please use API.send_command("{item}
async def send_command(
self,
command: str,
command: str or bytes,
parameters: str or int or bool = None,
ignore_errors: bool = False,
x19_command: bool = False,
) -> dict:
"""Send an API command to the miner and return the result."""
try:
@@ -143,7 +146,8 @@ If you are sure you want to use this command please use API.send_command("{item}
# validate the command succeeded
validation = self.validate_command_output(data)
if not validation[0]:
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
if not x19_command:
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
raise APIError(validation[1])
return data

View File

@@ -163,6 +163,7 @@ class BTMinerAPI(BaseMinerAPI):
command: str or bytes,
parameters: str or int or bool = None,
ignore_errors: bool = False,
**kwargs,
) -> dict:
"""Send a command to the miner API.
@@ -434,7 +435,7 @@ class BTMinerAPI(BaseMinerAPI):
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
async def update_firmware(self):
async def update_firmware(self): # noqa - static
# to be determined if this will be added later
# requires a file stream in bytes
return NotImplementedError

View File

@@ -76,3 +76,13 @@ SAMPLE CONFIG
}
}
"""
def general_config_convert_pools(config: dict):
out_config = {}
pools = config.get("pool_groups")
if pools:
if len(pools) > 0:
pools = pools[0]
out_config = pools["pools"][:3]
return out_config

View File

@@ -1,6 +1,6 @@
import time
import yaml
import toml
def bos_config_convert(config: dict):

317
config/miner_config.py Normal file
View File

@@ -0,0 +1,317 @@
from dataclasses import dataclass, asdict
from typing import List, Literal
import random
import string
import toml
import yaml
import json
import time
@dataclass
class _Pool:
url: str = ""
username: str = ""
password: str = ""
def from_dict(self, data: dict):
for key in data.keys():
if key == "url":
self.url = data[key]
if key in ["user", "username"]:
self.username = data[key]
if key in ["pass", "password"]:
self.password = data[key]
return self
def as_x19(self, user_suffix: str = None):
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_bos(self, user_suffix: str = None):
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "password": self.password}
return pool
@dataclass
class _PoolGroup:
quota: int = 1
group_name: str = None
pools: List[_Pool] = None
def __post_init__(self):
if not self.group_name:
self.group_name = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(6)
) # generate random pool group name in case it isn't set
def from_dict(self, data: dict):
pools = []
for key in data.keys():
if key in ["name", "group_name"]:
self.group_name = data[key]
if key == "quota":
self.quota = data[key]
if key in ["pools", "pool"]:
for pool in data[key]:
pools.append(_Pool().from_dict(pool))
self.pools = pools
return self
def as_x19(self, user_suffix: str = None):
pools = []
for pool in self.pools[:3]:
pools.append(pool.as_x19(user_suffix=user_suffix))
return pools
def as_bos(self, user_suffix: str = None):
group = {
"name": self.group_name,
"quota": self.quota,
"pool": [pool.as_bos(user_suffix=user_suffix) for pool in self.pools],
}
return group
@dataclass
class MinerConfig:
pool_groups: List[_PoolGroup] = None
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
temp_target: float = 70.0
temp_hot: float = 80.0
temp_dangerous: float = 10.0
minimum_fans: int = None
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
asicboost: bool = None
autotuning_enabled: bool = True
autotuning_wattage: int = 900
dps_enabled: bool = None
dps_power_step: int = None
dps_min_power: int = None
dps_shutdown_enabled: bool = None
dps_shutdown_duration: float = None
def as_dict(self):
data_dict = asdict(self)
for key in asdict(self).keys():
if data_dict[key] is None:
del data_dict[key]
return data_dict
def as_toml(self):
return toml.dumps(self.as_dict())
def as_yaml(self):
return yaml.dump(self.as_dict(), sort_keys=False)
def from_raw(self, data: dict):
pool_groups = []
for key in data.keys():
if key == "pools":
pool_groups.append(_PoolGroup().from_dict({"pools": data[key]}))
elif key == "group":
for group in data[key]:
pool_groups.append(_PoolGroup().from_dict(group))
if key == "bitmain-fan-ctrl":
if data[key]:
self.temp_mode = "manual"
if data.get("bitmain-fan-pwm"):
self.fan_speed = int(data["bitmain-fan-pwm"])
elif key == "fan_control":
for _key in data[key].keys():
if _key == "min_fans":
self.minimum_fans = data[key][_key]
elif _key == "speed":
self.fan_speed = data[key][_key]
elif key == "temp_control":
for _key in data[key].keys():
if _key == "mode":
self.temp_mode = data[key][_key]
elif _key == "target_temp":
self.temp_target = data[key][_key]
elif _key == "hot_temp":
self.temp_hot = data[key][_key]
elif _key == "dangerous_temp":
self.temp_dangerous = data[key][_key]
if key == "hash_chain_global":
if data[key].get("asic_boost"):
self.asicboost = data[key]["asic_boost"]
if key == "autotuning":
for _key in data[key].keys():
if _key == "enabled":
self.autotuning_enabled = data[key][_key]
elif _key == "psu_power_limit":
self.autotuning_wattage = data[key][_key]
if key == "power_scaling":
for _key in data[key].keys():
if _key == "enabled":
self.dps_enabled = data[key][_key]
elif _key == "power_step":
self.dps_power_step = data[key][_key]
elif _key == "min_psu_power_limit":
self.dps_min_power = data[key][_key]
elif _key == "shutdown_enabled":
self.dps_shutdown_enabled = data[key][_key]
elif _key == "shutdown_duration":
self.dps_shutdown_duration = data[key][_key]
self.pool_groups = pool_groups
return self
def from_dict(self, data: dict):
pool_groups = []
for group in data["pool_groups"]:
pool_groups.append(_PoolGroup().from_dict(group))
for key in data.keys():
if getattr(self, key) and not key == "pool_groups":
setattr(self, key, data[key])
self.pool_groups = pool_groups
return self
def from_toml(self, data: str):
return self.from_dict(toml.loads(data))
def from_yaml(self, data: str):
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
def as_x19(self, user_suffix: str = None):
cfg = {
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
"bitmain-fan-ctrl": False,
"bitmain-fan-pwn": 100,
}
if not self.temp_mode == "auto":
cfg["bitmain-fan-ctrl"] = True
if self.fan_speed:
cfg["bitmain-fan-ctrl"] = str(self.fan_speed)
return json.dumps(cfg)
def as_bos(self, model: str = "S9", user_suffix: str = None):
cfg = {
"format": {
"version": "1.2+",
"model": f"Antminer {model}",
"generator": "Upstream Config Utility",
"timestamp": int(time.time()),
},
"group": [
group.as_bos(user_suffix=user_suffix) for group in self.pool_groups
],
"temp_control": {
"mode": self.temp_mode,
"target_temp": self.temp_target,
"hot_temp": self.temp_hot,
"dangerous_temp": self.temp_dangerous,
},
}
if self.autotuning_enabled or self.autotuning_wattage:
cfg["autotuning"] = {}
if self.autotuning_enabled:
cfg["autotuning"]["enabled"] = self.autotuning_enabled
if self.autotuning_wattage:
cfg["autotuning"]["psu_power_limit"] = self.autotuning_wattage
if self.asicboost:
cfg["hash_chain_global"] = {}
cfg["hash_chain_global"]["asic_boost"] = self.asicboost
if any(
[
getattr(self, item)
for item in [
"dps_enabled",
"dps_power_step",
"dps_min_power",
"dps_shutdown_enabled",
"dps_shutdown_duration",
]
]
):
cfg["power_scaling"] = {}
if self.dps_enabled:
cfg["power_scaling"]["enabled"] = self.dps_enabled
if self.dps_power_step:
cfg["power_scaling"]["power_step"] = self.dps_power_step
if self.dps_min_power:
cfg["power_scaling"]["min_psu_power_limit"] = self.dps_min_power
if self.dps_shutdown_enabled:
cfg["power_scaling"]["shutdown_enabled"] = self.dps_shutdown_enabled
if self.dps_shutdown_duration:
cfg["power_scaling"]["shutdown_duration"] = self.dps_shutdown_duration
return toml.dumps(cfg)
if __name__ == "__main__":
import pprint
bos_conf = {
"group": [
{
"name": "group",
"pool": [
{
"password": "123",
"url": "stratum2+tcp://v2.us-east.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt",
"user": "UpstreamDataInc.test",
},
{
"password": "123",
"url": "stratum2+tcp://v2.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt",
"user": "UpstreamDataInc.test",
},
{
"password": "123",
"url": "stratum+tcp://stratum.slushpool.com:3333",
"user": "UpstreamDataInc.test",
},
],
"quota": 1,
},
{
"name": "group_2",
"pool": [
{
"password": "123",
"url": "stratum2+tcp://v2.us-east.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt",
"user": "UpstreamDataInc.test",
},
{
"password": "123",
"url": "stratum2+tcp://v2.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt",
"user": "UpstreamDataInc.test",
},
{
"password": "123",
"url": "stratum+tcp://stratum.slushpool.com:3333",
"user": "UpstreamDataInc.test",
},
],
"quota": 1,
},
],
}
pprint.pprint(MinerConfig().from_dict(bos_conf).as_dict())

115
data/__init__.py Normal file
View File

@@ -0,0 +1,115 @@
from dataclasses import dataclass, field, asdict
from datetime import datetime
@dataclass
class MinerData:
"""A Dataclass to standardize data returned from miners (specifically AnyMiner().get_data())
:param ip: The IP of the miner as a str.
:param datetime: The time and date this data was generated.
:param model: The model of the miner as a str.
:param hostname: The network hostname of the miner as a str.
:param hashrate: The hashrate of the miner in TH/s as a int.
:param left_board_temp: The temp of the left PCB as an int.
:param left_board_chip_temp: The temp of the left board chips as an int.
:param center_board_temp: The temp of the center PCB as an int.
:param center_board_chip_temp: The temp of the center board chips as an int.
:param right_board_temp: The temp of the right PCB as an int.
:param right_board_chip_temp: The temp of the right board chips as an int.
:param wattage: Wattage of the miner as an int.
:param fan_1: The speed of the first fan as an int.
:param fan_2: The speed of the second fan as an int.
:param fan_3: The speed of the third fan as an int.
:param fan_4: The speed of the fourth fan as an int.
:param left_chips: The number of chips online in the left board as an int.
:param center_chips: The number of chips online in the left board as an int.
:param right_chips: The number of chips online in the left board as an int.
:param ideal_chips: The ideal number of chips in the miner as an int.
:param pool_split: The pool split as a str.
:param pool_1_url: The first pool url on the miner as a str.
:param pool_1_user: The first pool user on the miner as a str.
:param pool_2_url: The second pool url on the miner as a str.
:param pool_2_user: The second pool user on the miner as a str.
"""
ip: str
datetime: datetime = None
mac: str = "00:00:00:00:00:00"
model: str = "Unknown"
hostname: str = "Unknown"
hashrate: float = 0
temperature_avg: int = field(init=False)
left_board_temp: int = 0
left_board_chip_temp: int = 0
center_board_temp: int = 0
center_board_chip_temp: int = 0
right_board_temp: int = 0
right_board_chip_temp: int = 0
wattage: int = 0
fan_1: int = -1
fan_2: int = -1
fan_3: int = -1
fan_4: int = -1
left_chips: int = 0
center_chips: int = 0
right_chips: int = 0
total_chips: int = field(init=False)
ideal_chips: int = 1
percent_ideal: float = field(init=False)
nominal: int = field(init=False)
pool_split: str = "0"
pool_1_url: str = "Unknown"
pool_1_user: str = "Unknown"
pool_2_url: str = ""
pool_2_user: str = ""
def __post_init__(self):
self.datetime = datetime.now()
@property
def total_chips(self): # noqa - Skip PyCharm inspection
return self.right_chips + self.center_chips + self.left_chips
@total_chips.setter
def total_chips(self, val):
pass
@property
def nominal(self): # noqa - Skip PyCharm inspection
return self.ideal_chips == self.total_chips
@nominal.setter
def nominal(self, val):
pass
@property
def percent_ideal(self): # noqa - Skip PyCharm inspection
return round((self.total_chips / self.ideal_chips) * 100)
@percent_ideal.setter
def percent_ideal(self, val):
pass
@property
def temperature_avg(self): # noqa - Skip PyCharm inspection
total_temp = 0
temp_count = 0
for temp in [
self.left_board_chip_temp,
self.center_board_chip_temp,
self.right_board_chip_temp,
]:
if not temp == 0:
total_temp += temp
temp_count += 1
if not temp_count > 0:
return 0
return round(total_temp / temp_count)
@temperature_avg.setter
def temperature_avg(self, val):
pass
def asdict(self):
return asdict(self)

BIN
dev-requirements.txt Normal file

Binary file not shown.

View File

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

View File

@@ -9,6 +9,7 @@ import datetime
import sys
import os
from cx_Freeze import setup, Executable
from setuptools import find_packages
base = None
if sys.platform == "win32":
@@ -16,7 +17,6 @@ if sys.platform == "win32":
version = datetime.datetime.now()
version = version.strftime("%y.%m.%d")
print(version)
setup(
@@ -30,6 +30,7 @@ setup(
os.path.join(os.getcwd(), "settings/settings.toml"),
os.path.join(os.getcwd(), "static/CFG-Util-README.md"),
],
"excludes": ["tests", "tools.web_testbench", "tools.web_monitor"],
},
},
executables=[

View File

@@ -1,5 +1,8 @@
import asyncssh
import logging
import ipaddress
from data import MinerData
class BaseMiner:
@@ -14,10 +17,21 @@ class BaseMiner:
self.hostname = None
self.nominal_chips = 1
self.version = None
self.fan_count = 2
self.config = None
def __repr__(self):
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
def __lt__(self, other):
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
def __gt__(self, other):
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
@@ -86,19 +100,5 @@ class BaseMiner:
async def get_mac(self):
return None
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Split": "0",
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
return data
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))

View File

@@ -1,13 +1,19 @@
import ipaddress
import logging
from API.bmminer import BMMinerAPI
from miners import BaseMiner
import logging
from data import MinerData
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
class BMMiner(BaseMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
self.ip = ipaddress.ip_address(ip)
self.api = BMMinerAPI(ip)
self.api_type = "BMMiner"
self.uname = "root"
@@ -61,7 +67,7 @@ class BMMiner(BaseMiner):
# return ? if we fail to get hostname with no ssh connection
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
except Exception as e:
except Exception:
# return ? if we fail to get hostname with an exception
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
@@ -121,40 +127,30 @@ class BMMiner(BaseMiner):
return True
return False
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Total": 0,
"Ideal": self.nominal_chips * 3,
"Left Board": 0,
"Center Board": 0,
"Right Board": 0,
"Nominal": False,
"Split": "0",
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
async def get_data(self) -> MinerData:
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
board_offset = -1
fan_offset = -1
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
if model:
data["Model"] = model
data.model = model
if hostname:
data["Hostname"] = hostname
data.hostname = hostname
if mac:
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
miner_data = await self.api.multicommand("summary", "pools", "stats")
miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True
)
if miner_data:
break
@@ -171,28 +167,47 @@ class BMMiner(BaseMiner):
if len(hr) > 0:
hr = hr[0].get("GHS av")
if hr:
data["Hashrate"] = round(hr / 1000, 2)
data.hashrate = round(hr / 1000, 2)
if stats:
boards = stats.get("STATS")
if boards:
if len(boards) > 0:
data["Left Board"] = boards[1].get("chain_acn1")
data["Center Board"] = boards[1].get("chain_acn2")
data["Right Board"] = boards[1].get("chain_acn3")
data["Total"] = boards[1].get("total_acn")
for board_num in range(1, 16, 5):
for _b_num in range(5):
b = boards[1].get(f"chain_acn{board_num + _b_num}")
if data["Total"] == data["Ideal"]:
data["Nominal"] = True
if b and not b == 0 and board_offset == -1:
board_offset = board_num
if board_offset == -1:
board_offset = 1
data.left_chips = boards[1].get(f"chain_acn{board_offset}")
data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
if stats:
temp = stats.get("STATS")
if temp:
if len(temp) > 1:
for item in ["temp2", "temp1", "temp3"]:
temperature = temp[1].get(item)
if temperature and not temperature == 0.0:
data["Temperature"] = round(temperature)
for fan_num in range(1, 8, 4):
for _f_num in range(4):
f = temp[1].get(f"fan{fan_num + _f_num}")
if f and not f == 0 and fan_offset == -1:
fan_offset = fan_num
if fan_offset == -1:
fan_offset = 1
for fan in range(self.fan_count):
setattr(
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
)
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
for item in range(3):
board_temp = temp[1].get(f"temp{item + board_offset}")
chip_temp = temp[1].get(f"temp2_{item + board_offset}")
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
setattr(data, f"{board_map[item]}_temp", board_temp)
if pools:
pool_1 = None
@@ -220,27 +235,24 @@ class BMMiner(BaseMiner):
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
if pool_1.startswith("stratum+tcp://"):
pool_1.replace("stratum+tcp://", "")
if pool_1.startswith("stratum2+tcp://"):
pool_1.replace("stratum2+tcp://", "")
data["Pool 1"] = pool_1
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_1_url = pool_1
if pool_1_user:
data["Pool 1 User"] = pool_1_user
data["Pool User"] = pool_1_user
data.pool_1_user = pool_1_user
if pool_2:
if pool_2.startswith("stratum+tcp://"):
pool_2.replace("stratum+tcp://", "")
if pool_2.startswith("stratum2+tcp://"):
pool_2.replace("stratum2+tcp://", "")
data["Pool 2"] = pool_2
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
if pool_2_user:
data["Pool 2 User"] = pool_2_user
data.pool_2_user = pool_2_user
if quota:
data["Split"] = str(quota)
data.pool_split = str(quota)
return data

View File

@@ -1,19 +1,30 @@
import ipaddress
import logging
import json
import toml
from miners import BaseMiner
from API.bosminer import BOSMinerAPI
import toml
from config.bos import bos_config_convert, general_config_convert_bos
import logging
from API import APIError
from data import MinerData
from config.miner_config import MinerConfig
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
class BOSMiner(BaseMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
self.ip = ipaddress.ip_address(ip)
self.api = BOSMinerAPI(ip)
self.api_type = "BOSMiner"
self.uname = "root"
self.pwd = "admin"
self.config = None
async def send_ssh_command(self, cmd: str) -> str or None:
"""Send a command to the miner over ssh.
@@ -91,8 +102,9 @@ class BOSMiner(BaseMiner):
async with sftp.open("/etc/bosminer.toml") as file:
toml_data = toml.loads(await file.read())
logging.debug(f"{self}: Converting config file.")
cfg = bos_config_convert(toml_data)
cfg = MinerConfig().from_raw(toml_data)
self.config = cfg
return self.config
async def get_hostname(self) -> str:
"""Get miner hostname.
@@ -112,7 +124,7 @@ class BOSMiner(BaseMiner):
else:
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
except Exception as e:
except Exception:
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
@@ -127,7 +139,17 @@ class BOSMiner(BaseMiner):
return self.model + " (BOS)"
# get devdetails data
version_data = await self.api.devdetails()
try:
version_data = await self.api.devdetails()
except APIError as e:
version_data = None
if e.message == "Not ready":
cfg = json.loads(await self.send_ssh_command("bosminer config --data"))
model = cfg.get("data").get("format").get("model")
if model:
model = model.replace("Antminer ", "")
self.model = model
return self.model + " (BOS)"
# if we get data back, parse it for model
if version_data:
@@ -158,7 +180,7 @@ class BOSMiner(BaseMiner):
# if we get the version data, parse it
if version_data:
self.version = version_data.stdout.split("-")[5]
self.version = version_data.split("-")[5]
logging.debug(f"Found version for {self.ip}: {self.version}")
return self.version
@@ -171,11 +193,17 @@ class BOSMiner(BaseMiner):
logging.debug(f"{self}: Sending config.")
if ip_user:
suffix = str(self.ip).split(".")[-1]
toml_conf = toml.dumps(
general_config_convert_bos(yaml_config, user_suffix=suffix)
toml_conf = (
MinerConfig()
.from_yaml(yaml_config)
.as_bos(model=self.model.replace(" (BOS)", ""), user_suffix=suffix)
)
else:
toml_conf = toml.dumps(general_config_convert_bos(yaml_config))
toml_conf = (
MinerConfig()
.from_yaml(yaml_config)
.as_bos(model=self.model.replace(" (BOS)", ""))
)
async with (await self._get_ssh_connection()) as conn:
logging.debug(f"{self}: Opening SFTP connection.")
async with conn.start_sftp_client() as sftp:
@@ -197,10 +225,6 @@ class BOSMiner(BaseMiner):
offset = devs[0]["ID"]
for board in devs:
boards[board["ID"] - offset] = []
if not board["Chips"] == self.nominal_chips:
nominal = False
else:
nominal = True
if not board["Chips"] == self.nominal_chips:
nominal = False
else:
@@ -239,66 +263,73 @@ class BOSMiner(BaseMiner):
if not bad > 0:
return str(self.ip)
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Total": 0,
"Ideal": self.nominal_chips * 3,
"Left Board": 0,
"Center Board": 0,
"Right Board": 0,
"Nominal": False,
"Split": "0",
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
async def get_data(self) -> MinerData:
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
board_offset = -1
fan_offset = -1
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
if model:
data["Model"] = model
data.model = model
if hostname:
data["Hostname"] = hostname
data.hostname = hostname
if mac:
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
miner_data = await self.api.multicommand(
"summary", "temps", "tunerstatus", "pools", "devdetails"
)
try:
miner_data = await self.api.multicommand(
"summary", "temps", "tunerstatus", "pools", "devdetails", "fans"
)
except APIError as e:
if str(e.message) == "Not ready":
miner_data = await self.api.multicommand(
"summary", "tunerstatus", "pools", "fans"
)
if miner_data:
break
if not miner_data:
return data
summary = miner_data.get("summary")[0]
temps = miner_data.get("temps")[0]
tunerstatus = miner_data.get("tunerstatus")[0]
pools = miner_data.get("pools")[0]
devdetails = miner_data.get("devdetails")[0]
summary = miner_data.get("summary")
temps = miner_data.get("temps")
tunerstatus = miner_data.get("tunerstatus")
pools = miner_data.get("pools")
devdetails = miner_data.get("devdetails")
fans = miner_data.get("fans")
if summary:
hr = summary.get("SUMMARY")
hr = summary[0].get("SUMMARY")
if hr:
if len(hr) > 0:
hr = hr[0].get("MHS av")
hr = hr[0].get("MHS 1m")
if hr:
data["Hashrate"] = round(hr / 1000000, 2)
data.hashrate = round(hr / 1000000, 2)
if temps:
temp = temps.get("TEMPS")
temp = temps[0].get("TEMPS")
if temp:
if len(temp) > 0:
temp = temp[0].get("Chip")
if temp:
data["Temperature"] = round(temp)
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
offset = 6 if temp[0]["ID"] in [6, 7, 8] else temp[0]["ID"]
for board in temp:
_id = board["ID"] - offset
chip_temp = round(board["Chip"])
board_temp = round(board["Board"])
setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
setattr(data, f"{board_map[_id]}_temp", board_temp)
if fans:
fan_data = fans[0].get("FANS")
if fan_data:
for fan in range(self.fan_count):
setattr(data, f"fan_{fan+1}", fan_data[fan]["RPM"])
if pools:
pool_1 = None
@@ -308,7 +339,7 @@ class BOSMiner(BaseMiner):
pool_1_quota = 1
pool_2_quota = 1
quota = 0
for pool in pools.get("POOLS"):
for pool in pools[0].get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
@@ -326,47 +357,44 @@ class BOSMiner(BaseMiner):
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
pool_1 = pool_1.replace("stratum+tcp://", "")
pool_1 = pool_1.replace("stratum2+tcp://", "")
data["Pool 1"] = pool_1
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_1_url = pool_1
if pool_1_user:
data["Pool 1 User"] = pool_1_user
data["Pool User"] = pool_1_user
data.pool_1_user = pool_1_user
if pool_2:
pool_2 = pool_2.replace("stratum+tcp://", "")
pool_2 = pool_2.replace("stratum2+tcp://", "")
data["Pool 2"] = pool_2
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
if pool_2_user:
data["Pool 2 User"] = pool_2_user
data.pool_2_user = pool_2_user
if quota:
data["Split"] = str(quota)
data.pool_split = str(quota)
if tunerstatus:
tuner = tunerstatus.get("TUNERSTATUS")
tuner = tunerstatus[0].get("TUNERSTATUS")
if tuner:
if len(tuner) > 0:
wattage = tuner[0].get("PowerLimit")
if wattage:
data["Wattage"] = wattage
data.wattage = wattage
if devdetails:
boards = devdetails.get("DEVDETAILS")
boards = devdetails[0].get("DEVDETAILS")
if boards:
if len(boards) > 0:
board_map = {0: "Left Board", 1: "Center Board", 2: "Right Board"}
offset = boards[0]["ID"]
board_map = {0: "left_chips", 1: "center_chips", 2: "right_chips"}
offset = 6 if boards[0]["ID"] in [6, 7, 8] else boards[0]["ID"]
for board in boards:
id = board["ID"] - offset
_id = board["ID"] - offset
chips = board["Chips"]
data["Total"] += chips
data[board_map[id]] = chips
if data["Total"] == data["Ideal"]:
data["Nominal"] = True
setattr(data, board_map[_id], chips)
return data

View File

@@ -1,11 +1,10 @@
from miners import BaseMiner
from API.bosminer import BOSMinerAPI
import toml
from config.bos import bos_config_convert, general_config_convert_bos
import logging
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
import asyncssh
from API.bosminer import BOSMinerAPI
from miners import BaseMiner
class BOSMinerOld(BaseMiner):
def __init__(self, ip: str) -> None:

View File

@@ -1,14 +1,20 @@
import ipaddress
import logging
from API.btminer import BTMinerAPI
from miners import BaseMiner
from API import APIError
import logging
from data import MinerData
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
class BTMiner(BaseMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
self.ip = ipaddress.ip_address(ip)
self.api = BTMinerAPI(ip)
self.api_type = "BTMiner"
@@ -35,9 +41,9 @@ class BTMiner(BaseMiner):
self.hostname = host
return self.hostname
except APIError:
logging.warning(f"Failed to get hostname for miner: {self}")
logging.info(f"Failed to get hostname for miner: {self}")
return "?"
except Exception as e:
except Exception:
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
@@ -74,50 +80,50 @@ class BTMiner(BaseMiner):
async def get_mac(self):
mac = ""
data = await self.api.get_miner_info()
data = await self.api.summary()
if data:
if "Msg" in data.keys():
if "mac" in data["Msg"].keys():
mac = data["Msg"]["mac"]
if data.get("SUMMARY"):
if len(data["SUMMARY"]) > 0:
_mac = data["SUMMARY"][0].get("MAC")
if _mac:
mac = _mac
if mac == "":
try:
data = await self.api.get_miner_info()
if data:
if "Msg" in data.keys():
if "mac" in data["Msg"].keys():
mac = data["Msg"]["mac"]
except APIError:
pass
return str(mac).upper()
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Total": 0,
"Ideal": self.nominal_chips * 3,
"Left Board": 0,
"Center Board": 0,
"Right Board": 0,
"Nominal": False,
"Split": "0",
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
mac = None
try:
model = await self.get_model()
except APIError:
logging.info(f"Failed to get model: {self}")
model = None
data.model = "Whatsminer"
try:
hostname = await self.get_hostname()
except APIError:
logging.warning(f"Failed to get hostname and model: {self}")
model = None
data["Model"] = "Whatsminer"
logging.info(f"Failed to get hostname: {self}")
hostname = None
data["Hostname"] = "Whatsminer"
data.hostname = "Whatsminer"
if model:
data["Model"] = model
data.model = model
if hostname:
data["Hostname"] = hostname
data.hostname = hostname
miner_data = None
for i in range(DATA_RETRIES):
try:
@@ -138,41 +144,45 @@ class BTMiner(BaseMiner):
summary_data = summary.get("SUMMARY")
if summary_data:
if len(summary_data) > 0:
hr = summary_data[0].get("MHS av")
if summary_data[0].get("MAC"):
mac = summary_data[0]["MAC"]
data.fan_1 = summary_data[0]["Fan Speed In"]
data.fan_2 = summary_data[0]["Fan Speed Out"]
hr = summary_data[0].get("MHS 1m")
if hr:
data["Hashrate"] = round(hr / 1000000, 2)
data.hashrate = round(hr / 1000000, 2)
wattage = summary_data[0].get("Power")
if wattage:
data["Wattage"] = round(wattage)
data.wattage = round(wattage)
if devs:
temp_data = devs.get("DEVS")
if temp_data:
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
for board in temp_data:
temp = board.get("Chip Temp Avg")
if temp and not temp == 0.0:
data["Temperature"] = round(temp)
break
_id = board["ASC"]
chip_temp = round(board["Chip Temp Avg"])
board_temp = round(board["Temperature"])
setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
setattr(data, f"{board_map[_id]}_temp", board_temp)
if devs:
boards = devs.get("DEVS")
if boards:
if len(boards) > 0:
board_map = {0: "Left Board", 1: "Center Board", 2: "Right Board"}
board_map = {0: "left_chips", 1: "center_chips", 2: "right_chips"}
if "ID" in boards[0].keys():
id_key = "ID"
else:
id_key = "ASC"
offset = boards[0][id_key]
for board in boards:
id = board[id_key] - offset
_id = board[id_key] - offset
chips = board["Effective Chips"]
data["Total"] += chips
data[board_map[id]] = chips
if data["Total"] == data["Ideal"]:
data["Nominal"] = True
setattr(data, board_map[_id], chips)
if pools:
pool_1 = None
@@ -200,27 +210,34 @@ class BTMiner(BaseMiner):
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
if pool_1.startswith("stratum+tcp://"):
pool_1.replace("stratum+tcp://", "")
if pool_1.startswith("stratum2+tcp://"):
pool_1.replace("stratum2+tcp://", "")
data["Pool 1"] = pool_1
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_1_url = pool_1
if pool_1_user:
data["Pool 1 User"] = pool_1_user
data["Pool User"] = pool_1_user
data.pool_1_user = pool_1_user
if pool_2:
if pool_2.startswith("stratum+tcp://"):
pool_2.replace("stratum+tcp://", "")
if pool_2.startswith("stratum2+tcp://"):
pool_2.replace("stratum2+tcp://", "")
data["Pool 2"] = pool_2
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
if pool_2_user:
data["Pool 2 User"] = pool_2_user
data.pool_2_user = pool_2_user
if quota:
data["Split"] = str(quota)
data.pool_split = str(quota)
if not mac:
try:
mac = await self.get_mac()
except APIError:
logging.info(f"Failed to get mac: {self}")
mac = None
if mac:
data.mac = mac
return data

View File

@@ -1,18 +1,25 @@
from miners import BaseMiner
from API.cgminer import CGMinerAPI
from API import APIError
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
import ipaddress
import logging
from API.cgminer import CGMinerAPI
from miners import BaseMiner
from API import APIError
from data import MinerData
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
class CGMiner(BaseMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
self.ip = ipaddress.ip_address(ip)
self.api = CGMinerAPI(ip)
self.api_type = "CGMiner"
self.uname = "root"
self.pwd = "admin"
self.config = None
async def get_model(self):
if self.model:
@@ -102,29 +109,21 @@ class CGMiner(BaseMiner):
print(str(self.config))
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Split": 0,
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
if model:
data["Model"] = model
data.model = model
if hostname:
data["Hostname"] = hostname
data.hostname = hostname
if mac:
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
miner_data = await self.api.multicommand("summary", "pools", "stats")
@@ -142,18 +141,25 @@ class CGMiner(BaseMiner):
hr = summary.get("SUMMARY")
if hr:
if len(hr) > 0:
hr = hr[0].get("GHS av")
hr = hr[0].get("GHS 1m")
if hr:
data["Hashrate"] = round(hr / 1000, 2)
data.hashrate = round(hr / 1000, 2)
if stats:
temp = stats.get("STATS")
if temp:
if len(temp) > 1:
for item in ["temp2", "temp1", "temp3"]:
temperature = temp[1].get(item)
if temperature and not temperature == 0.0:
data["Temperature"] = round(temperature)
data.fan_1 = temp[1].get("fan1")
data.fan_2 = temp[1].get("fan2")
data.fan_3 = temp[1].get("fan3")
data.fan_4 = temp[1].get("fan4")
board_map = {1: "left_board", 2: "center_board", 3: "right_board"}
for item in range(1, 4):
board_temp = temp[1].get(f"temp{item}")
chip_temp = temp[1].get(f"temp2_{item}")
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
setattr(data, f"{board_map[item]}_temp", board_temp)
if pools:
pool_1 = None
@@ -181,27 +187,24 @@ class CGMiner(BaseMiner):
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
if pool_1.startswith("stratum+tcp://"):
pool_1.replace("stratum+tcp://", "")
if pool_1.startswith("stratum2+tcp://"):
pool_1.replace("stratum2+tcp://", "")
data["Pool 1"] = pool_1
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_1_url = pool_1
if pool_1_user:
data["Pool 1 User"] = pool_1_user
data["Pool User"] = pool_1_user
data.pool_1_user = pool_1_user
if pool_2:
if pool_2.startswith("stratum+tcp://"):
pool_2.replace("stratum+tcp://", "")
if pool_2.startswith("stratum2+tcp://"):
pool_2.replace("stratum2+tcp://", "")
data["Pool 2"] = pool_2
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
if pool_2_user:
data["Pool 2 User"] = pool_2_user
data.pool_2_user = pool_2_user
if quota:
data["Split"] = str(quota)
data.pool_split = str(quota)
return data

View File

@@ -1,10 +1,11 @@
from miners._backends import BMMiner
import ipaddress
class Hiveon(BMMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
self.ip = ipaddress.ip_address(ip)
self.api_type = "Hiveon"
self.uname = "root"
self.pwd = "admin"

View File

@@ -7,3 +7,4 @@ class S17(BaseMiner):
self.ip = ip
self.model = "S17"
self.nominal_chips = 48
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class S17Plus(BaseMiner):
self.ip = ip
self.model = "S17+"
self.nominal_chips = 65
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class S17Pro(BaseMiner):
self.ip = ip
self.model = "S17 Pro"
self.nominal_chips = 48
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class S17e(BaseMiner):
self.ip = ip
self.model = "S17e"
self.nominal_chips = 135
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class T17(BaseMiner):
self.ip = ip
self.model = "T17"
self.nominal_chips = 30
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class T17Plus(BaseMiner):
self.ip = ip
self.model = "T17+"
self.nominal_chips = 44
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class T17e(BaseMiner):
self.ip = ip
self.model = "T17e"
self.nominal_chips = 78
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class S19(BaseMiner):
self.ip = ip
self.model = "S19"
self.nominal_chips = 76
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class S19Pro(BaseMiner):
self.ip = ip
self.model = "S19 Pro"
self.nominal_chips = 114
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class S19a(BaseMiner):
self.ip = ip
self.model = "S19a"
self.nominal_chips = 72
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class S19j(BaseMiner):
self.ip = ip
self.model = "S19j"
self.nominal_chips = 114
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class S19jPro(BaseMiner):
self.ip = ip
self.model = "S19j Pro"
self.nominal_chips = 126
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class T19(BaseMiner):
self.ip = ip
self.model = "T19"
self.nominal_chips = 76
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class S9(BaseMiner):
self.ip = ip
self.model = "S9"
self.nominal_chips = 63
self.fan_count = 2

View File

@@ -0,0 +1,10 @@
from miners import BaseMiner
class S9i(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "S9i"
self.nominal_chips = 63
self.fan_count = 2

View File

@@ -7,3 +7,4 @@ class T9(BaseMiner):
self.ip = ip
self.model = "T9"
self.nominal_chips = 57
self.fan_count = 2

View File

@@ -1,2 +1,3 @@
from .S9 import S9
from .S9i import S9i
from .T9 import T9

View File

@@ -7,3 +7,4 @@ class Avalon1047(BaseMiner):
self.ip = ip
self.model = "Avalon 1047"
self.nominal_chips = 114
self.fan_count = 4

View File

@@ -7,3 +7,4 @@ class Avalon1066(BaseMiner):
self.ip = ip
self.model = "Avalon 1066"
self.nominal_chips = 114
self.fan_count = 4

View File

@@ -6,3 +6,4 @@ class Avalon821(BaseMiner):
super().__init__()
self.ip = ip
self.model = "Avalon 821"
self.fan_count = 2

View File

@@ -6,3 +6,4 @@ class Avalon841(BaseMiner):
super().__init__()
self.ip = ip
self.model = "Avalon 841"
self.fan_count = 2

View File

@@ -7,3 +7,4 @@ class M20S(BaseMiner):
self.ip = ip
self.model = "M20S"
self.nominal_chips = 66
self.fan_count = 2

View File

@@ -7,3 +7,4 @@ class M20SPlus(BaseMiner):
self.ip = ip
self.model = "M20S+"
self.nominal_chips = 66
self.fan_count = 2

View File

@@ -7,3 +7,4 @@ class M21(BaseMiner):
self.ip = ip
self.model = "M21"
self.nominal_chips = 105
self.fan_count = 2

View File

@@ -6,4 +6,23 @@ class M21S(BaseMiner):
super().__init__()
self.ip = ip
self.model = "M21S"
self.nominal_chips = 66
self.fan_count = 2
class M21SV60(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M21S V60"
self.nominal_chips = 105
self.fan_count = 2
class M21SV20(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M21S V20"
self.nominal_chips = 66
self.fan_count = 2

View File

@@ -7,3 +7,4 @@ class M21SPlus(BaseMiner):
self.ip = ip
self.model = "M21S+"
self.nominal_chips = 105
self.fan_count = 2

View File

@@ -2,5 +2,5 @@ from .M20S import M20S
from .M20S_Plus import M20SPlus
from .M21 import M21
from .M21S import M21S
from .M21S import M21S, M21SV20, M21SV60
from .M21S_Plus import M21SPlus

View File

@@ -6,3 +6,41 @@ class M30S(BaseMiner):
super().__init__()
self.ip = ip
self.model = "M30S"
self.nominal_chips = 148
self.fan_count = 2
class M30SV50(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S V50"
self.nominal_chips = 156
self.fan_count = 2
class M30SVG20(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VG20"
self.nominal_chips = 70
self.fan_count = 2
class M30SVE20(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VE20"
self.nominal_chips = 111
self.fan_count = 2
class M30SVE10(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S VE10"
self.nominal_chips = 105
self.fan_count = 2

View File

@@ -7,3 +7,31 @@ class M30SPlus(BaseMiner):
self.ip = ip
self.model = "M30S+"
self.nominal_chips = 156
self.fan_count = 2
class M30SPlusVG60(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VG60"
self.nominal_chips = 86
self.fan_count = 2
class M30SPlusVE40(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VE40"
self.nominal_chips = 156
self.fan_count = 2
class M30SPlusVF20(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S+ VF20"
self.nominal_chips = 111
self.fan_count = 2

View File

@@ -6,7 +6,23 @@ class M30SPlusPlus(BaseMiner):
super().__init__()
self.ip = ip
self.model = "M30S++"
self.nominal_chips = 111
self.fan_count = 2
class M30SPlusPlusVG30(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ V30"
self.nominal_chips = 111
self.fan_count = 2
class M30SPlusPlusVG40(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ V40"
self.nominal_chips = 117
# TODO: handle different chip counts, 111, 117,(128)
self.fan_count = 2

View File

@@ -6,3 +6,5 @@ class M31S(BaseMiner):
super().__init__()
self.ip = ip
self.model = "M31S"
# TODO: Add chip count for this miner (per board) - self.nominal_chips
self.fan_count = 2

View File

@@ -6,3 +6,14 @@ class M31SPlus(BaseMiner):
super().__init__()
self.ip = ip
self.model = "M31S+"
self.nominal_chips = 78
self.fan_count = 2
class M31SPlusVE20(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S+ VE20"
self.nominal_chips = 78
self.fan_count = 2

View File

@@ -7,3 +7,4 @@ class M32S(BaseMiner):
self.ip = ip
self.model = "M32S"
self.nominal_chips = 78
self.fan_count = 2

View File

@@ -1,8 +1,8 @@
from .M30S import M30S
from .M30S_Plus import M30SPlus
from .M30S_Plus_Plus import M30SPlusPlus
from .M30S import M30S, M30SVE10, M30SVE20, M30SVG20, M30SV50
from .M30S_Plus import M30SPlus, M30SPlusVG60, M30SPlusVE40, M30SPlusVF20
from .M30S_Plus_Plus import M30SPlusPlus, M30SPlusPlusVG30, M30SPlusPlusVG40
from .M31S import M31S
from .M31S_Plus import M31SPlus
from .M31S_Plus import M31SPlus, M31SPlusVE20
from .M32S import M32S

View File

@@ -1,8 +1,87 @@
from miners._backends import BMMiner
from miners._types import S17
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S17 # noqa - Ignore access to _module
import httpx
# TODO add config
class BMMinerS17(BMMiner, S17):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
try:
await client.post(url, data={"action": "startBlink"}, auth=auth)
except httpx.ReadTimeout:
# Expected behaviour
pass
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
await client.post(url, data={"action": "stopBlink"}, auth=auth)
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if not data["isBlinking"]:
return True
return False
async def check_light(self):
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,84 @@
from miners._backends import BMMiner
from miners._types import S17Plus
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S17Plus # noqa - Ignore access to _module
import httpx
class BMMinerS17Plus(BMMiner, S17Plus):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
try:
await client.post(url, data={"action": "startBlink"}, auth=auth)
except httpx.ReadTimeout:
# Expected behaviour
pass
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
await client.post(url, data={"action": "stopBlink"}, auth=auth)
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if not data["isBlinking"]:
return True
return False
async def check_light(self):
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,84 @@
from miners._backends import BMMiner
from miners._types import S17Pro
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S17Pro # noqa - Ignore access to _module
import httpx
class BMMinerS17Pro(BMMiner, S17Pro):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
try:
await client.post(url, data={"action": "startBlink"}, auth=auth)
except httpx.ReadTimeout:
# Expected behaviour
pass
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
await client.post(url, data={"action": "stopBlink"}, auth=auth)
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if not data["isBlinking"]:
return True
return False
async def check_light(self):
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,84 @@
from miners._backends import BMMiner
from miners._types import S17e
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S17e # noqa - Ignore access to _module
import httpx
class BMMinerS17e(BMMiner, S17e):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
try:
await client.post(url, data={"action": "startBlink"}, auth=auth)
except httpx.ReadTimeout:
# Expected behaviour
pass
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
await client.post(url, data={"action": "stopBlink"}, auth=auth)
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if not data["isBlinking"]:
return True
return False
async def check_light(self):
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,84 @@
from miners._backends import BMMiner
from miners._types import T17
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import T17 # noqa - Ignore access to _module
import httpx
class BMMinerT17(BMMiner, T17):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
try:
await client.post(url, data={"action": "startBlink"}, auth=auth)
except httpx.ReadTimeout:
# Expected behaviour
pass
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
await client.post(url, data={"action": "stopBlink"}, auth=auth)
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if not data["isBlinking"]:
return True
return False
async def check_light(self):
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,84 @@
from miners._backends import BMMiner
from miners._types import T17Plus
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import T17Plus # noqa - Ignore access to _module
import httpx
class BMMinerT17Plus(BMMiner, T17Plus):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
try:
await client.post(url, data={"action": "startBlink"}, auth=auth)
except httpx.ReadTimeout:
# Expected behaviour
pass
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
await client.post(url, data={"action": "stopBlink"}, auth=auth)
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if not data["isBlinking"]:
return True
return False
async def check_light(self):
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,84 @@
from miners._backends import BMMiner
from miners._types import T17e
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import T17e # noqa - Ignore access to _module
import httpx
class BMMinerT17e(BMMiner, T17e):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
try:
await client.post(url, data={"action": "startBlink"}, auth=auth)
except httpx.ReadTimeout:
# Expected behaviour
pass
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
await client.post(url, data={"action": "stopBlink"}, auth=auth)
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if not data["isBlinking"]:
return True
return False
async def check_light(self):
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.post(url, data={"action": "onPageLoaded"}, auth=auth)
if data.status_code == 200:
data = data.json()
if data["isBlinking"]:
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,103 @@
from miners._backends import BMMiner
from miners._types import S19
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S19 # noqa - Ignore access to _module
from config.miner_config import MinerConfig
import httpx
import json
import asyncio
class BMMinerS19(BMMiner, S19):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_config(self) -> MinerConfig:
url = f"http://{self.ip}/cgi-bin/get_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
self.config = MinerConfig().from_raw(data)
return self.config
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_x19()
try:
async with httpx.AsyncClient() as client:
await client.post(url, data=conf, auth=auth)
except httpx.ReadTimeout:
pass
for i in range(7):
data = await self.get_config()
if data.as_x19() == conf:
break
await asyncio.sleep(1)
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "true"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B000":
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "false"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B100":
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,103 @@
from miners._backends import BMMiner
from miners._types import S19Pro
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S19Pro # noqa - Ignore access to _module
from config.miner_config import MinerConfig
import httpx
import json
import asyncio
class BMMinerS19Pro(BMMiner, S19Pro):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_config(self) -> MinerConfig:
url = f"http://{self.ip}/cgi-bin/get_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
self.config = MinerConfig().from_raw(data)
return self.config
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_x19()
try:
async with httpx.AsyncClient() as client:
await client.post(url, data=conf, auth=auth)
except httpx.ReadTimeout:
pass
for i in range(7):
data = await self.get_config()
if data.as_x19() == conf:
break
await asyncio.sleep(1)
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "true"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B000":
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "false"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B100":
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,103 @@
from miners._backends import BMMiner
from miners._types import S19a
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S19a # noqa - Ignore access to _module
from config.miner_config import MinerConfig
import httpx
import json
import asyncio
class BMMinerS19a(BMMiner, S19a):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_config(self) -> MinerConfig:
url = f"http://{self.ip}/cgi-bin/get_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
self.config = MinerConfig().from_raw(data)
return self.config
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_x19()
try:
async with httpx.AsyncClient() as client:
await client.post(url, data=conf, auth=auth)
except httpx.ReadTimeout:
pass
for i in range(7):
data = await self.get_config()
if data.as_x19() == conf:
break
await asyncio.sleep(1)
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "true"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B000":
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "false"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B100":
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,103 @@
from miners._backends import BMMiner
from miners._types import S19j
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S19j # noqa - Ignore access to _module
from config.miner_config import MinerConfig
import httpx
import json
import asyncio
class BMMinerS19j(BMMiner, S19j):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_config(self) -> MinerConfig:
url = f"http://{self.ip}/cgi-bin/get_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
self.config = MinerConfig().from_raw(data)
return self.config
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_x19()
try:
async with httpx.AsyncClient() as client:
await client.post(url, data=conf, auth=auth)
except httpx.ReadTimeout:
pass
for i in range(7):
data = await self.get_config()
if data.as_x19() == conf:
break
await asyncio.sleep(1)
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "true"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B000":
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "false"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B100":
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,103 @@
from miners._backends import BMMiner
from miners._types import S19jPro
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S19jPro # noqa - Ignore access to _module
from config.miner_config import MinerConfig
import httpx
import json
import asyncio
class BMMinerS19jPro(BMMiner, S19jPro):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_config(self) -> MinerConfig:
url = f"http://{self.ip}/cgi-bin/get_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
self.config = MinerConfig().from_raw(data)
return self.config
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_x19()
try:
async with httpx.AsyncClient() as client:
await client.post(url, data=conf, auth=auth)
except httpx.ReadTimeout:
pass
for i in range(7):
data = await self.get_config()
if data.as_x19() == conf:
break
await asyncio.sleep(1)
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "true"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B000":
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "false"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B100":
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,8 +1,103 @@
from miners._backends import BMMiner
from miners._types import T19
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import T19 # noqa - Ignore access to _module
from config.miner_config import MinerConfig
import httpx
import json
import asyncio
class BMMinerT19(BMMiner, T19):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_config(self) -> MinerConfig:
url = f"http://{self.ip}/cgi-bin/get_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
self.config = MinerConfig().from_raw(data)
return self.config
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
auth = httpx.DigestAuth("root", "root")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_x19()
try:
async with httpx.AsyncClient() as client:
await client.post(url, data=conf, auth=auth)
except httpx.ReadTimeout:
pass
for i in range(7):
data = await self.get_config()
if data.as_x19() == conf:
break
await asyncio.sleep(1)
async def get_hostname(self) -> str or None:
hostname = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "hostname" in data.keys():
hostname = data["hostname"]
return hostname
async def get_mac(self):
mac = None
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if len(data.keys()) > 0:
if "macaddr" in data.keys():
mac = data["macaddr"]
return mac
async def fault_light_on(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "true"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B000":
return True
return False
async def fault_light_off(self) -> bool:
url = f"http://{self.ip}/cgi-bin/blink.cgi"
auth = httpx.DigestAuth("root", "root")
data = json.dumps({"blink": "false"})
async with httpx.AsyncClient() as client:
data = await client.post(url, data=data, auth=auth)
if data.status_code == 200:
data = data.json()
if data.get("code") == "B100":
return True
return False
async def reboot(self) -> bool:
url = f"http://{self.ip}/cgi-bin/reboot.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
return True
return False

View File

@@ -1,5 +1,5 @@
from miners._backends import BMMiner
from miners._types import S9
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S9 # noqa - Ignore access to _module
class BMMinerS9(BMMiner, S9):

View File

@@ -0,0 +1,8 @@
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import S9i # noqa - Ignore access to _module
class BMMinerS9i(BMMiner, S9i):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -1,5 +1,5 @@
from miners._backends import BMMiner
from miners._types import T9
from miners._backends import BMMiner # noqa - Ignore access to _module
from miners._types import T9 # noqa - Ignore access to _module
class BMMinerT9(BMMiner, T9):

View File

@@ -1,2 +1,3 @@
from .S9 import BMMinerS9
from .S9i import BMMinerS9i
from .T9 import BMMinerT9

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import S17
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import S17 # noqa - Ignore access to _module
class BOSMinerS17(BOSMiner, S17):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import S17Plus
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import S17Plus # noqa - Ignore access to _module
class BOSMinerS17Plus(BOSMiner, S17Plus):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import S17Pro
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import S17Pro # noqa - Ignore access to _module
class BOSMinerS17Pro(BOSMiner, S17Pro):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import S17e
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import S17e # noqa - Ignore access to _module
class BOSMinerS17e(BOSMiner, S17e):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import T17
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import T17 # noqa - Ignore access to _module
class BOSMinerT17(BOSMiner, T17):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import T17Plus
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import T17Plus # noqa - Ignore access to _module
class BOSMinerT17Plus(BOSMiner, T17Plus):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import T17e
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import T17e # noqa - Ignore access to _module
class BOSMinerT17e(BOSMiner, T17e):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import S19
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import S19 # noqa - Ignore access to _module
class BOSMinerS19(BOSMiner, S19):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import S19Pro
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import S19Pro # noqa - Ignore access to _module
class BOSMinerS19Pro(BOSMiner, S19Pro):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import S19j
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import S19j # noqa - Ignore access to _module
class BOSMinerS19j(BOSMiner, S19j):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import S19jPro
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import S19jPro # noqa - Ignore access to _module
class BOSMinerS19jPro(BOSMiner, S19jPro):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import T19
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import T19 # noqa - Ignore access to _module
class BOSMinerT19(BOSMiner, T19):

View File

@@ -1,5 +1,5 @@
from miners._backends import BOSMiner
from miners._types import S9
from miners._backends import BOSMiner # noqa - Ignore access to _module
from miners._types import S9 # noqa - Ignore access to _module
class BOSMinerS9(BOSMiner, S9):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import S17
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import S17 # noqa - Ignore access to _module
class CGMinerS17(CGMiner, S17):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import S17Plus
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import S17Plus # noqa - Ignore access to _module
class CGMinerS17Plus(CGMiner, S17Plus):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import S17Pro
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import S17Pro # noqa - Ignore access to _module
class CGMinerS17Pro(CGMiner, S17Pro):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import S17e
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import S17e # noqa - Ignore access to _module
class CGMinerS17e(CGMiner, S17e):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import T17
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import T17 # noqa - Ignore access to _module
class CGMinerT17(CGMiner, T17):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import T17Plus
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import T17Plus # noqa - Ignore access to _module
class CGMinerT17Plus(CGMiner, T17Plus):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import T17e
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import T17e # noqa - Ignore access to _module
class CGMinerT17e(CGMiner, T17e):

View File

@@ -1,8 +1,11 @@
from miners._backends import CGMiner
from miners._types import S19
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import S19 # noqa - Ignore access to _module
class CGMinerS19(CGMiner, S19):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str:
return "?"

View File

@@ -1,8 +1,11 @@
from miners._backends import CGMiner
from miners._types import S19Pro
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import S19Pro # noqa - Ignore access to _module
class CGMinerS19Pro(CGMiner, S19Pro):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str:
return "?"

View File

@@ -1,8 +1,11 @@
from miners._backends import CGMiner
from miners._types import S19j
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import S19j # noqa - Ignore access to _module
class CGMinerS19j(CGMiner, S19j):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str:
return "?"

View File

@@ -1,8 +1,11 @@
from miners._backends import CGMiner
from miners._types import S19jPro
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import S19jPro # noqa - Ignore access to _module
class CGMinerS19jPro(CGMiner, S19jPro):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str:
return "?"

View File

@@ -1,8 +1,11 @@
from miners._backends import CGMiner
from miners._types import T19
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import T19 # noqa - Ignore access to _module
class CGMinerT19(CGMiner, T19):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
async def get_hostname(self) -> str:
return "?"

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import S9
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import S9 # noqa - Ignore access to _module
class CGMinerS9(CGMiner, S9):

View File

@@ -1,11 +1,9 @@
from miners._backends.cgminer import CGMiner
from miners._backends.cgminer import CGMiner # noqa - Ignore access to _module
from miners._types.antminer import T9 # noqa - Ignore access to _module
class CGMinerT9(CGMiner):
class CGMinerT9(CGMiner, T9):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.model = "T9"
self.api_type = "CGMiner"
def __repr__(self) -> str:
return f"CGMinerT9: {str(self.ip)}"

View File

@@ -1,5 +1,5 @@
from miners._backends import Hiveon
from miners._types import T9
from miners._backends import Hiveon # noqa - Ignore access to _module
from miners._types import T9 # noqa - Ignore access to _module
class HiveonT9(Hiveon, T9):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import Avalon1047
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import Avalon1047 # noqa - Ignore access to _module
class CGMinerAvalon1047(CGMiner, Avalon1047):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import Avalon1066
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import Avalon1066 # noqa - Ignore access to _module
class CGMinerAvalon1066(CGMiner, Avalon1066):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import Avalon821
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import Avalon821 # noqa - Ignore access to _module
class CGMinerAvalon821(CGMiner, Avalon821):

View File

@@ -1,5 +1,5 @@
from miners._backends import CGMiner
from miners._types import Avalon841
from miners._backends import CGMiner # noqa - Ignore access to _module
from miners._types import Avalon841 # noqa - Ignore access to _module
class CGMinerAvalon841(CGMiner, Avalon841):

View File

@@ -1,12 +1,18 @@
from typing import TypeVar, Tuple, List
from collections.abc import AsyncIterable
from miners import BaseMiner
from miners.antminer import *
from miners.whatsminer import *
from miners.avalonminer import *
from miners._backends.cgminer import CGMiner
from miners._backends.bmminer import BMMiner
from miners._backends.bosminer import BOSMiner
from miners._backends.btminer import BTMiner
from miners._backends.bosminer_old import BOSMinerOld
from miners._backends.cgminer import CGMiner # noqa - Ignore _module import
from miners._backends.bmminer import BMMiner # noqa - Ignore _module import
from miners._backends.bosminer import BOSMiner # noqa - Ignore _module import
from miners._backends.btminer import BTMiner # noqa - Ignore _module import
from miners._backends.bosminer_old import BOSMinerOld # noqa - Ignore _module import
from miners.unknown import UnknownMiner
@@ -22,6 +28,10 @@ from settings import (
NETWORK_PING_TIMEOUT as PING_TIMEOUT,
)
import asyncssh
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
MINER_CLASSES = {
"Antminer S9": {
"Default": BOSMinerS9,
@@ -30,6 +40,10 @@ MINER_CLASSES = {
"BMMiner": BMMinerS9,
"CGMiner": CGMinerS9,
},
"Antminer S9i": {
"Default": BMMinerS9i,
"BMMiner": BMMinerS9i,
},
"Antminer S17": {
"Default": BMMinerS17,
"BOSMiner+": BOSMinerS17,
@@ -121,6 +135,8 @@ MINER_CLASSES = {
"M21S": {
"Default": BTMinerM21S,
"BTMiner": BTMinerM21S,
"60": BTMinerM21SV60,
"20": BTMinerM21SV20,
},
"M21S+": {
"Default": BTMinerM21SPlus,
@@ -129,14 +145,23 @@ MINER_CLASSES = {
"M30S": {
"Default": BTMinerM30S,
"BTMiner": BTMinerM30S,
"50": BTMinerM30SV50,
"G20": BTMinerM30SVG20,
"E20": BTMinerM30SVE20,
"E10": BTMinerM30SVE10,
},
"M30S+": {
"Default": BTMinerM30SPlus,
"BTMiner": BTMinerM30SPlus,
"F20": BTMinerM30SPlusVF20,
"E40": BTMinerM30SPlusVE40,
"G60": BTMinerM30SPlusVG60,
},
"M30S++": {
"Default": BTMinerM30SPlusPlus,
"BTMiner": BTMinerM30SPlusPlus,
"G40": BTMinerM30SPlusPlusVG40,
"G30": BTMinerM30SPlusPlusVG30,
},
"M31S": {
"Default": BTMinerM31S,
@@ -145,6 +170,7 @@ MINER_CLASSES = {
"M31S+": {
"Default": BTMinerM31SPlus,
"BTMiner": BTMinerM31SPlus,
"E20": BTMinerM31SPlusVE20,
},
"M32S": {
"Default": BTMinerM32S,
@@ -163,10 +189,12 @@ class Singleton(type):
class MinerFactory(metaclass=Singleton):
def __init__(self):
def __init__(self) -> None:
self.miners = {}
async def get_miner_generator(self, ips: list):
async def get_miner_generator(
self, ips: List[ipaddress.ip_address or str]
) -> AsyncIterable[AnyMiner]:
"""
Get Miner objects from ip addresses using an async generator.
@@ -188,7 +216,7 @@ class MinerFactory(metaclass=Singleton):
for miner in scanned:
yield await miner
async def get_miner(self, ip: ipaddress.ip_address or str):
async def get_miner(self, ip: ipaddress.ip_address or str) -> AnyMiner:
"""Decide a miner type using the IP address of the miner."""
if isinstance(ip, str):
ip = ipaddress.ip_address(ip)
@@ -199,12 +227,13 @@ class MinerFactory(metaclass=Singleton):
miner = UnknownMiner(str(ip))
api = None
model = None
ver = None
# try to get the API multiple times based on retries
for i in range(GET_VERSION_RETRIES):
try:
# get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None
new_model, new_api = await asyncio.wait_for(
new_model, new_api, new_ver = await asyncio.wait_for(
self._get_miner_type(ip), timeout=PING_TIMEOUT
)
@@ -213,8 +242,10 @@ class MinerFactory(metaclass=Singleton):
api = new_api
if new_model and not model:
model = new_model
if new_ver and not ver:
ver = new_ver
# if we find the API and model, dont need to loop anymore
# if we find the API and model, don't need to loop anymore
if api and model:
break
except asyncio.TimeoutError:
@@ -237,6 +268,9 @@ class MinerFactory(metaclass=Singleton):
return miner
if api not in MINER_CLASSES[model].keys():
api = "Default"
if ver in MINER_CLASSES[model].keys():
miner = MINER_CLASSES[model][ver](str(ip))
return miner
miner = MINER_CLASSES[model][api](str(ip))
# if we cant find a model, check if we found the API
@@ -255,55 +289,73 @@ class MinerFactory(metaclass=Singleton):
elif "BMMiner" in api:
miner = BMMiner(str(ip))
# save the miner to the cache at its IP
self.miners[ip] = miner
# save the miner to the cache at its IP if its not unknown
if not isinstance(miner, UnknownMiner):
self.miners[ip] = miner
# return the miner
return miner
def clear_cached_miners(self):
def clear_cached_miners(self) -> None:
"""Clear the miner factory cache."""
# empty out self.miners
self.miners = {}
async def _get_miner_type(self, ip: ipaddress.ip_address or str) -> tuple:
async def _get_miner_type(
self, ip: ipaddress.ip_address or str
) -> Tuple[str or None, str or None, str or None]:
model = None
api = None
ver = None
devdetails = None
version = None
try:
# get device details and version data
data = await self._send_api_command(str(ip), "devdetails+version")
# validate success
validation = await self._validate_command(data)
if not validation[0]:
raise APIError(validation[1])
# copy each part of the main command to devdetails and version
devdetails = data["devdetails"][0]
version = data["version"][0]
except APIError as e:
except APIError:
# if getting data fails we need to check again
data = None
# if data is None then get it a slightly different way
if not data:
try:
# try devdetails and version separately (X19s mainly require this)
# get devdetails and validate
devdetails = await self._send_api_command(str(ip), "devdetails")
validation = await self._validate_command(devdetails)
if not validation[0]:
# if devdetails fails try version instead
devdetails = None
# get version and validate
version = await self._send_api_command(str(ip), "version")
validation = await self._validate_command(version)
if not validation[0]:
# finally try get_version (Whatsminers) and validate
version = await self._send_api_command(str(ip), "get_version")
validation = await self._validate_command(version)
# if this fails we raise an error to be caught below
if not validation[0]:
raise APIError(validation[1])
except APIError as e:
# catch APIError and let the factory know we cant get data
logging.warning(f"{ip}: API Command Error: {e}")
return None, None
return None, None, None
# if we have devdetails, we can get model data from there
if devdetails:
if "DEVDETAILS" in devdetails.keys() and not devdetails["DEVDETAILS"] == []:
# check for model, for most miners
@@ -319,6 +371,7 @@ class MinerFactory(metaclass=Singleton):
if "s9" in devdetails["STATUS"][0]["Description"]:
model = "Antminer S9"
# if we have version we can get API type from here
if version:
if "VERSION" in version.keys():
# check if there are any BMMiner strings in any of the dict keys
@@ -331,36 +384,73 @@ class MinerFactory(metaclass=Singleton):
):
api = "CGMiner"
elif any(
"BTMiner" in string for string in version["VERSION"][0].keys()
):
api = "BTMiner"
# check if there are any BOSMiner strings in any of the dict keys
elif any(
"BOSminer" in string for string in version["VERSION"][0].keys()
):
api = "BOSMiner"
if "plus" in version["VERSION"][0]["BOSminer"]:
if version["VERSION"][0].get("BOSminer"):
if "plus" in version["VERSION"][0]["BOSminer"]:
api = "BOSMiner+"
if "BOSminer+" in version["VERSION"][0].keys():
api = "BOSMiner+"
# if all that fails, check the Description to see if it is a whatsminer
if version.get("Description") and "whatsminer" in version.get(
"Description"
if version.get("Description") and (
"whatsminer" in version.get("Description")
):
api = "BTMiner"
# if we have no model from devdetails but have version, try to get it from there
if version and not model:
# make sure version isn't blank
if (
"VERSION" in version.keys()
and version.get("VERSION")
and not version.get("VERSION") == []
):
model = version["VERSION"][0]["Type"]
# try to get "Type" which is model
if version["VERSION"][0].get("Type"):
model = version["VERSION"][0]["Type"]
# braiins OS bug check just in case
elif "am2-s17" in version["STATUS"][0]["Description"]:
model = "Antminer S17"
# final try on a braiins OS bug with devdetails not returning
else:
async with asyncssh.connect(
str(ip),
known_hosts=None,
username="root",
password="admin",
server_host_key_algs=["ssh-rsa"],
) as conn:
cfg = await conn.run("bosminer config --data")
if cfg:
cfg = json.loads(cfg.stdout)
model = cfg.get("data").get("format").get("model")
if model:
# whatsminer have a V in their version string (M20SV41), remove everything after it
if "V" in model:
model = model.split("V")[0]
_ver = model.split("V")
if len(_ver) > 1:
ver = model.split("V")[1]
model = model.split("V")[0]
# don't need "Bitmain", just "Antminer XX" as model
if "Bitmain " in model:
model = model.replace("Bitmain ", "")
return model, api, ver
return model, api
async def _validate_command(self, data: dict) -> tuple:
@staticmethod
async def _validate_command(data: dict) -> Tuple[bool, str or None]:
"""Check if the returned command output is correctly formatted."""
# check if the data returned is correct or an error
if not data:
@@ -386,7 +476,8 @@ class MinerFactory(metaclass=Singleton):
return False, data["STATUS"][0]["Msg"]
return True, None
async def _send_api_command(self, ip: ipaddress.ip_address or str, command: str):
@staticmethod
async def _send_api_command(ip: ipaddress.ip_address or str, command: str) -> dict:
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(ip), 4028)
@@ -426,12 +517,12 @@ class MinerFactory(metaclass=Singleton):
str_data = str_data.replace(",}", "}")
# fix an error with a btminer return having a newline that breaks json.loads()
str_data = str_data.replace("\n", "")
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
# fix an error with a bmminer return missing a specific comma that breaks json.loads()
str_data = str_data.replace("}{", "},{")
# parse the json
data = json.loads(str_data)
# handle bad json
except json.decoder.JSONDecodeError as e:
except json.decoder.JSONDecodeError:
# raise APIError(f"Decode Error: {data}")
data = None

Some files were not shown because too many files have changed in this diff Show More