diff --git a/pyasic/config/__init__.py b/pyasic/config/__init__.py index 98511bee..87f0e93d 100644 --- a/pyasic/config/__init__.py +++ b/pyasic/config/__init__.py @@ -104,6 +104,19 @@ class _Pool: pool = {"url": self.url, "user": username, "pass": self.password} return pool + def as_x5(self, user_suffix: str = None) -> dict: + """Convert the data in this class to a dict usable by an X5 device. + + Parameters: + user_suffix: The suffix to append to username. + """ + username = self.username + if user_suffix: + username = f"{username}{user_suffix}" + + pool = {"url": self.url, "user": username, "pass": self.password} + return pool + def as_goldshell(self, user_suffix: str = None) -> dict: """Convert the data in this class to a dict usable by a goldshell device. @@ -240,6 +253,30 @@ class _PoolGroup: return pools + def as_x5(self, user_suffix: str = None) -> dict: + """Convert the data in this class to a list usable by an X5 device. + + Parameters: + user_suffix: The suffix to append to username. + """ + pools = { + "_ant_pool1url": "", + "_ant_pool1user": "", + "_ant_pool1pw": "", + "_ant_pool2url": "", + "_ant_pool2user": "", + "_ant_pool2pw": "", + "_ant_pool3url": "", + "_ant_pool3user": "", + "_ant_pool3pw": "", + } + for idx, pool in enumerate(self.pools[:3]): + pools[f"_ant_pool{idx+1}url"] = pool.as_x5(user_suffix=user_suffix)["url"] + pools[f"_ant_pool{idx+1}user"] = pool.as_x5(user_suffix=user_suffix)["user"] + pools[f"_ant_pool{idx+1}pw"] = pool.as_x5(user_suffix=user_suffix)["pass"] + + return pools + def as_goldshell(self, user_suffix: str = None) -> list: """Convert the data in this class to a list usable by a goldshell device. @@ -555,6 +592,16 @@ class MinerConfig: return cfg + def as_x5(self, user_suffix: str = None) -> dict: + """Convert the data in this class to a config usable by an X5 device. + + Parameters: + user_suffix: The suffix to append to username. + """ + cfg = self.pool_groups[0].as_x5(user_suffix=user_suffix) + + return cfg + def as_goldshell(self, user_suffix: str = None) -> list: """Convert the data in this class to a config usable by a goldshell device. diff --git a/pyasic/miners/btc/innosilicon/cgminer/T3X/T3H_Plus.py b/pyasic/miners/btc/innosilicon/cgminer/T3X/T3H_Plus.py index 866c81b7..4411b7b7 100644 --- a/pyasic/miners/btc/innosilicon/cgminer/T3X/T3H_Plus.py +++ b/pyasic/miners/btc/innosilicon/cgminer/T3X/T3H_Plus.py @@ -82,6 +82,9 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus): async def get_mac( self, web_get_all: dict = None, web_overview: dict = None ) -> Optional[str]: + if web_get_all: + web_get_all = web_get_all["all"] + if not web_get_all and not web_overview: try: web_overview = await self.web.overview() @@ -123,6 +126,9 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus): async def get_hashrate( self, api_summary: dict = None, web_get_all: dict = None ) -> Optional[float]: + if web_get_all: + web_get_all = web_get_all["all"] + if not api_summary and not web_get_all: try: api_summary = await self.api.summary() @@ -146,6 +152,9 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus): async def get_hashboards( self, api_stats: dict = None, web_get_all: dict = None ) -> List[HashBoard]: + if web_get_all: + web_get_all = web_get_all["all"] + hashboards = [ HashBoard(slot=i, expected_chips=self.nominal_chips) for i in range(self.ideal_hashboards) @@ -201,6 +210,9 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus): async def get_wattage( self, web_get_all: dict = None, api_stats: dict = None ) -> Optional[int]: + if web_get_all: + web_get_all = web_get_all["all"] + if not web_get_all: try: web_get_all = await self.web.get_all() @@ -233,6 +245,9 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus): return wattage async def get_fans(self, web_get_all: dict = None) -> List[Fan]: + if web_get_all: + web_get_all = web_get_all["all"] + if not web_get_all: try: web_get_all = await self.web.get_all() @@ -304,6 +319,9 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus): return errors async def get_wattage_limit(self, web_get_all: dict = None) -> Optional[int]: + if web_get_all: + web_get_all = web_get_all["all"] + if not web_get_all: try: web_get_all = await self.web.get_all() diff --git a/pyasic/miners/ckb/__init__.py b/pyasic/miners/ckb/__init__.py new file mode 100644 index 00000000..d6d7eef1 --- /dev/null +++ b/pyasic/miners/ckb/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .goldshell import * diff --git a/pyasic/miners/ckb/_backends/__init__.py b/pyasic/miners/ckb/_backends/__init__.py new file mode 100644 index 00000000..45ee0db4 --- /dev/null +++ b/pyasic/miners/ckb/_backends/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .bfgminer import BFGMiner +from .goldshell import Goldshell diff --git a/pyasic/miners/ckb/_backends/bfgminer.py b/pyasic/miners/ckb/_backends/bfgminer.py new file mode 100644 index 00000000..8de9a6c5 --- /dev/null +++ b/pyasic/miners/ckb/_backends/bfgminer.py @@ -0,0 +1,318 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +import ipaddress +import logging +from collections import namedtuple +from typing import List, Optional, Tuple + +from pyasic.API.bfgminer import BFGMinerAPI +from pyasic.config import MinerConfig +from pyasic.data import Fan, HashBoard +from pyasic.data.error_codes import MinerErrorData +from pyasic.errors import APIError +from pyasic.miners.base import BaseMiner + + +class BFGMiner(BaseMiner): + """Base handler for BFGMiner based miners.""" + + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: + super().__init__(ip) + self.ip = ipaddress.ip_address(ip) + self.api = BFGMinerAPI(ip, api_ver) + self.api_type = "BFGMiner" + self.api_ver = api_ver + self.uname = "root" + self.pwd = "admin" + + async def get_config(self) -> MinerConfig: + # get pool data + try: + pools = await self.api.pools() + except APIError: + return self.config + + self.config = MinerConfig().from_api(pools["POOLS"]) + return self.config + + async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + return None + + async def fault_light_off(self) -> bool: + return False + + async def fault_light_on(self) -> bool: + return False + + async def restart_backend(self) -> bool: + return False + + async def stop_mining(self) -> bool: + return False + + async def resume_mining(self) -> bool: + return False + + async def set_power_limit(self, wattage: int) -> bool: + return False + + ################################################## + ### DATA GATHERING FUNCTIONS (get_{some_data}) ### + ################################################## + + async def get_mac(self) -> str: + return "00:00:00:00:00:00" + + async def get_model(self, api_devdetails: dict = None) -> Optional[str]: + if self.model: + logging.debug(f"Found model for {self.ip}: {self.model}") + return self.model + + if not api_devdetails: + try: + api_devdetails = await self.api.devdetails() + except APIError: + pass + + if api_devdetails: + try: + self.model = api_devdetails["DEVDETAILS"][0]["Model"].replace( + "Antminer ", "" + ) + logging.debug(f"Found model for {self.ip}: {self.model}") + return self.model + except (TypeError, IndexError, KeyError): + pass + + logging.warning(f"Failed to get model for miner: {self}") + return None + + async def get_api_ver(self, api_version: dict = None) -> Optional[str]: + # Check to see if the version info is already cached + if self.api_ver: + return self.api_ver + + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + if api_version: + try: + self.api_ver = api_version["VERSION"][0]["API"] + except (KeyError, IndexError): + pass + + return self.api_ver + + async def get_fw_ver(self, api_version: dict = None) -> Optional[str]: + # Check to see if the version info is already cached + if self.fw_ver: + return self.fw_ver + + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + if api_version: + try: + self.fw_ver = api_version["VERSION"][0]["CompileTime"] + except (KeyError, IndexError): + pass + + return self.fw_ver + + async def get_version( + self, api_version: dict = None + ) -> Tuple[Optional[str], Optional[str]]: + # check if version is cached + miner_version = namedtuple("MinerVersion", "api_ver fw_ver") + return miner_version( + api_ver=await self.get_api_ver(api_version), + fw_ver=await self.get_fw_ver(api_version=api_version), + ) + + async def reboot(self) -> bool: + return False + + async def get_fan_psu(self): + return None + + async def get_hostname(self) -> Optional[str]: + return None + + async def get_hashrate(self, api_summary: dict = None) -> Optional[float]: + # get hr from API + if not api_summary: + try: + api_summary = await self.api.summary() + except APIError: + pass + + if api_summary: + try: + return round(float(api_summary["SUMMARY"][0]["MHS 20s"] / 1000000), 2) + except (IndexError, KeyError, ValueError, TypeError): + pass + + async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]: + hashboards = [] + + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + try: + board_offset = -1 + boards = api_stats["STATS"] + if len(boards) > 1: + 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 b and not b == 0 and board_offset == -1: + board_offset = board_num + if board_offset == -1: + board_offset = 1 + + for i in range(board_offset, board_offset + self.ideal_hashboards): + hashboard = HashBoard( + slot=i - board_offset, expected_chips=self.nominal_chips + ) + + chip_temp = boards[1].get(f"temp{i}") + if chip_temp: + hashboard.chip_temp = round(chip_temp) + + temp = boards[1].get(f"temp2_{i}") + if temp: + hashboard.temp = round(temp) + + hashrate = boards[1].get(f"chain_rate{i}") + if hashrate: + hashboard.hashrate = round(float(hashrate) / 1000, 2) + + chips = boards[1].get(f"chain_acn{i}") + if chips: + hashboard.chips = chips + hashboard.missing = False + if (not chips) or (not chips > 0): + hashboard.missing = True + hashboards.append(hashboard) + except (IndexError, KeyError, ValueError, TypeError): + pass + + return hashboards + + async def get_env_temp(self) -> Optional[float]: + return None + + async def get_wattage(self) -> Optional[int]: + return None + + async def get_wattage_limit(self) -> Optional[int]: + return None + + async def get_fans(self, api_stats: dict = None) -> List[Fan]: + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + fans_data = [None, None, None, None] + if api_stats: + try: + fan_offset = -1 + + for fan_num in range(0, 8, 4): + for _f_num in range(4): + f = api_stats["STATS"][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): + fans_data[fan] = api_stats["STATS"][1].get(f"fan{fan_offset+fan}") + except (KeyError, IndexError): + pass + fans = [Fan(speed=d) if d else Fan() for d in fans_data] + + return fans + + async def get_pools(self, api_pools: dict = None) -> List[dict]: + groups = [] + + if not api_pools: + try: + api_pools = await self.api.pools() + except APIError: + pass + + if api_pools: + try: + pools = {} + for i, pool in enumerate(api_pools["POOLS"]): + pools[f"pool_{i + 1}_url"] = ( + pool["URL"] + .replace("stratum+tcp://", "") + .replace("stratum2+tcp://", "") + ) + pools[f"pool_{i + 1}_user"] = pool["User"] + pools["quota"] = pool["Quota"] if pool.get("Quota") else "0" + + groups.append(pools) + except KeyError: + pass + return groups + + async def get_errors(self) -> List[MinerErrorData]: + return [] + + async def get_fault_light(self) -> bool: + return False + + async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]: + # X19 method, not sure compatibility + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + try: + ideal_rate = api_stats["STATS"][1]["total_rateideal"] + try: + rate_unit = api_stats["STATS"][1]["rate_unit"] + except KeyError: + rate_unit = "GH" + if rate_unit == "GH": + return round(ideal_rate / 1000, 2) + if rate_unit == "MH": + return round(ideal_rate / 1000000, 2) + else: + return round(ideal_rate, 2) + except (KeyError, IndexError): + pass diff --git a/pyasic/miners/ckb/_backends/goldshell.py b/pyasic/miners/ckb/_backends/goldshell.py new file mode 100644 index 00000000..3e42ca00 --- /dev/null +++ b/pyasic/miners/ckb/_backends/goldshell.py @@ -0,0 +1,101 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from typing import List, Optional + +from pyasic.config import MinerConfig +from pyasic.data import Fan, HashBoard +from pyasic.errors import APIError +from pyasic.miners.hns._backends import BFGMiner +from pyasic.web.goldshell import GoldshellWebAPI + + +class Goldshell(BFGMiner): + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: + super().__init__(ip, api_ver) + self.web = GoldshellWebAPI(ip) + + async def get_config(self) -> MinerConfig: + return MinerConfig().from_raw(await self.web.pools()) + + async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + pools_data = await self.web.pools() + # have to delete all the pools one at a time first + for pool in pools_data: + await self.web.delpool( + url=pool["url"], + user=pool["user"], + password=pool["pass"], + dragid=pool["dragid"], + ) + + self.config = config + + # send them back 1 at a time + for pool in config.as_goldshell(user_suffix=user_suffix): + await self.web.newpool( + url=pool["url"], user=pool["user"], password=pool["pass"] + ) + + async def get_mac(self, web_setting: dict = None) -> str: + if not web_setting: + try: + web_setting = await self.web.setting() + except APIError: + pass + + if web_setting: + try: + return web_setting["name"] + except KeyError: + pass + + async def get_fw_ver(self, web_status: dict = None) -> str: + if not web_status: + try: + web_status = await self.web.setting() + except APIError: + pass + + if web_status: + try: + return web_status["firmware"] + except KeyError: + pass + + async def get_hashboards(self, api_devs: dict = None) -> List[HashBoard]: + if not api_devs: + try: + api_devs = await self.api.devs() + except APIError: + pass + + hashboards = [ + HashBoard(slot=i, expected_chips=self.nominal_chips) + for i in range(self.ideal_hashboards) + ] + + if api_devs: + for board in api_devs["DEVS"]: + if board.get("ID"): + try: + b_id = board["ID"] + hashboards[b_id].hashrate = round(board["MHS 20s"] / 1000000, 2) + hashboards[b_id].temp = board["tstemp-2"] + hashboards[b_id].missing = False + except KeyError: + pass + + return hashboards diff --git a/pyasic/miners/ckb/_types/__init__.py b/pyasic/miners/ckb/_types/__init__.py new file mode 100644 index 00000000..d6d7eef1 --- /dev/null +++ b/pyasic/miners/ckb/_types/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .goldshell import * diff --git a/pyasic/miners/ckb/_types/goldshell/CK5.py b/pyasic/miners/ckb/_types/goldshell/CK5.py new file mode 100644 index 00000000..809cd386 --- /dev/null +++ b/pyasic/miners/ckb/_types/goldshell/CK5.py @@ -0,0 +1,26 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from pyasic.miners.makes import GoldshellMiner + + +class CK5(GoldshellMiner): # noqa - ignore ABC method implementation + def __init__(self, ip: str): + super().__init__() + self.ip = ip + self.model = "CK5" + self.ideal_hashboards = 4 + self.chip_count = 18 + self.fan_count = 4 diff --git a/pyasic/miners/ckb/_types/goldshell/__init__.py b/pyasic/miners/ckb/_types/goldshell/__init__.py new file mode 100644 index 00000000..54fb009f --- /dev/null +++ b/pyasic/miners/ckb/_types/goldshell/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .CK5 import CK5 diff --git a/pyasic/miners/ckb/goldshell/__init__.py b/pyasic/miners/ckb/goldshell/__init__.py new file mode 100644 index 00000000..75327f3a --- /dev/null +++ b/pyasic/miners/ckb/goldshell/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .bfgminer import * diff --git a/pyasic/miners/ckb/goldshell/bfgminer/CKX/CK5.py b/pyasic/miners/ckb/goldshell/bfgminer/CKX/CK5.py new file mode 100644 index 00000000..f8327192 --- /dev/null +++ b/pyasic/miners/ckb/goldshell/bfgminer/CKX/CK5.py @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from pyasic.miners.ckb._backends import Goldshell # noqa - Ignore access to _module +from pyasic.miners.ckb._types import CK5 # noqa - Ignore access to _module + + +class BFGMinerCK5(Goldshell, CK5): + pass diff --git a/pyasic/miners/ckb/goldshell/bfgminer/CKX/__init__.py b/pyasic/miners/ckb/goldshell/bfgminer/CKX/__init__.py new file mode 100644 index 00000000..1c09abf1 --- /dev/null +++ b/pyasic/miners/ckb/goldshell/bfgminer/CKX/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .CK5 import BFGMinerCK5 diff --git a/pyasic/miners/ckb/goldshell/bfgminer/__init__.py b/pyasic/miners/ckb/goldshell/bfgminer/__init__.py new file mode 100644 index 00000000..9fbf21f7 --- /dev/null +++ b/pyasic/miners/ckb/goldshell/bfgminer/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .CKX import * diff --git a/pyasic/miners/dcr/__init__.py b/pyasic/miners/dcr/__init__.py new file mode 100644 index 00000000..a2151b40 --- /dev/null +++ b/pyasic/miners/dcr/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .antminer import * diff --git a/pyasic/miners/dcr/_backends/X5.py b/pyasic/miners/dcr/_backends/X5.py new file mode 100644 index 00000000..c331e672 --- /dev/null +++ b/pyasic/miners/dcr/_backends/X5.py @@ -0,0 +1,202 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from typing import List, Optional, Union + +from pyasic.config import MinerConfig +from pyasic.data import Fan, HashBoard +from pyasic.errors import APIError +from pyasic.miners.zec._backends import CGMiner # noqa - Ignore access to _module +from pyasic.web.X5 import X5WebAPI + + +class X5(CGMiner): + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: + super().__init__(ip, api_ver=api_ver) + self.ip = ip + self.web = X5WebAPI(ip) + + async def get_config(self) -> MinerConfig: + data = await self.web.get_miner_conf() + if data: + self.config = MinerConfig().from_raw(data) + return self.config + + async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + await self.web.set_miner_conf(config.as_x5(user_suffix=user_suffix)) + + async def get_mac(self) -> Union[str, None]: + try: + data = await self.web.get_system_info() + if data: + return data["macaddr"] + except KeyError: + pass + + async def fault_light_on(self) -> bool: + # this should time out, after it does do a check + await self.web.blink(blink=True) + try: + data = await self.web.get_blink_status() + if data: + if data["isBlinking"]: + self.light = True + except KeyError: + pass + return self.light + + async def fault_light_off(self) -> bool: + await self.web.blink(blink=False) + try: + data = await self.web.get_blink_status() + if data: + if not data["isBlinking"]: + self.light = False + except KeyError: + pass + return self.light + + async def reboot(self) -> bool: + data = await self.web.reboot() + if data: + return True + return False + + async def get_fault_light(self, web_get_blink_status: dict = None) -> bool: + if self.light: + return self.light + + if not web_get_blink_status: + try: + web_get_blink_status = await self.web.get_blink_status() + except APIError: + pass + + if web_get_blink_status: + try: + self.light = web_get_blink_status["isBlinking"] + except KeyError: + pass + return self.light + + async def get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: + if not web_get_system_info: + try: + web_get_system_info = await self.web.get_system_info() + except APIError: + pass + + if web_get_system_info: + try: + return web_get_system_info["hostname"] + except KeyError: + pass + + async def get_model(self, web_get_system_info: dict = None) -> Optional[str]: + if self.model: + return self.model + + if not web_get_system_info: + try: + web_get_system_info = await self.web.get_system_info() + except APIError: + pass + + if web_get_system_info: + try: + return web_get_system_info["minertype"] + except APIError: + pass + + async def get_fans(self, api_stats: dict = None) -> List[Fan]: + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + fans_data = [Fan(), Fan(), Fan(), Fan()] + if api_stats: + try: + fan_offset = -1 + + for fan_num in range(1, 8, 4): + for _f_num in range(4): + f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}") + if f and not f == 0 and fan_offset == -1: + fan_offset = fan_num + 2 + if fan_offset == -1: + fan_offset = 3 + + for fan in range(self.fan_count): + fans_data[fan] = Fan( + api_stats["STATS"][1].get(f"fan{fan_offset+fan}") + ) + except (KeyError, IndexError): + pass + return fans_data + + async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]: + hashboards = [] + + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + try: + board_offset = -1 + boards = api_stats["STATS"] + if len(boards) > 1: + 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 b and not b == 0 and board_offset == -1: + board_offset = board_num + if board_offset == -1: + board_offset = 1 + + for i in range(board_offset, board_offset + self.ideal_hashboards): + hashboard = HashBoard( + slot=i - board_offset, expected_chips=self.nominal_chips + ) + + chip_temp = boards[1].get(f"temp{i}") + if chip_temp: + hashboard.chip_temp = round(chip_temp) + + temp = boards[1].get(f"temp2_{i}") + if temp: + hashboard.temp = round(temp) + + hashrate = boards[1].get(f"chain_rate{i}") + if hashrate: + hashboard.hashrate = round(float(hashrate) / 1000, 2) + + chips = boards[1].get(f"chain_acn{i}") + if chips: + hashboard.chips = chips + hashboard.missing = False + if (not chips) or (not chips > 0): + hashboard.missing = True + hashboards.append(hashboard) + except (IndexError, KeyError, ValueError, TypeError): + pass + + return hashboards diff --git a/pyasic/miners/dcr/_backends/__init__.py b/pyasic/miners/dcr/_backends/__init__.py new file mode 100644 index 00000000..bdd18624 --- /dev/null +++ b/pyasic/miners/dcr/_backends/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .cgminer import CGMiner +from .X5 import X5 diff --git a/pyasic/miners/dcr/_backends/cgminer.py b/pyasic/miners/dcr/_backends/cgminer.py new file mode 100644 index 00000000..bd2007c8 --- /dev/null +++ b/pyasic/miners/dcr/_backends/cgminer.py @@ -0,0 +1,373 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +import ipaddress +import logging +from collections import namedtuple +from typing import List, Optional, Tuple + +import asyncssh + +from pyasic.API.cgminer import CGMinerAPI +from pyasic.config import MinerConfig +from pyasic.data import Fan, HashBoard +from pyasic.data.error_codes import MinerErrorData +from pyasic.errors import APIError +from pyasic.miners.base import BaseMiner + + +class CGMiner(BaseMiner): + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: + super().__init__(ip) + self.ip = ipaddress.ip_address(ip) + self.api = CGMinerAPI(ip, api_ver) + self.api_ver = api_ver + self.api_type = "CGMiner" + self.uname = "root" + self.pwd = "admin" + self.config = None + + async def send_ssh_command(self, cmd: str) -> Optional[str]: + result = None + + try: + conn = await self._get_ssh_connection() + except (asyncssh.Error, OSError): + return None + + # open an ssh connection + async with conn: + # 3 retries + for i in range(3): + try: + # run the command and get the result + result = await conn.run(cmd) + result = result.stdout + + except Exception as e: + # if the command fails, log it + logging.warning(f"{self} command {cmd} error: {e}") + + # on the 3rd retry, return None + if i == 3: + return + continue + # return the result, either command output or None + return result + + async def restart_backend(self) -> bool: + """Restart cgminer hashing process. Wraps [`restart_cgminer`][pyasic.miners._backends.cgminer.CGMiner.restart_cgminer] to standardize.""" + return await self.restart_cgminer() + + async def restart_cgminer(self) -> bool: + """Restart cgminer hashing process.""" + commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"] + commands = ";".join(commands) + try: + _ret = await self.send_ssh_command(commands) + except (asyncssh.Error, OSError): + return False + else: + if isinstance(_ret, str): + return True + return False + + async def reboot(self) -> bool: + """Reboots power to the physical miner.""" + logging.debug(f"{self}: Sending reboot command.") + try: + _ret = await self.send_ssh_command("reboot") + except (asyncssh.Error, OSError): + return False + else: + logging.debug(f"{self}: Reboot command completed.") + if isinstance(_ret, str): + return True + return False + + async def resume_mining(self) -> bool: + return False + + async def stop_mining(self) -> bool: + return False + + async def get_config(self) -> MinerConfig: + api_pools = await self.api.pools() + + if api_pools: + self.config = MinerConfig().from_api(api_pools["POOLS"]) + return self.config + + async def fault_light_off(self) -> bool: + return False + + async def fault_light_on(self) -> bool: + return False + + async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + return None + + async def set_power_limit(self, wattage: int) -> bool: + return False + + ################################################## + ### DATA GATHERING FUNCTIONS (get_{some_data}) ### + ################################################## + + async def get_mac(self) -> Optional[str]: + return None + + async def get_model(self, api_devdetails: dict = None) -> Optional[str]: + if self.model: + logging.debug(f"Found model for {self.ip}: {self.model}") + return self.model + + if not api_devdetails: + try: + api_devdetails = await self.api.devdetails() + except APIError: + pass + + if api_devdetails: + try: + self.model = api_devdetails["DEVDETAILS"][0]["Model"].replace( + "Antminer ", "" + ) + logging.debug(f"Found model for {self.ip}: {self.model}") + return self.model + except (TypeError, IndexError, KeyError): + pass + + logging.warning(f"Failed to get model for miner: {self}") + return None + + async def get_version( + self, api_version: dict = None + ) -> Tuple[Optional[str], Optional[str]]: + miner_version = namedtuple("MinerVersion", "api_ver fw_ver") + return miner_version( + api_ver=await self.get_api_ver(api_version=api_version), + fw_ver=await self.get_fw_ver(api_version=api_version), + ) + + async def get_api_ver(self, api_version: dict = None) -> Optional[str]: + if self.api_ver: + return self.api_ver + + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + if api_version: + try: + self.api_ver = api_version["VERSION"][0]["API"] + except (KeyError, IndexError): + pass + + return self.api_ver + + async def get_fw_ver(self, api_version: dict = None) -> Optional[str]: + if self.fw_ver: + return self.fw_ver + + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + if api_version: + try: + self.fw_ver = api_version["VERSION"][0]["CGMiner"] + except (KeyError, IndexError): + pass + + return self.fw_ver + + async def get_hostname(self) -> Optional[str]: + try: + hn = await self.send_ssh_command("cat /proc/sys/kernel/hostname") + except (asyncssh.Error, OSError): + return None + if hn: + self.hostname = hn + return self.hostname + + async def get_hashrate(self, api_summary: dict = None) -> Optional[float]: + # get hr from API + if not api_summary: + try: + api_summary = await self.api.summary() + except APIError: + pass + + if api_summary: + try: + return round( + float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2 + ) + except (IndexError, KeyError, ValueError, TypeError): + pass + + async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]: + hashboards = [] + + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + try: + board_offset = -1 + boards = api_stats["STATS"] + if len(boards) > 1: + 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 b and not b == 0 and board_offset == -1: + board_offset = board_num + if board_offset == -1: + board_offset = 1 + + for i in range(board_offset, board_offset + self.ideal_hashboards): + hashboard = HashBoard( + slot=i - board_offset, expected_chips=self.nominal_chips + ) + + chip_temp = boards[1].get(f"temp{i}") + if chip_temp: + hashboard.chip_temp = round(chip_temp) + + temp = boards[1].get(f"temp2_{i}") + if temp: + hashboard.temp = round(temp) + + hashrate = boards[1].get(f"chain_rate{i}") + if hashrate: + hashboard.hashrate = round(float(hashrate) / 1000, 2) + + chips = boards[1].get(f"chain_acn{i}") + if chips: + hashboard.chips = chips + hashboard.missing = False + if (not chips) or (not chips > 0): + hashboard.missing = True + hashboards.append(hashboard) + except (IndexError, KeyError, ValueError, TypeError): + pass + + return hashboards + + async def get_env_temp(self) -> Optional[float]: + return None + + async def get_wattage(self) -> Optional[int]: + return None + + async def get_wattage_limit(self) -> Optional[int]: + return None + + async def get_fans(self, api_stats: dict = None) -> List[Fan]: + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + fans_data = [Fan(), Fan(), Fan(), Fan()] + if api_stats: + try: + fan_offset = -1 + + for fan_num in range(1, 8, 4): + for _f_num in range(4): + f = api_stats["STATS"][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): + fans_data[fan] = Fan( + api_stats["STATS"][1].get(f"fan{fan_offset+fan}") + ) + except (KeyError, IndexError): + pass + return fans_data + + async def get_fan_psu(self) -> Optional[int]: + return None + + async def get_pools(self, api_pools: dict = None) -> List[dict]: + groups = [] + + if not api_pools: + try: + api_pools = await self.api.pools() + except APIError: + pass + + if api_pools: + try: + pools = {} + for i, pool in enumerate(api_pools["POOLS"]): + pools[f"pool_{i + 1}_url"] = ( + pool["URL"] + .replace("stratum+tcp://", "") + .replace("stratum2+tcp://", "") + ) + pools[f"pool_{i + 1}_user"] = pool["User"] + pools["quota"] = pool["Quota"] if pool.get("Quota") else "0" + + groups.append(pools) + except KeyError: + pass + return groups + + async def get_errors(self) -> List[MinerErrorData]: + return [] + + async def get_fault_light(self) -> bool: + return False + + async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]: + # X19 method, not sure compatibility + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + try: + ideal_rate = api_stats["STATS"][1]["total_rateideal"] + try: + rate_unit = api_stats["STATS"][1]["rate_unit"] + except KeyError: + rate_unit = "GH" + if rate_unit == "GH": + return round(ideal_rate / 1000, 2) + if rate_unit == "MH": + return round(ideal_rate / 1000000, 2) + else: + return round(ideal_rate, 2) + except (KeyError, IndexError): + pass diff --git a/pyasic/miners/dcr/_types/__init__.py b/pyasic/miners/dcr/_types/__init__.py new file mode 100644 index 00000000..a2151b40 --- /dev/null +++ b/pyasic/miners/dcr/_types/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .antminer import * diff --git a/pyasic/miners/dcr/_types/antminer/X5/DR5.py b/pyasic/miners/dcr/_types/antminer/X5/DR5.py new file mode 100644 index 00000000..3ddb7242 --- /dev/null +++ b/pyasic/miners/dcr/_types/antminer/X5/DR5.py @@ -0,0 +1,27 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from pyasic.miners.makes import AntMiner + + +class DR5(AntMiner): # noqa - ignore ABC method implementation + def __init__(self, ip: str): + super().__init__() + self.ip = ip + self.model = "DR5" + self.nominal_chips = 72 + self.ideal_hashboards = 3 + self.fan_count = 2 diff --git a/pyasic/miners/dcr/_types/antminer/X5/__init__.py b/pyasic/miners/dcr/_types/antminer/X5/__init__.py new file mode 100644 index 00000000..aae3b7fc --- /dev/null +++ b/pyasic/miners/dcr/_types/antminer/X5/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .DR5 import DR5 diff --git a/pyasic/miners/dcr/_types/antminer/__init__.py b/pyasic/miners/dcr/_types/antminer/__init__.py new file mode 100644 index 00000000..9e820153 --- /dev/null +++ b/pyasic/miners/dcr/_types/antminer/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .X5 import * diff --git a/pyasic/miners/dcr/antminer/__init__.py b/pyasic/miners/dcr/antminer/__init__.py new file mode 100644 index 00000000..10eb58cb --- /dev/null +++ b/pyasic/miners/dcr/antminer/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .cgminer import * diff --git a/pyasic/miners/dcr/antminer/cgminer/X5/DR5.py b/pyasic/miners/dcr/antminer/cgminer/X5/DR5.py new file mode 100644 index 00000000..1bfdea70 --- /dev/null +++ b/pyasic/miners/dcr/antminer/cgminer/X5/DR5.py @@ -0,0 +1,22 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from pyasic.miners.dcr._backends import X5 # noqa - Ignore access to _module +from pyasic.miners.dcr._types import DR5 # noqa - Ignore access to _module + + +class CGMinerDR5(X5, DR5): + pass diff --git a/pyasic/miners/dcr/antminer/cgminer/X5/__init__.py b/pyasic/miners/dcr/antminer/cgminer/X5/__init__.py new file mode 100644 index 00000000..4379571c --- /dev/null +++ b/pyasic/miners/dcr/antminer/cgminer/X5/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .DR5 import CGMinerDR5 diff --git a/pyasic/miners/dcr/antminer/cgminer/__init__.py b/pyasic/miners/dcr/antminer/cgminer/__init__.py new file mode 100644 index 00000000..9e820153 --- /dev/null +++ b/pyasic/miners/dcr/antminer/cgminer/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .X5 import * diff --git a/pyasic/miners/etc/__init__.py b/pyasic/miners/etc/__init__.py new file mode 100644 index 00000000..75f27e83 --- /dev/null +++ b/pyasic/miners/etc/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .innosilicon import * diff --git a/pyasic/miners/etc/_backends/__init__.py b/pyasic/miners/etc/_backends/__init__.py new file mode 100644 index 00000000..700b7980 --- /dev/null +++ b/pyasic/miners/etc/_backends/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from .cgminer import CGMiner diff --git a/pyasic/miners/etc/_backends/cgminer.py b/pyasic/miners/etc/_backends/cgminer.py new file mode 100644 index 00000000..7fd898b2 --- /dev/null +++ b/pyasic/miners/etc/_backends/cgminer.py @@ -0,0 +1,397 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +import ipaddress +import logging +from collections import namedtuple +from typing import List, Optional, Tuple + +import asyncssh + +from pyasic.API.cgminer import CGMinerAPI +from pyasic.config import MinerConfig +from pyasic.data import Fan, HashBoard +from pyasic.data.error_codes import MinerErrorData +from pyasic.errors import APIError +from pyasic.miners.base import BaseMiner + + +class CGMiner(BaseMiner): + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: + super().__init__(ip) + self.ip = ipaddress.ip_address(ip) + self.api = CGMinerAPI(ip, api_ver) + self.api_ver = api_ver + self.api_type = "CGMiner" + self.uname = "root" + self.pwd = "admin" + self.config = None + + async def send_ssh_command(self, cmd: str) -> Optional[str]: + result = None + + try: + conn = await self._get_ssh_connection() + except (asyncssh.Error, OSError): + return None + + # open an ssh connection + async with conn: + # 3 retries + for i in range(3): + try: + # run the command and get the result + result = await conn.run(cmd) + result = result.stdout + + except Exception as e: + # if the command fails, log it + logging.warning(f"{self} command {cmd} error: {e}") + + # on the 3rd retry, return None + if i == 3: + return + continue + # return the result, either command output or None + return result + + async def restart_backend(self) -> bool: + """Restart cgminer hashing process. Wraps [`restart_cgminer`][pyasic.miners._backends.cgminer.CGMiner.restart_cgminer] to standardize.""" + return await self.restart_cgminer() + + async def restart_cgminer(self) -> bool: + """Restart cgminer hashing process.""" + commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"] + commands = ";".join(commands) + try: + _ret = await self.send_ssh_command(commands) + except (asyncssh.Error, OSError): + return False + else: + if isinstance(_ret, str): + return True + return False + + async def reboot(self) -> bool: + """Reboots power to the physical miner.""" + logging.debug(f"{self}: Sending reboot command.") + try: + _ret = await self.send_ssh_command("reboot") + except (asyncssh.Error, OSError): + return False + else: + logging.debug(f"{self}: Reboot command completed.") + if isinstance(_ret, str): + return True + return False + + async def resume_mining(self) -> bool: + try: + commands = [ + "mkdir -p /etc/tmp/", + 'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root', + "crontab -u root /etc/tmp/root", + "/usr/bin/cgminer-monitor >/dev/null 2>&1", + ] + commands = ";".join(commands) + await self.send_ssh_command(commands) + except (asyncssh.Error, OSError): + return False + else: + return True + + async def stop_mining(self) -> bool: + try: + commands = [ + "mkdir -p /etc/tmp/", + 'echo "" > /etc/tmp/root', + "crontab -u root /etc/tmp/root", + "killall cgminer", + ] + commands = ";".join(commands) + await self.send_ssh_command(commands) + except (asyncssh.Error, OSError): + return False + else: + return True + + async def get_config(self) -> MinerConfig: + api_pools = await self.api.pools() + + if api_pools: + self.config = MinerConfig().from_api(api_pools["POOLS"]) + return self.config + + async def fault_light_off(self) -> bool: + return False + + async def fault_light_on(self) -> bool: + return False + + async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + return None + + async def set_power_limit(self, wattage: int) -> bool: + return False + + ################################################## + ### DATA GATHERING FUNCTIONS (get_{some_data}) ### + ################################################## + + async def get_mac(self) -> Optional[str]: + return None + + async def get_model(self, api_devdetails: dict = None) -> Optional[str]: + if self.model: + logging.debug(f"Found model for {self.ip}: {self.model}") + return self.model + + if not api_devdetails: + try: + api_devdetails = await self.api.devdetails() + except APIError: + pass + + if api_devdetails: + try: + self.model = api_devdetails["DEVDETAILS"][0]["Model"].replace( + "Antminer ", "" + ) + logging.debug(f"Found model for {self.ip}: {self.model}") + return self.model + except (TypeError, IndexError, KeyError): + pass + + logging.warning(f"Failed to get model for miner: {self}") + return None + + async def get_version( + self, api_version: dict = None + ) -> Tuple[Optional[str], Optional[str]]: + miner_version = namedtuple("MinerVersion", "api_ver fw_ver") + return miner_version( + api_ver=await self.get_api_ver(api_version=api_version), + fw_ver=await self.get_fw_ver(api_version=api_version), + ) + + async def get_api_ver(self, api_version: dict = None) -> Optional[str]: + if self.api_ver: + return self.api_ver + + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + if api_version: + try: + self.api_ver = api_version["VERSION"][0]["API"] + except (KeyError, IndexError): + pass + + return self.api_ver + + async def get_fw_ver(self, api_version: dict = None) -> Optional[str]: + if self.fw_ver: + return self.fw_ver + + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + if api_version: + try: + self.fw_ver = api_version["VERSION"][0]["CGMiner"] + except (KeyError, IndexError): + pass + + return self.fw_ver + + async def get_hostname(self) -> Optional[str]: + try: + hn = await self.send_ssh_command("cat /proc/sys/kernel/hostname") + except (asyncssh.Error, OSError): + return None + if hn: + self.hostname = hn + return self.hostname + + async def get_hashrate(self, api_summary: dict = None) -> Optional[float]: + # get hr from API + if not api_summary: + try: + api_summary = await self.api.summary() + except APIError: + pass + + if api_summary: + try: + return round( + float(float(api_summary["SUMMARY"][0]["GHS 5s"]) / 1000), 2 + ) + except (IndexError, KeyError, ValueError, TypeError): + pass + + async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]: + hashboards = [] + + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + try: + board_offset = -1 + boards = api_stats["STATS"] + if len(boards) > 1: + 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 b and not b == 0 and board_offset == -1: + board_offset = board_num + if board_offset == -1: + board_offset = 1 + + for i in range(board_offset, board_offset + self.ideal_hashboards): + hashboard = HashBoard( + slot=i - board_offset, expected_chips=self.nominal_chips + ) + + chip_temp = boards[1].get(f"temp{i}") + if chip_temp: + hashboard.chip_temp = round(chip_temp) + + temp = boards[1].get(f"temp2_{i}") + if temp: + hashboard.temp = round(temp) + + hashrate = boards[1].get(f"chain_rate{i}") + if hashrate: + hashboard.hashrate = round(float(hashrate) / 1000, 2) + + chips = boards[1].get(f"chain_acn{i}") + if chips: + hashboard.chips = chips + hashboard.missing = False + if (not chips) or (not chips > 0): + hashboard.missing = True + hashboards.append(hashboard) + except (IndexError, KeyError, ValueError, TypeError): + pass + + return hashboards + + async def get_env_temp(self) -> Optional[float]: + return None + + async def get_wattage(self) -> Optional[int]: + return None + + async def get_wattage_limit(self) -> Optional[int]: + return None + + async def get_fans(self, api_stats: dict = None) -> List[Fan]: + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + fans_data = [Fan(), Fan(), Fan(), Fan()] + if api_stats: + try: + fan_offset = -1 + + for fan_num in range(1, 8, 4): + for _f_num in range(4): + f = api_stats["STATS"][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): + fans_data[fan] = Fan( + api_stats["STATS"][1].get(f"fan{fan_offset+fan}") + ) + except (KeyError, IndexError): + pass + return fans_data + + async def get_fan_psu(self) -> Optional[int]: + return None + + async def get_pools(self, api_pools: dict = None) -> List[dict]: + groups = [] + + if not api_pools: + try: + api_pools = await self.api.pools() + except APIError: + pass + + if api_pools: + try: + pools = {} + for i, pool in enumerate(api_pools["POOLS"]): + pools[f"pool_{i + 1}_url"] = ( + pool["URL"] + .replace("stratum+tcp://", "") + .replace("stratum2+tcp://", "") + ) + pools[f"pool_{i + 1}_user"] = pool["User"] + pools["quota"] = pool["Quota"] if pool.get("Quota") else "0" + + groups.append(pools) + except KeyError: + pass + return groups + + async def get_errors(self) -> List[MinerErrorData]: + return [] + + async def get_fault_light(self) -> bool: + return False + + async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]: + # X19 method, not sure compatibility + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + try: + ideal_rate = api_stats["STATS"][1]["total_rateideal"] + try: + rate_unit = api_stats["STATS"][1]["rate_unit"] + except KeyError: + rate_unit = "GH" + if rate_unit == "GH": + return round(ideal_rate / 1000, 2) + if rate_unit == "MH": + return round(ideal_rate / 1000000, 2) + else: + return round(ideal_rate, 2) + except (KeyError, IndexError): + pass diff --git a/pyasic/miners/etc/_types/__init__.py b/pyasic/miners/etc/_types/__init__.py new file mode 100644 index 00000000..64091897 --- /dev/null +++ b/pyasic/miners/etc/_types/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from .innosilicon import * diff --git a/pyasic/miners/etc/_types/innosilicon/A10X/A10X.py b/pyasic/miners/etc/_types/innosilicon/A10X/A10X.py new file mode 100644 index 00000000..9759d2c3 --- /dev/null +++ b/pyasic/miners/etc/_types/innosilicon/A10X/A10X.py @@ -0,0 +1,27 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from pyasic.miners.makes import InnosiliconMiner + + +class A10X(InnosiliconMiner): # noqa - ignore ABC method implementation + def __init__(self, ip: str) -> None: + super().__init__() + self.ip = ip + self.model = "A10X" + self.nominal_chips = 9 + self.ideal_hashboards = 4 + self.fan_count = 4 diff --git a/pyasic/miners/etc/_types/innosilicon/A10X/__init__.py b/pyasic/miners/etc/_types/innosilicon/A10X/__init__.py new file mode 100644 index 00000000..25510e24 --- /dev/null +++ b/pyasic/miners/etc/_types/innosilicon/A10X/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from .A10X import A10X diff --git a/pyasic/miners/etc/_types/innosilicon/__init__.py b/pyasic/miners/etc/_types/innosilicon/__init__.py new file mode 100644 index 00000000..10558f91 --- /dev/null +++ b/pyasic/miners/etc/_types/innosilicon/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from .A10X import * diff --git a/pyasic/miners/etc/innosilicon/__init__.py b/pyasic/miners/etc/innosilicon/__init__.py new file mode 100644 index 00000000..5d1891a7 --- /dev/null +++ b/pyasic/miners/etc/innosilicon/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from .cgminer import * diff --git a/pyasic/miners/etc/innosilicon/cgminer/A10X/A10X.py b/pyasic/miners/etc/innosilicon/cgminer/A10X/A10X.py new file mode 100644 index 00000000..dc476dcc --- /dev/null +++ b/pyasic/miners/etc/innosilicon/cgminer/A10X/A10X.py @@ -0,0 +1,357 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +import logging +from typing import List, Optional + +from pyasic.config import MinerConfig +from pyasic.data import Fan, HashBoard +from pyasic.data.error_codes import InnosiliconError, MinerErrorData +from pyasic.errors import APIError +from pyasic.miners.etc._backends import CGMiner # noqa - Ignore access to _module +from pyasic.miners.etc._types import A10X # noqa - Ignore access to _module +from pyasic.web.inno import InnosiliconWebAPI + + +class CGMinerA10X(CGMiner, A10X): + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: + super().__init__(ip, api_ver=api_ver) + self.ip = ip + self.web = InnosiliconWebAPI(ip) + + async def fault_light_on(self) -> bool: + return False + + async def fault_light_off(self) -> bool: + return False + + async def get_config(self, api_pools: dict = None) -> MinerConfig: + if not api_pools: + try: + api_pools = await self.api.pools() + except APIError as e: + logging.warning(e) + + if api_pools: + if "POOLS" in api_pools.keys(): + cfg = MinerConfig().from_api(api_pools["POOLS"]) + self.config = cfg + return self.config + + async def reboot(self) -> bool: + try: + data = await self.web.reboot() + except APIError: + pass + else: + return data["success"] + + async def restart_cgminer(self) -> bool: + try: + data = await self.web.restart_cgminer() + except APIError: + pass + else: + return data["success"] + + async def restart_backend(self) -> bool: + return await self.restart_cgminer() + + async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + self.config = config + await self.web.update_pools(config.as_inno(user_suffix=user_suffix)) + + ################################################## + ### DATA GATHERING FUNCTIONS (get_{some_data}) ### + ################################################## + + async def get_mac( + self, web_get_all: dict = None, web_overview: dict = None + ) -> Optional[str]: + if web_get_all: + web_get_all = web_get_all["all"] + + if not web_get_all and not web_overview: + try: + web_overview = await self.web.overview() + except APIError: + pass + + if web_get_all: + try: + mac = web_get_all["mac"] + return mac.upper() + except KeyError: + pass + + if web_overview: + try: + mac = web_overview["version"]["ethaddr"] + return mac.upper() + except KeyError: + pass + + async def get_model(self, web_type: dict = None) -> Optional[str]: + if self.model: + logging.debug(f"Found model for {self.ip}: {self.model}") + return self.model + + if not web_type: + try: + web_type = await self.web.type() + except APIError: + pass + + if web_type: + try: + self.model = web_type["type"] + return self.model + except KeyError: + pass + + async def get_hashrate( + self, api_summary: dict = None, web_get_all: dict = None + ) -> Optional[float]: + if web_get_all: + web_get_all = web_get_all["all"] + + if not api_summary and not web_get_all: + try: + api_summary = await self.api.summary() + except APIError: + pass + + if web_get_all: + try: + return round(float(web_get_all["total_hash"]["Hash Rate"] / 1000000), 5) + except KeyError: + pass + + if api_summary: + try: + return round( + float(api_summary["SUMMARY"][0]["MHS 1m"] / 1000000000000), 5 + ) + except (KeyError, IndexError): + pass + + async def get_hashboards( + self, api_stats: dict = None, web_get_all: dict = None + ) -> List[HashBoard]: + hashboards = [ + HashBoard(slot=i, expected_chips=self.nominal_chips) + for i in range(self.ideal_hashboards) + ] + if web_get_all: + web_get_all = web_get_all["all"] + + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if not web_get_all: + try: + web_get_all = await self.web.get_all() + except APIError: + pass + else: + web_get_all = web_get_all["all"] + + if api_stats: + if api_stats.get("STATS"): + for board in api_stats["STATS"]: + try: + idx = board["Chain ID"] + chips = board["Num active chips"] + except KeyError: + pass + else: + hashboards[idx].chips = chips + hashboards[idx].missing = False + + if web_get_all: + if web_get_all.get("chain"): + for board in web_get_all["chain"]: + idx = board.get("ASC") + if idx is not None: + temp = board.get("Temp min") + if temp: + hashboards[idx].temp = round(temp) + + hashrate = board.get("Hash Rate H") + if hashrate: + hashboards[idx].hashrate = round( + hashrate / 1000000000000, 5 + ) + + chip_temp = board.get("Temp max") + if chip_temp: + hashboards[idx].chip_temp = round(chip_temp) + + return hashboards + + async def get_wattage( + self, web_get_all: dict = None, api_stats: dict = None + ) -> Optional[int]: + if web_get_all: + web_get_all = web_get_all["all"] + + if not web_get_all: + try: + web_get_all = await self.web.get_all() + except APIError: + pass + else: + web_get_all = web_get_all["all"] + + if web_get_all: + try: + return web_get_all["power"] + except KeyError: + pass + + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + if api_stats.get("STATS"): + for board in api_stats["STATS"]: + try: + wattage = board["power"] + except KeyError: + pass + else: + wattage = int(wattage) + return wattage + + async def get_fans(self, web_get_all: dict = None) -> List[Fan]: + if web_get_all: + web_get_all = web_get_all["all"] + + if not web_get_all: + try: + web_get_all = await self.web.get_all() + except APIError: + pass + else: + web_get_all = web_get_all["all"] + + fan_data = [Fan(), Fan(), Fan(), Fan()] + if web_get_all: + try: + spd = web_get_all["fansSpeed"] + except KeyError: + pass + else: + round((int(spd) * 6000) / 100) + for i in range(self.fan_count): + fan_data[i] = Fan(spd) + + return fan_data + + async def get_pools(self, api_pools: dict = None) -> List[dict]: + groups = [] + + if not api_pools: + try: + api_pools = await self.api.pools() + except APIError: + pass + + if api_pools: + try: + pools = {} + for i, pool in enumerate(api_pools["POOLS"]): + pools[f"pool_{i + 1}_url"] = ( + pool["URL"] + .replace("stratum+tcp://", "") + .replace("stratum2+tcp://", "") + ) + pools[f"pool_{i + 1}_user"] = pool["User"] + pools["quota"] = pool["Quota"] if pool.get("Quota") else "0" + + groups.append(pools) + except KeyError: + pass + return groups + + async def get_errors( + self, web_get_error_detail: dict = None + ) -> List[MinerErrorData]: # noqa: named this way for automatic functionality + errors = [] + if not web_get_error_detail: + try: + web_get_error_detail = await self.web.get_error_detail() + except APIError: + pass + + if web_get_error_detail: + try: + # only 1 error? + # TODO: check if this should be a loop, can't remember. + err = web_get_error_detail["code"] + except KeyError: + pass + else: + err = int(err) + if not err == 0: + errors.append(InnosiliconError(error_code=err)) + return errors + + async def get_fw_ver(self, api_version: dict = None) -> Optional[str]: + if self.fw_ver: + return self.fw_ver + + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + if api_version: + try: + self.fw_ver = api_version["VERSION"][0]["CGMiner"].split("-")[-1:][0] + except (KeyError, IndexError): + pass + + return self.fw_ver + + async def get_wattage_limit(self, web_get_all: dict = None) -> Optional[int]: + if web_get_all: + web_get_all = web_get_all["all"] + + if not web_get_all: + try: + web_get_all = await self.web.get_all() + except APIError: + pass + else: + web_get_all = web_get_all["all"] + + if web_get_all: + try: + level = web_get_all["running_mode"]["level"] + except KeyError: + pass + else: + # this is very possibly not correct. + level = int(level) + limit = 1250 + (250 * level) + return limit diff --git a/pyasic/miners/etc/innosilicon/cgminer/A10X/__init__.py b/pyasic/miners/etc/innosilicon/cgminer/A10X/__init__.py new file mode 100644 index 00000000..9d7075b9 --- /dev/null +++ b/pyasic/miners/etc/innosilicon/cgminer/A10X/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from .A10X import CGMinerA10X diff --git a/pyasic/miners/etc/innosilicon/cgminer/__init__.py b/pyasic/miners/etc/innosilicon/cgminer/__init__.py new file mode 100644 index 00000000..10558f91 --- /dev/null +++ b/pyasic/miners/etc/innosilicon/cgminer/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +from .A10X import * diff --git a/pyasic/miners/kda/__init__.py b/pyasic/miners/kda/__init__.py new file mode 100644 index 00000000..d6d7eef1 --- /dev/null +++ b/pyasic/miners/kda/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .goldshell import * diff --git a/pyasic/miners/kda/_backends/__init__.py b/pyasic/miners/kda/_backends/__init__.py new file mode 100644 index 00000000..45ee0db4 --- /dev/null +++ b/pyasic/miners/kda/_backends/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .bfgminer import BFGMiner +from .goldshell import Goldshell diff --git a/pyasic/miners/kda/_backends/bfgminer.py b/pyasic/miners/kda/_backends/bfgminer.py new file mode 100644 index 00000000..8de9a6c5 --- /dev/null +++ b/pyasic/miners/kda/_backends/bfgminer.py @@ -0,0 +1,318 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ + +import ipaddress +import logging +from collections import namedtuple +from typing import List, Optional, Tuple + +from pyasic.API.bfgminer import BFGMinerAPI +from pyasic.config import MinerConfig +from pyasic.data import Fan, HashBoard +from pyasic.data.error_codes import MinerErrorData +from pyasic.errors import APIError +from pyasic.miners.base import BaseMiner + + +class BFGMiner(BaseMiner): + """Base handler for BFGMiner based miners.""" + + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: + super().__init__(ip) + self.ip = ipaddress.ip_address(ip) + self.api = BFGMinerAPI(ip, api_ver) + self.api_type = "BFGMiner" + self.api_ver = api_ver + self.uname = "root" + self.pwd = "admin" + + async def get_config(self) -> MinerConfig: + # get pool data + try: + pools = await self.api.pools() + except APIError: + return self.config + + self.config = MinerConfig().from_api(pools["POOLS"]) + return self.config + + async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + return None + + async def fault_light_off(self) -> bool: + return False + + async def fault_light_on(self) -> bool: + return False + + async def restart_backend(self) -> bool: + return False + + async def stop_mining(self) -> bool: + return False + + async def resume_mining(self) -> bool: + return False + + async def set_power_limit(self, wattage: int) -> bool: + return False + + ################################################## + ### DATA GATHERING FUNCTIONS (get_{some_data}) ### + ################################################## + + async def get_mac(self) -> str: + return "00:00:00:00:00:00" + + async def get_model(self, api_devdetails: dict = None) -> Optional[str]: + if self.model: + logging.debug(f"Found model for {self.ip}: {self.model}") + return self.model + + if not api_devdetails: + try: + api_devdetails = await self.api.devdetails() + except APIError: + pass + + if api_devdetails: + try: + self.model = api_devdetails["DEVDETAILS"][0]["Model"].replace( + "Antminer ", "" + ) + logging.debug(f"Found model for {self.ip}: {self.model}") + return self.model + except (TypeError, IndexError, KeyError): + pass + + logging.warning(f"Failed to get model for miner: {self}") + return None + + async def get_api_ver(self, api_version: dict = None) -> Optional[str]: + # Check to see if the version info is already cached + if self.api_ver: + return self.api_ver + + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + if api_version: + try: + self.api_ver = api_version["VERSION"][0]["API"] + except (KeyError, IndexError): + pass + + return self.api_ver + + async def get_fw_ver(self, api_version: dict = None) -> Optional[str]: + # Check to see if the version info is already cached + if self.fw_ver: + return self.fw_ver + + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + if api_version: + try: + self.fw_ver = api_version["VERSION"][0]["CompileTime"] + except (KeyError, IndexError): + pass + + return self.fw_ver + + async def get_version( + self, api_version: dict = None + ) -> Tuple[Optional[str], Optional[str]]: + # check if version is cached + miner_version = namedtuple("MinerVersion", "api_ver fw_ver") + return miner_version( + api_ver=await self.get_api_ver(api_version), + fw_ver=await self.get_fw_ver(api_version=api_version), + ) + + async def reboot(self) -> bool: + return False + + async def get_fan_psu(self): + return None + + async def get_hostname(self) -> Optional[str]: + return None + + async def get_hashrate(self, api_summary: dict = None) -> Optional[float]: + # get hr from API + if not api_summary: + try: + api_summary = await self.api.summary() + except APIError: + pass + + if api_summary: + try: + return round(float(api_summary["SUMMARY"][0]["MHS 20s"] / 1000000), 2) + except (IndexError, KeyError, ValueError, TypeError): + pass + + async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]: + hashboards = [] + + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + try: + board_offset = -1 + boards = api_stats["STATS"] + if len(boards) > 1: + 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 b and not b == 0 and board_offset == -1: + board_offset = board_num + if board_offset == -1: + board_offset = 1 + + for i in range(board_offset, board_offset + self.ideal_hashboards): + hashboard = HashBoard( + slot=i - board_offset, expected_chips=self.nominal_chips + ) + + chip_temp = boards[1].get(f"temp{i}") + if chip_temp: + hashboard.chip_temp = round(chip_temp) + + temp = boards[1].get(f"temp2_{i}") + if temp: + hashboard.temp = round(temp) + + hashrate = boards[1].get(f"chain_rate{i}") + if hashrate: + hashboard.hashrate = round(float(hashrate) / 1000, 2) + + chips = boards[1].get(f"chain_acn{i}") + if chips: + hashboard.chips = chips + hashboard.missing = False + if (not chips) or (not chips > 0): + hashboard.missing = True + hashboards.append(hashboard) + except (IndexError, KeyError, ValueError, TypeError): + pass + + return hashboards + + async def get_env_temp(self) -> Optional[float]: + return None + + async def get_wattage(self) -> Optional[int]: + return None + + async def get_wattage_limit(self) -> Optional[int]: + return None + + async def get_fans(self, api_stats: dict = None) -> List[Fan]: + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + fans_data = [None, None, None, None] + if api_stats: + try: + fan_offset = -1 + + for fan_num in range(0, 8, 4): + for _f_num in range(4): + f = api_stats["STATS"][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): + fans_data[fan] = api_stats["STATS"][1].get(f"fan{fan_offset+fan}") + except (KeyError, IndexError): + pass + fans = [Fan(speed=d) if d else Fan() for d in fans_data] + + return fans + + async def get_pools(self, api_pools: dict = None) -> List[dict]: + groups = [] + + if not api_pools: + try: + api_pools = await self.api.pools() + except APIError: + pass + + if api_pools: + try: + pools = {} + for i, pool in enumerate(api_pools["POOLS"]): + pools[f"pool_{i + 1}_url"] = ( + pool["URL"] + .replace("stratum+tcp://", "") + .replace("stratum2+tcp://", "") + ) + pools[f"pool_{i + 1}_user"] = pool["User"] + pools["quota"] = pool["Quota"] if pool.get("Quota") else "0" + + groups.append(pools) + except KeyError: + pass + return groups + + async def get_errors(self) -> List[MinerErrorData]: + return [] + + async def get_fault_light(self) -> bool: + return False + + async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]: + # X19 method, not sure compatibility + if not api_stats: + try: + api_stats = await self.api.stats() + except APIError: + pass + + if api_stats: + try: + ideal_rate = api_stats["STATS"][1]["total_rateideal"] + try: + rate_unit = api_stats["STATS"][1]["rate_unit"] + except KeyError: + rate_unit = "GH" + if rate_unit == "GH": + return round(ideal_rate / 1000, 2) + if rate_unit == "MH": + return round(ideal_rate / 1000000, 2) + else: + return round(ideal_rate, 2) + except (KeyError, IndexError): + pass diff --git a/pyasic/miners/kda/_backends/goldshell.py b/pyasic/miners/kda/_backends/goldshell.py new file mode 100644 index 00000000..3e42ca00 --- /dev/null +++ b/pyasic/miners/kda/_backends/goldshell.py @@ -0,0 +1,101 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from typing import List, Optional + +from pyasic.config import MinerConfig +from pyasic.data import Fan, HashBoard +from pyasic.errors import APIError +from pyasic.miners.hns._backends import BFGMiner +from pyasic.web.goldshell import GoldshellWebAPI + + +class Goldshell(BFGMiner): + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: + super().__init__(ip, api_ver) + self.web = GoldshellWebAPI(ip) + + async def get_config(self) -> MinerConfig: + return MinerConfig().from_raw(await self.web.pools()) + + async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + pools_data = await self.web.pools() + # have to delete all the pools one at a time first + for pool in pools_data: + await self.web.delpool( + url=pool["url"], + user=pool["user"], + password=pool["pass"], + dragid=pool["dragid"], + ) + + self.config = config + + # send them back 1 at a time + for pool in config.as_goldshell(user_suffix=user_suffix): + await self.web.newpool( + url=pool["url"], user=pool["user"], password=pool["pass"] + ) + + async def get_mac(self, web_setting: dict = None) -> str: + if not web_setting: + try: + web_setting = await self.web.setting() + except APIError: + pass + + if web_setting: + try: + return web_setting["name"] + except KeyError: + pass + + async def get_fw_ver(self, web_status: dict = None) -> str: + if not web_status: + try: + web_status = await self.web.setting() + except APIError: + pass + + if web_status: + try: + return web_status["firmware"] + except KeyError: + pass + + async def get_hashboards(self, api_devs: dict = None) -> List[HashBoard]: + if not api_devs: + try: + api_devs = await self.api.devs() + except APIError: + pass + + hashboards = [ + HashBoard(slot=i, expected_chips=self.nominal_chips) + for i in range(self.ideal_hashboards) + ] + + if api_devs: + for board in api_devs["DEVS"]: + if board.get("ID"): + try: + b_id = board["ID"] + hashboards[b_id].hashrate = round(board["MHS 20s"] / 1000000, 2) + hashboards[b_id].temp = board["tstemp-2"] + hashboards[b_id].missing = False + except KeyError: + pass + + return hashboards diff --git a/pyasic/miners/kda/_types/__init__.py b/pyasic/miners/kda/_types/__init__.py new file mode 100644 index 00000000..d6d7eef1 --- /dev/null +++ b/pyasic/miners/kda/_types/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .goldshell import * diff --git a/pyasic/miners/kda/_types/goldshell/KD5.py b/pyasic/miners/kda/_types/goldshell/KD5.py new file mode 100644 index 00000000..d0bb5961 --- /dev/null +++ b/pyasic/miners/kda/_types/goldshell/KD5.py @@ -0,0 +1,26 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from pyasic.miners.makes import GoldshellMiner + + +class KD5(GoldshellMiner): # noqa - ignore ABC method implementation + def __init__(self, ip: str): + super().__init__() + self.ip = ip + self.model = "KD5" + self.ideal_hashboards = 4 + self.chip_count = 18 + self.fan_count = 4 diff --git a/pyasic/miners/kda/_types/goldshell/KDMax.py b/pyasic/miners/kda/_types/goldshell/KDMax.py new file mode 100644 index 00000000..354e46db --- /dev/null +++ b/pyasic/miners/kda/_types/goldshell/KDMax.py @@ -0,0 +1,26 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from pyasic.miners.makes import GoldshellMiner + + +class KDMax(GoldshellMiner): # noqa - ignore ABC method implementation + def __init__(self, ip: str): + super().__init__() + self.ip = ip + self.model = "KD Max" + self.ideal_hashboards = 4 + self.chip_count = 18 + self.fan_count = 4 diff --git a/pyasic/miners/kda/_types/goldshell/__init__.py b/pyasic/miners/kda/_types/goldshell/__init__.py new file mode 100644 index 00000000..7d6e4589 --- /dev/null +++ b/pyasic/miners/kda/_types/goldshell/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .KD5 import KD5 +from .KDMax import KDMax diff --git a/pyasic/miners/kda/goldshell/__init__.py b/pyasic/miners/kda/goldshell/__init__.py new file mode 100644 index 00000000..75327f3a --- /dev/null +++ b/pyasic/miners/kda/goldshell/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .bfgminer import * diff --git a/pyasic/miners/kda/goldshell/bfgminer/KDX/KD5.py b/pyasic/miners/kda/goldshell/bfgminer/KDX/KD5.py new file mode 100644 index 00000000..a9b682fc --- /dev/null +++ b/pyasic/miners/kda/goldshell/bfgminer/KDX/KD5.py @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from pyasic.miners.kda._backends import Goldshell # noqa - Ignore access to _module +from pyasic.miners.kda._types import KD5 # noqa - Ignore access to _module + + +class BFGMinerKD5(Goldshell, KD5): + pass diff --git a/pyasic/miners/kda/goldshell/bfgminer/KDX/KDMax.py b/pyasic/miners/kda/goldshell/bfgminer/KDX/KDMax.py new file mode 100644 index 00000000..578601b2 --- /dev/null +++ b/pyasic/miners/kda/goldshell/bfgminer/KDX/KDMax.py @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from pyasic.miners.kda._backends import Goldshell # noqa - Ignore access to _module +from pyasic.miners.kda._types import KDMax # noqa - Ignore access to _module + + +class BFGMinerKDMax(Goldshell, KDMax): + pass diff --git a/pyasic/miners/kda/goldshell/bfgminer/KDX/__init__.py b/pyasic/miners/kda/goldshell/bfgminer/KDX/__init__.py new file mode 100644 index 00000000..6b445d19 --- /dev/null +++ b/pyasic/miners/kda/goldshell/bfgminer/KDX/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .KD5 import BFGMinerKD5 +from .KDMax import BFGMinerKDMax diff --git a/pyasic/miners/kda/goldshell/bfgminer/__init__.py b/pyasic/miners/kda/goldshell/bfgminer/__init__.py new file mode 100644 index 00000000..5902568b --- /dev/null +++ b/pyasic/miners/kda/goldshell/bfgminer/__init__.py @@ -0,0 +1,16 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +from .KDX import * diff --git a/pyasic/miners/miner_factory.py b/pyasic/miners/miner_factory.py index f26d1a83..b7e063e0 100644 --- a/pyasic/miners/miner_factory.py +++ b/pyasic/miners/miner_factory.py @@ -34,7 +34,11 @@ from pyasic.miners.btc._backends.bosminer_old import ( # noqa - Ignore _module BOSMinerOld, ) from pyasic.miners.btc._backends.btminer import BTMiner # noqa - Ignore _module import +from pyasic.miners.ckb import * +from pyasic.miners.dcr import * +from pyasic.miners.etc import * from pyasic.miners.hns import * +from pyasic.miners.kda import * from pyasic.miners.ltc import * from pyasic.miners.unknown import UnknownMiner from pyasic.miners.zec import * @@ -42,6 +46,10 @@ from pyasic.misc import Singleton from pyasic.settings import PyasicSettings MINER_CLASSES = { + "ANTMINER DR5": { + "Default": CGMinerDR5, + "CGMiner": CGMinerDR5, + }, "ANTMINER L7": { "Default": BMMinerL7, "BMMiner": BMMinerL7, @@ -158,11 +166,26 @@ MINER_CLASSES = { "CGMiner": CGMinerT19, "VNish": VNishT19, }, + "GOLDSHELL CK5": { + "Default": BFGMinerCK5, + "BFGMiner": BFGMinerCK5, + "CGMiner": BFGMinerCK5, + }, "GOLDSHELL HS5": { "Default": BFGMinerHS5, "BFGMiner": BFGMinerHS5, "CGMiner": BFGMinerHS5, }, + "GOLDSHELL KD5": { + "Default": BFGMinerKD5, + "BFGMiner": BFGMinerKD5, + "CGMiner": BFGMinerKD5, + }, + "GOLDSHELL KDMAX": { + "Default": BFGMinerKDMax, + "BFGMiner": BFGMinerKDMax, + "CGMiner": BFGMinerKDMax, + }, "M20": {"Default": BTMinerM20V10, "BTMiner": BTMinerM20V10, "10": BTMinerM20V10}, "M20S": { "Default": BTMinerM20SV10, @@ -516,6 +539,10 @@ MINER_CLASSES = { "Default": CGMinerInnosiliconT3HPlus, "CGMiner": CGMinerInnosiliconT3HPlus, }, + "A10X": { + "Default": CGMinerA10X, + "CGMiner": CGMinerA10X, + }, "Unknown": {"Default": UnknownMiner}, } @@ -829,7 +856,7 @@ class MinerFactory(metaclass=Singleton): model = _model if model: - if "DRAGONMINT" in model: + if "DRAGONMINT" in model or "A10" in model: _model = await self.__get_dragonmint_version_from_web(ip) if _model: model = _model diff --git a/pyasic/settings/__init__.py b/pyasic/settings/__init__.py index fa8da746..9309d534 100644 --- a/pyasic/settings/__init__.py +++ b/pyasic/settings/__init__.py @@ -33,10 +33,11 @@ class PyasicSettings(metaclass=Singleton): global_innosilicon_password = "admin" global_x19_password = "root" global_x17_password = "root" + global_x15_password = "root" + global_x7_password = "root" + global_x5_password = "root" global_vnish_password = "admin" - global_goldshell_password = ( - "123456789" # "bbad7537f4c8b6ea31eea0b3d760e257" in ciphertext - ) + global_goldshell_password = "123456789" debug: bool = False logfile: bool = False diff --git a/pyasic/web/X15.py b/pyasic/web/X15.py index 4282d1f3..83eee9cf 100644 --- a/pyasic/web/X15.py +++ b/pyasic/web/X15.py @@ -25,7 +25,7 @@ from pyasic.web import BaseWebAPI class X15WebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) - self.pwd = PyasicSettings().global_x17_password + self.pwd = PyasicSettings().global_x15_password async def send_command( self, diff --git a/pyasic/web/X5.py b/pyasic/web/X5.py new file mode 100644 index 00000000..87fbc365 --- /dev/null +++ b/pyasic/web/X5.py @@ -0,0 +1,72 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +import json +from typing import Union + +import httpx + +from pyasic.settings import PyasicSettings +from pyasic.web import BaseWebAPI + + +class X5WebAPI(BaseWebAPI): + def __init__(self, ip: str) -> None: + super().__init__(ip) + self.pwd = PyasicSettings().global_x5_password + + async def send_command( + self, + command: Union[str, bytes], + ignore_errors: bool = False, + allow_warning: bool = True, + **parameters: Union[str, int, bool], + ) -> dict: + url = f"http://{self.ip}/cgi-bin/{command}.cgi" + auth = httpx.DigestAuth(self.username, self.pwd) + try: + async with httpx.AsyncClient() as client: + if parameters: + data = await client.post(url, data=parameters, auth=auth) + else: + data = await client.get(url, auth=auth) + except httpx.HTTPError: + pass + else: + if data.status_code == 200: + try: + return data.json() + except json.decoder.JSONDecodeError: + pass + + async def get_system_info(self) -> dict: + return await self.send_command("get_system_info") + + async def blink(self, blink: bool) -> dict: + if blink: + return await self.send_command("blink", action="startBlink") + return await self.send_command("blink", action="stopBlink") + + async def reboot(self) -> dict: + return await self.send_command("reboot") + + async def get_blink_status(self) -> dict: + return await self.send_command("blink", action="onPageLoaded") + + async def get_miner_conf(self) -> dict: + return await self.send_command("get_miner_conf") + + async def set_miner_conf(self, conf: dict) -> dict: + return await self.send_command("set_miner_conf", **conf) diff --git a/pyasic/web/X7.py b/pyasic/web/X7.py index 2cffcf1f..975aa5f5 100644 --- a/pyasic/web/X7.py +++ b/pyasic/web/X7.py @@ -25,7 +25,7 @@ from pyasic.web import BaseWebAPI class X7WebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) - self.pwd = PyasicSettings().global_x19_password + self.pwd = PyasicSettings().global_x7_password async def send_command( self,