Compare commits

..

8 Commits

Author SHA1 Message Date
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
37 changed files with 361 additions and 55 deletions

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

@@ -7,6 +7,7 @@ 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.
@@ -34,6 +35,7 @@ class MinerData:
ip: str
datetime: datetime = None
mac: str = "00:00:00:00:00:00"
model: str = "Unknown"
hostname: str = "Unknown"
hashrate: float = 0

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

@@ -135,6 +135,7 @@ class BMMiner(BaseMiner):
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
if model:
data.model = model
@@ -142,6 +143,9 @@ class BMMiner(BaseMiner):
if hostname:
data.hostname = hostname
if mac:
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
miner_data = await self.api.multicommand(

View File

@@ -264,6 +264,7 @@ class BOSMiner(BaseMiner):
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
if model:
data.model = model
@@ -271,6 +272,9 @@ class BOSMiner(BaseMiner):
if hostname:
data.hostname = hostname
if mac:
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
try:

View File

@@ -41,7 +41,7 @@ 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:
logging.warning(f"Failed to get hostname for miner: {self}")
@@ -80,23 +80,41 @@ 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 = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
mac = None
try:
model = await self.get_model()
hostname = await self.get_hostname()
except APIError:
logging.warning(f"Failed to get hostname and model: {self}")
logging.info(f"Failed to get model: {self}")
model = None
data.model = "Whatsminer"
try:
hostname = await self.get_hostname()
except APIError:
logging.info(f"Failed to get hostname: {self}")
hostname = None
data.hostname = "Whatsminer"
@@ -105,6 +123,7 @@ class BTMiner(BaseMiner):
if hostname:
data.hostname = hostname
miner_data = None
for i in range(DATA_RETRIES):
try:
@@ -125,6 +144,9 @@ class BTMiner(BaseMiner):
summary_data = summary.get("SUMMARY")
if summary_data:
if len(summary_data) > 0:
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"]
@@ -208,4 +230,14 @@ class BTMiner(BaseMiner):
if 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

@@ -113,12 +113,17 @@ class CGMiner(BaseMiner):
model = await self.get_model()
hostname = await self.get_hostname()
mac = await self.get_mac()
if model:
data.model = model
if 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")

View File

@@ -1,10 +1,19 @@
from miners import BaseMiner
class M21S(BaseMiner):
class M21SV60(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M21S"
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

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

View File

@@ -8,3 +8,12 @@ class M30S(BaseMiner):
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

View File

@@ -8,3 +8,12 @@ class M30SPlus(BaseMiner):
self.model = "M30S+"
self.nominal_chips = 156
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

View File

@@ -5,7 +5,7 @@ class M30SPlusPlusVG30(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++V30"
self.model = "M30S++ V30"
self.nominal_chips = 111
self.fan_count = 2
@@ -14,6 +14,6 @@ class M30SPlusPlusVG40(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++V40"
self.model = "M30S++ V40"
self.nominal_chips = 117
self.fan_count = 2

View File

@@ -8,3 +8,12 @@ class M31SPlus(BaseMiner):
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

@@ -1,8 +1,8 @@
from .M30S import M30S
from .M30S_Plus import M30SPlus
from .M30S import M30S, M30SV50
from .M30S_Plus import M30SPlus, M30SPlusVE40
from .M30S_Plus_Plus import M30SPlusPlusVG30, M30SPlusPlusVG40
from .M31S import M31S
from .M31S_Plus import M31SPlus
from .M31S_Plus import M31SPlus, M31SPlusVE20
from .M32S import M32S

View File

@@ -4,6 +4,9 @@ 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)
@@ -22,6 +25,19 @@ class BMMinerS17(BMMiner, S17):
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")

View File

@@ -22,6 +22,19 @@ class BMMinerS17Plus(BMMiner, S17Plus):
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")

View File

@@ -22,6 +22,19 @@ class BMMinerS17Pro(BMMiner, S17Pro):
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")

View File

@@ -22,6 +22,19 @@ class BMMinerS17e(BMMiner, S17e):
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")

View File

@@ -22,6 +22,19 @@ class BMMinerT17(BMMiner, T17):
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")

View File

@@ -22,6 +22,19 @@ class BMMinerT17Plus(BMMiner, T17Plus):
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")

View File

@@ -22,6 +22,19 @@ class BMMinerT17e(BMMiner, T17e):
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")

View File

@@ -5,6 +5,9 @@ import httpx
import json
# TODO add config
class BMMinerS19(BMMiner, S19):
def __init__(self, ip: str) -> None:
super().__init__(ip)
@@ -23,6 +26,19 @@ class BMMinerS19(BMMiner, S19):
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")

View File

@@ -23,6 +23,19 @@ class BMMinerS19Pro(BMMiner, S19Pro):
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")

View File

@@ -23,6 +23,19 @@ class BMMinerS19a(BMMiner, S19a):
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")

View File

@@ -23,6 +23,19 @@ class BMMinerS19j(BMMiner, S19j):
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")

View File

@@ -23,6 +23,19 @@ class BMMinerS19jPro(BMMiner, S19jPro):
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")

View File

@@ -23,6 +23,19 @@ class BMMinerT19(BMMiner, T19):
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")

View File

@@ -133,8 +133,10 @@ MINER_CLASSES = {
"BTMiner": BTMinerM21,
},
"M21S": {
"Default": BTMinerM21S,
"BTMiner": BTMinerM21S,
"Default": BTMinerM21SV60,
"BTMiner": BTMinerM21SV60,
"60": BTMinerM21SV60,
"20": BTMinerM21SV20,
},
"M21S+": {
"Default": BTMinerM21SPlus,
@@ -143,10 +145,12 @@ MINER_CLASSES = {
"M30S": {
"Default": BTMinerM30S,
"BTMiner": BTMinerM30S,
"50": BTMinerM30SV50,
},
"M30S+": {
"Default": BTMinerM30SPlus,
"BTMiner": BTMinerM30SPlus,
"40": BTMinerM30SPlusVE40,
},
"M30S++": {
"Default": BTMinerM30SPlusPlusVG40,
@@ -161,6 +165,7 @@ MINER_CLASSES = {
"M31S+": {
"Default": BTMinerM31SPlus,
"BTMiner": BTMinerM31SPlus,
"20": BTMinerM31SPlusVE20,
},
"M32S": {
"Default": BTMinerM32S,
@@ -430,12 +435,17 @@ class MinerFactory(metaclass=Singleton):
if model:
# whatsminer have a V in their version string (M20SV41), remove everything after it
if "V" in model:
ver = model.split("VG")[1]
model = model.split("V")[0]
_ver = model.split("V")
if len(_ver) > 1:
ver = model.split("V")[1]
if "VE" in model:
ver = model.split("VE")[1]
if "VG" in model:
ver = model.split("VG")[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
@staticmethod

View File

@@ -1,8 +1,14 @@
from miners._backends import BTMiner # noqa - Ignore access to _module
from miners._types import M21S # noqa - Ignore access to _module
from miners._types import M21SV20, M21SV60 # noqa - Ignore access to _module
class BTMinerM21S(BTMiner, M21S):
class BTMinerM21SV20(BTMiner, M21SV20):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM21SV60(BTMiner, M21SV60):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -2,5 +2,5 @@ from .M20S import BTMinerM20S
from .M20S_Plus import BTMinerM20SPlus
from .M21 import BTMinerM21
from .M21S import BTMinerM21S
from .M21S import BTMinerM21SV20, BTMinerM21SV60
from .M21S_Plus import BTMinerM21SPlus

View File

@@ -1,8 +1,14 @@
from miners._backends import BTMiner # noqa - Ignore access to _module
from miners._types import M30S # noqa - Ignore access to _module
from miners._types import M30S, M30SV50 # noqa - Ignore access to _module
class BTMinerM30S(BTMiner, M30S):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM30SV50(BTMiner, M30SV50):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -1,8 +1,14 @@
from miners._backends import BTMiner # noqa - Ignore access to _module
from miners._types import M30SPlus # noqa - Ignore access to _module
from miners._types import M30SPlus, M30SPlusVE40 # noqa - Ignore access to _module
class BTMinerM30SPlus(BTMiner, M30SPlus):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM30SPlusVE40(BTMiner, M30SPlusVE40):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -1,8 +1,14 @@
from miners._backends import BTMiner # noqa - Ignore access to _module
from miners._types import M31SPlus # noqa - Ignore access to _module
from miners._types import M31SPlus, M31SPlusVE20 # noqa - Ignore access to _module
class BTMinerM31SPlus(BTMiner, M31SPlus):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM31SPlusVE20(BTMiner, M31SPlusVE20):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -1,8 +1,8 @@
from .M30S import BTMinerM30S
from .M30S_Plus import BTMinerM30SPlus
from .M30S import BTMinerM30S, BTMinerM30SV50
from .M30S_Plus import BTMinerM30SPlus, BTMinerM30SPlusVE40
from .M30S_Plus_Plus import BTMinerM30SPlusPlusVG40, BTMinerM30SPlusPlusVG30
from .M31S import BTMinerM31S
from .M31S_Plus import BTMinerM31SPlus
from .M31S_Plus import BTMinerM31SPlus, BTMinerM31SPlusVE20
from .M32S import BTMinerM32S

View File

@@ -13,6 +13,10 @@ if (
def main():
from logger import init_logger
init_logger()
asyncio.run(ui())

View File

@@ -1,32 +1,26 @@
from datetime import datetime, timedelta
from io import BytesIO
from typing import List, Dict
from data import MinerData
import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator
from matplotlib.dates import DateFormatter
import numpy as np
from reportlab.lib import colors
from matplotlib.ticker import MultipleLocator
from reportlab.lib.pagesizes import letter, inch
from reportlab.lib.styles import (
ParagraphStyle,
TA_CENTER, # noqa - not declared in __all__
)
from reportlab.lib.utils import ImageReader
from reportlab.platypus import (
SimpleDocTemplate,
KeepInFrame,
Table,
Image,
Paragraph,
TableStyle,
PageBreak,
Spacer,
)
from io import BytesIO
from svglib.svglib import svg2rlg
from data import MinerData
async def generate_pdf(data: Dict[str, List[MinerData]], file_loc):
doc = SimpleDocTemplate(

View File

@@ -229,9 +229,9 @@ class TableManager(metaclass=Singleton):
"Temp",
"Total",
"Ideal",
"Left Chips",
"Center Chips",
"Right Chips",
"Left Board",
"Center Board",
"Right Board",
]:
if isinstance(self.data[data_key][self.sort_key], str):
return -300