diff --git a/API/btminer.py b/API/btminer.py index 13d26d34..de878a81 100644 --- a/API/btminer.py +++ b/API/btminer.py @@ -514,6 +514,14 @@ class BTMinerAPI(BaseMinerAPI): """ API 'get_version' command. + Returns a dict containing version information. + """ + return await self.get_version() + + async def get_version(self): + """ + API 'get_version' command. + Returns a dict containing version information. """ return await self.send_command("get_version") diff --git a/cfg_util/func/miners.py b/cfg_util/func/miners.py index f75d634e..cc428dd9 100644 --- a/cfg_util/func/miners.py +++ b/cfg_util/func/miners.py @@ -45,7 +45,7 @@ async def scan_network(network): async for found_miner in get_miner_genenerator: all_miners.append(found_miner) all_miners.sort(key=lambda x: x.ip) - window["ip_table"].update([[str(miner.ip), "", "", "", ""] for miner in all_miners]) + window["ip_table"].update([[str(miner.ip)] for miner in all_miners]) progress_bar_len += 1 asyncio.create_task(update_prog_bar(progress_bar_len)) await update_ui_with_data("ip_count", str(len(all_miners))) @@ -147,7 +147,7 @@ async def scan_and_get_data(network): # can output "Identifying" for each found item, but it gets a bit cluttered # and could possibly be confusing for the end user because of timing on # adding the IPs - # window["ip_table"].update([["Identifying...", "", "", "", ""] for miner in miners]) + # window["ip_table"].update([["Identifying..."] for miner in miners]) progress_bar_len += 1 asyncio.create_task(update_prog_bar(progress_bar_len)) progress_bar_len += network_size - len(miners) @@ -157,7 +157,7 @@ async def scan_and_get_data(network): async for found_miner in get_miner_genenerator: all_miners.append(found_miner) all_miners.sort(key=lambda x: x.ip) - window["ip_table"].update([[str(miner.ip), "", "", "", ""] for miner in all_miners]) + window["ip_table"].update([[str(miner.ip)] for miner in all_miners]) progress_bar_len += 1 asyncio.create_task(update_prog_bar(progress_bar_len)) await update_ui_with_data("ip_count", str(len(all_miners))) diff --git a/miners/antminer/X17/bos.py b/miners/antminer/X17/bos.py index 224285f6..bf40f897 100644 --- a/miners/antminer/X17/bos.py +++ b/miners/antminer/X17/bos.py @@ -5,7 +5,7 @@ import toml from config.bos import bos_config_convert, general_config_convert_bos -class BOSminerX17(BaseMiner): +class BOSMinerX17(BaseMiner): def __init__(self, ip: str) -> None: api = BOSMinerAPI(ip) super().__init__(ip, api) diff --git a/miners/miner_factory.py b/miners/miner_factory.py index 0ae509e6..2d071729 100644 --- a/miners/miner_factory.py +++ b/miners/miner_factory.py @@ -1,3 +1,12 @@ +from miners.antminer.S9.bos import BOSMinerS9 +from miners.antminer.X17.bos import BOSMinerX17 + +from miners.whatsminer.M20 import BTMinerM20 +from miners.whatsminer.M21 import BTMinerM21 +from miners.whatsminer.M30 import BTMinerM30 +from miners.whatsminer.M31 import BTMinerM31 +from miners.whatsminer.M32 import BTMinerM32 + from miners.bosminer import BOSminer from miners.bmminer import BMMiner from miners.cgminer import CGMiner @@ -32,37 +41,53 @@ class MinerFactory: for miner in scanned: yield await miner - async def get_miner(self, ip: ipaddress.ip_address) -> BOSminer or CGMiner or BMMiner or UnknownMiner: + async def get_miner(self, ip: ipaddress.ip_address) -> BOSminer or CGMiner or BMMiner or BTMiner or UnknownMiner: """Decide a miner type using the IP address of the miner.""" # check if the miner already exists in cache if ip in self.miners: return self.miners[ip] - # get the version data - version = None + miner = UnknownMiner(str(ip)) + api = None for i in range(GET_VERSION_RETRIES): - version_data = await self._get_version_data(ip) - if version_data: - # if we got version data, get a list of the keys so we can check type of miner - version = list(version_data['VERSION'][0].keys()) + api = await self._get_api_type(ip) + if api: break - if version: - # check version against different return miner types - if "BOSminer" in version or "BOSminer+" in version: - miner = BOSminer(str(ip)) - elif "CGMiner" in version: - miner = CGMiner(str(ip)) - elif "BMMiner" in version: - miner = BMMiner(str(ip)) - elif "BTMiner" in version: - miner = BTMiner(str(ip)) - else: - print(f"Bad API response: {version}") - miner = UnknownMiner(str(ip)) - else: - # if we don't get version, miner type is unknown - print(f"No API response: {str(ip)}") - miner = UnknownMiner(str(ip)) - # save the miner in cache + model = None + for i in range(GET_VERSION_RETRIES): + model = await self._get_miner_model(ip) + if model: + break + if model: + if "Antminer" in model: + if model == "Antminer S9": + if "BOSMiner" in api: + miner = BOSMinerS9(str(ip)) + elif "CGMiner" in api: + miner = CGMiner(str(ip)) + elif "BMMiner" in api: + miner = BMMiner(str(ip)) + elif "17" in model: + if "BOSMiner" in api: + miner = BOSMinerX17(str(ip)) + elif "CGMiner" in api: + miner = CGMiner(str(ip)) + elif "BMMiner" in api: + miner = BMMiner(str(ip)) + elif "19" in model: + if "CGMiner" in api: + miner = CGMiner(str(ip)) + elif "BMMiner" in api: + miner = BMMiner(str(ip)) + elif "M20" in model: + miner = BTMinerM20(str(ip)) + elif "M21" in model: + miner = BTMinerM21(str(ip)) + elif "M30" in model: + miner = BTMinerM30(str(ip)) + elif "M31" in model: + miner = BTMinerM31(str(ip)) + elif "M32" in model: + miner = BTMinerM32(str(ip)) self.miners[ip] = miner return miner @@ -70,101 +95,107 @@ class MinerFactory: """Clear the miner factory cache.""" self.miners = {} - @staticmethod - async def _get_version_data(ip: ipaddress.ip_address) -> dict or None: - """Get data on the version of the miner to return the right miner.""" - for i in range(3): - try: - # open a connection to the miner - fut = asyncio.open_connection(str(ip), 4028) - # get reader and writer streams - try: - reader, writer = await asyncio.wait_for(fut, timeout=7) - except asyncio.exceptions.TimeoutError: - return None - - # create the command - cmd = {"command": "version"} - - # send the command - writer.write(json.dumps(cmd).encode('utf-8')) - await writer.drain() - - # instantiate data - data = b"" - - # loop to receive all the data - while True: - d = await reader.read(4096) - if not d: - break - data += d - - if data.endswith(b"\x00"): - data = json.loads(data.decode('utf-8')[:-1]) - else: - # some stupid whatsminers need a different command - fut = asyncio.open_connection(str(ip), 4028) - # get reader and writer streams + async def _get_miner_model(self, ip: ipaddress.ip_address or str) -> dict or None: + model = None + try: + data = await self._send_api_command(str(ip), "devdetails") + if data.get("STATUS"): + if data["STATUS"][0].get("STATUS") not in ["I", "S"]: try: - reader, writer = await asyncio.wait_for(fut, timeout=7) - except asyncio.exceptions.TimeoutError: - return None - - # create the command - cmd = {"command": "get_version"} - - # send the command - writer.write(json.dumps(cmd).encode('utf-8')) - await writer.drain() - - # instantiate data - data = b"" - - # loop to receive all the data - while True: - d = await reader.read(4096) - if not d: - break - data += d - - data = data.decode('utf-8').replace("\n", "") - data = json.loads(data) - - # close the connection - writer.close() - await writer.wait_closed() - # check if the data returned is correct or an error - # if status isn't a key, it is a multicommand - if "STATUS" not in data.keys(): - for key in data.keys(): - # make sure not to try to turn id into a dict - if not key == "id": - # make sure they succeeded - if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]: - # this is an error - raise APIError(data["STATUS"][0]["Msg"]) + data = await self._send_api_command(str(ip), "version") + model = data["VERSION"][0]["Type"] + except: + print(f"Get Model Exception: {ip}") else: - # check for stupid whatsminer formatting - if not isinstance(data["STATUS"], list): - if data["STATUS"] not in ("S", "I"): - raise APIError(data["Msg"]) - else: - if "whatsminer" in data["Description"]: - return {"VERSION": [{"BTMiner": data["Description"]}]} - # make sure the command succeeded - elif data["STATUS"][0]["STATUS"] not in ("S", "I"): - # this is an error - raise APIError(data["STATUS"][0]["Msg"]) - # return the data - return data - except OSError as e: - if e.winerror == 121: - return None - else: - print(ip, e) - # except json.decoder.JSONDecodeError: - # print("Decode Error @ " + str(ip) + str(data)) - # except Exception as e: - # print(ip, e) + model = data["DEVDETAILS"][0]["Model"] + if model: + return model + except OSError as e: + if e.winerror == 121: + return None + else: + print(ip, e) + return None + + async def _send_api_command(self, ip: ipaddress.ip_address or str, command: str): + try: + # get reader and writer streams + reader, writer = await asyncio.open_connection(str(ip), 4028) + # handle OSError 121 + except OSError as e: + if e.winerror == "121": + print("Semaphore Timeout has Expired.") + return {} + + # create the command + cmd = {"command": command} + + # send the command + writer.write(json.dumps(cmd).encode('utf-8')) + await writer.drain() + + # instantiate data + data = b"" + + # loop to receive all the data + try: + while True: + d = await reader.read(4096) + if not d: + break + data += d + except Exception as e: + print(e) + + try: + # some json from the API returns with a null byte (\x00) on the end + if data.endswith(b"\x00"): + # handle the null byte + str_data = data.decode('utf-8')[:-1] + else: + # no null byte + str_data = data.decode('utf-8') + # fix an error with a btminer return having an extra comma that breaks json.loads() + str_data = str_data.replace(",}", "}") + # fix an error with a btminer return having a newline that breaks json.loads() + str_data = str_data.replace("\n", "") + # fix an error with a bmminer return not having a specific comma that breaks json.loads() + str_data = str_data.replace("}{", "},{") + # parse the json + parsed_data = json.loads(str_data) + # handle bad json + except json.decoder.JSONDecodeError as e: + print(e) + raise APIError(f"Decode Error: {data}") + data = parsed_data + + # close the connection + writer.close() + await writer.wait_closed() + + return data + + + async def _get_api_type(self, ip: ipaddress.ip_address or str) -> dict or None: + """Get data on the version of the miner to return the right miner.""" + api = None + try: + data = await self._send_api_command(str(ip), "version") + if data.get("STATUS") and not data.get("STATUS") == "E": + if data["STATUS"][0].get("STATUS") in ["I", "S"]: + if "BMMiner" in data["VERSION"][0].keys(): + api = "BMMiner" + elif "CGMiner" in data["VERSION"][0].keys(): + api = "CGMiner" + elif "BOSminer" in data["VERSION"][0].keys() or "BOSminer+" in data["VERSION"][0].keys(): + api = "BOSMiner" + elif data.get("Description") and "whatsminer" in data.get("Description"): + api = "BTMiner" + if api: + return api + except OSError as e: + if e.winerror == 121: + return None + else: + print(ip, e) return None