diff --git a/pyasic/config/__init__.py b/pyasic/config/__init__.py index e29641fe..502d328e 100644 --- a/pyasic/config/__init__.py +++ b/pyasic/config/__init__.py @@ -91,6 +91,19 @@ class _Pool: pool = {"url": self.url, "user": username, "pass": self.password} return pool + def as_x15(self, user_suffix: str = None) -> dict: + """Convert the data in this class to a dict usable by an X19 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_inno(self, user_suffix: str = None) -> dict: """Convert the data in this class to a dict usable by an Innosilicon device. @@ -188,6 +201,34 @@ class _PoolGroup: pools.append(pool.as_x19(user_suffix=user_suffix)) return pools + def as_x15(self, user_suffix: str = None) -> dict: + """Convert the data in this class to a list usable by an X15 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_x15(user_suffix=user_suffix)["url"] + pools[f"_ant_pool{idx+1}user"] = pool.as_x15(user_suffix=user_suffix)[ + "user" + ] + pools[f"_ant_pool{idx+1}pw"] = pool.as_x15(user_suffix=user_suffix)[ + "pass" + ] + + return pools + def as_inno(self, user_suffix: str = None) -> dict: """Convert the data in this class to a list usable by an Innosilicon device. @@ -482,6 +523,16 @@ class MinerConfig: return cfg + def as_x15(self, user_suffix: str = None) -> dict: + """Convert the data in this class to a config usable by an X15 device. + + Parameters: + user_suffix: The suffix to append to username. + """ + cfg = self.pool_groups[0].as_x15(user_suffix=user_suffix) + + return cfg + def as_avalon(self, user_suffix: str = None) -> str: """Convert the data in this class to a config usable by an Avalonminer device. diff --git a/pyasic/miners/_backends/X15.py b/pyasic/miners/_backends/X15.py new file mode 100644 index 00000000..55cc427a --- /dev/null +++ b/pyasic/miners/_backends/X15.py @@ -0,0 +1,176 @@ +# ------------------------------------------------------------------------------ +# 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 Union + +from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module +from pyasic.web.X15 import X15WebAPI +from typing import List +from pyasic.data import Fan, HashBoard +from pyasic.errors import APIError +from pyasic.config import MinerConfig, X19PowerMode + + +class X15(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 = X15WebAPI(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_x15(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) -> bool: + if self.light: + return self.light + try: + data = await self.web.get_blink_status() + if data: + self.light = data["isBlinking"] + except KeyError: + pass + return self.light + + async def get_hostname(self) -> Union[str, None]: + try: + data = await self.web.get_system_info() + if data: + return data["hostname"] + except KeyError: + 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), 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/_backends/__init__.py b/pyasic/miners/_backends/__init__.py index 28645520..f8562a3f 100644 --- a/pyasic/miners/_backends/__init__.py +++ b/pyasic/miners/_backends/__init__.py @@ -23,3 +23,4 @@ from .hiveon import Hiveon from .vnish import VNish from .X19 import X19 from .X7 import X7 +from .X15 import X15 diff --git a/pyasic/miners/_backends/cgminer.py b/pyasic/miners/_backends/cgminer.py index c6b4701c..7fd898b2 100644 --- a/pyasic/miners/_backends/cgminer.py +++ b/pyasic/miners/_backends/cgminer.py @@ -128,12 +128,8 @@ class CGMiner(BaseMiner): else: return True - async def get_config(self, api_pools: dict = None) -> MinerConfig: - # get pool data - try: - api_pools = await self.api.pools() - except APIError: - pass + async def get_config(self) -> MinerConfig: + api_pools = await self.api.pools() if api_pools: self.config = MinerConfig().from_api(api_pools["POOLS"]) diff --git a/pyasic/miners/_types/antminer/X15/Z15.py b/pyasic/miners/_types/antminer/X15/Z15.py new file mode 100644 index 00000000..851ecf1c --- /dev/null +++ b/pyasic/miners/_types/antminer/X15/Z15.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._types.makes import AntMiner + + +class Z15(AntMiner): # noqa - ignore ABC method implementation + def __init__(self, ip: str): + super().__init__() + self.ip = ip + self.model = "Z15" + self.nominal_chips = 3 + self.fan_count = 2 diff --git a/pyasic/miners/_types/antminer/X15/__init__.py b/pyasic/miners/_types/antminer/X15/__init__.py new file mode 100644 index 00000000..2fdccb75 --- /dev/null +++ b/pyasic/miners/_types/antminer/X15/__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 .Z15 import * \ No newline at end of file diff --git a/pyasic/miners/_types/antminer/__init__.py b/pyasic/miners/_types/antminer/__init__.py index 809024d7..bac87eb9 100644 --- a/pyasic/miners/_types/antminer/__init__.py +++ b/pyasic/miners/_types/antminer/__init__.py @@ -15,5 +15,6 @@ # ------------------------------------------------------------------------------ from .X7 import * from .X9 import * +from .X15 import * from .X17 import * from .X19 import * diff --git a/pyasic/miners/antminer/cgminer/X15/Z15.py b/pyasic/miners/antminer/cgminer/X15/Z15.py new file mode 100644 index 00000000..4b30fc07 --- /dev/null +++ b/pyasic/miners/antminer/cgminer/X15/Z15.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._backends import X15 # noqa - Ignore access to _module +from pyasic.miners._types import Z15 # noqa - Ignore access to _module + + +class CGMinerZ15(X15, Z15): + pass diff --git a/pyasic/miners/antminer/cgminer/X15/__init__.py b/pyasic/miners/antminer/cgminer/X15/__init__.py new file mode 100644 index 00000000..01ed451c --- /dev/null +++ b/pyasic/miners/antminer/cgminer/X15/__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 .Z15 import CGMinerZ15 \ No newline at end of file diff --git a/pyasic/miners/antminer/cgminer/__init__.py b/pyasic/miners/antminer/cgminer/__init__.py index 2d43e0e4..f1bc3e6a 100644 --- a/pyasic/miners/antminer/cgminer/__init__.py +++ b/pyasic/miners/antminer/cgminer/__init__.py @@ -15,5 +15,6 @@ # ------------------------------------------------------------------------------ from .X9 import * +from .X15 import * from .X17 import * from .X19 import * diff --git a/pyasic/miners/miner_factory.py b/pyasic/miners/miner_factory.py index 4f767dad..0d94f4dd 100644 --- a/pyasic/miners/miner_factory.py +++ b/pyasic/miners/miner_factory.py @@ -63,6 +63,10 @@ MINER_CLASSES = { "Hiveon": HiveonT9, "CGMiner": CGMinerT9, }, + "ANTMINER Z15": { + "Default": CGMinerZ15, + "CGMiner": CGMinerZ15, + }, "ANTMINER S17": { "Default": BMMinerS17, "BOSMiner+": BOSMinerS17, diff --git a/pyasic/web/X15.py b/pyasic/web/X15.py new file mode 100644 index 00000000..4282d1f3 --- /dev/null +++ b/pyasic/web/X15.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 X15WebAPI(BaseWebAPI): + def __init__(self, ip: str) -> None: + super().__init__(ip) + self.pwd = PyasicSettings().global_x17_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)