Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62b14a78b7 | ||
|
|
0ff505bbb4 | ||
|
|
b6c8c930a2 | ||
|
|
903bb93c4e | ||
|
|
59667cf104 | ||
|
|
3fd1b41bec | ||
|
|
6569107f64 | ||
|
|
9d746a6dcb | ||
|
|
fce4c07c32 | ||
|
|
094857758a |
10
docs/miners/innosilicon/T3X.md
Normal file
10
docs/miners/innosilicon/T3X.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# pyasic
|
||||
## T3X Models
|
||||
|
||||
## T3H+
|
||||
|
||||
::: pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
@@ -37,6 +37,7 @@ Supported miner types are here on this list. If your miner (or miner version) i
|
||||
* [M30S++][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlus]:
|
||||
* [VG30][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30]
|
||||
* [VG40][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40]
|
||||
* [VH60][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVH60]
|
||||
* [M31S][pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S]
|
||||
* [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]:
|
||||
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20]
|
||||
@@ -90,3 +91,6 @@ Supported miner types are here on this list. If your miner (or miner version) i
|
||||
* [A1026][pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026]
|
||||
* [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047]
|
||||
* [A1066][pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066]
|
||||
* Stock Firmware Innosilicon Miners:
|
||||
* T3X Series:
|
||||
* [T3H+][pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus]
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M30S+VG40
|
||||
## M30S++VG40
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
|
||||
handler: python
|
||||
@@ -97,6 +97,14 @@
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M30S++VH60
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVH60
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
|
||||
## M31S
|
||||
|
||||
@@ -130,7 +138,7 @@
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M32
|
||||
## M32V20
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20
|
||||
handler: python
|
||||
|
||||
@@ -22,6 +22,7 @@ nav:
|
||||
- Avalon 10X: "miners/avalonminer/A10X.md"
|
||||
- Whatsminer M2X: "miners/whatsminer/M2X.md"
|
||||
- Whatsminer M3X: "miners/whatsminer/M3X.md"
|
||||
- Innosilicon T3X: "miners/innosilicon/T3X.md"
|
||||
|
||||
- Network:
|
||||
- Miner Network: "network/miner_network.md"
|
||||
|
||||
@@ -78,6 +78,23 @@ class _Pool:
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
username = self.username
|
||||
if user_suffix:
|
||||
username = f"{username}{user_suffix}"
|
||||
|
||||
pool = {
|
||||
f"Pool": self.url,
|
||||
f"UserName": username,
|
||||
f"Password": self.password,
|
||||
}
|
||||
return pool
|
||||
|
||||
def as_avalon(self, user_suffix: str = None) -> str:
|
||||
"""Convert the data in this class to a string usable by an Avalonminer device.
|
||||
|
||||
@@ -154,6 +171,19 @@ class _PoolGroup:
|
||||
pools.append(pool.as_x19(user_suffix=user_suffix))
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
pools = {}
|
||||
for idx, pool in enumerate(self.pools[:3]):
|
||||
pool_data = pool.as_inno(user_suffix=user_suffix)
|
||||
for key in pool_data:
|
||||
pools[f"{key}{idx+1}"] = pool_data[key]
|
||||
return pools
|
||||
|
||||
def as_wm(self, user_suffix: str = None) -> List[dict]:
|
||||
"""Convert the data in this class to a list usable by an Whatsminer device.
|
||||
|
||||
@@ -359,7 +389,15 @@ class MinerConfig:
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
return self.pool_groups[0].as_x19(user_suffix=user_suffix)
|
||||
return self.pool_groups[0].as_wm(user_suffix=user_suffix)
|
||||
|
||||
def as_inno(self, user_suffix: str = None) -> dict:
|
||||
"""Convert the data in this class to a config usable by an Innosilicon device.
|
||||
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
|
||||
|
||||
def as_x19(self, user_suffix: str = None) -> str:
|
||||
"""Convert the data in this class to a config usable by an X19 device.
|
||||
|
||||
@@ -18,7 +18,7 @@ from datetime import datetime, timezone
|
||||
import time
|
||||
import json
|
||||
|
||||
from .error_codes import X19Error, WhatsminerError, BraiinsOSError
|
||||
from .error_codes import X19Error, WhatsminerError, BraiinsOSError, InnosiliconError
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -54,7 +54,7 @@ class MinerData:
|
||||
total_chips: The total number of chips on all boards. Calculated automatically.
|
||||
ideal_chips: The ideal number of chips in the miner as an int.
|
||||
percent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
|
||||
nominal: The nominal amount of chips in the miner. Calculated automatically.
|
||||
nominal: Whether the number of chips in the miner is nominal. Calculated automatically.
|
||||
pool_split: The pool split as a str.
|
||||
pool_1_url: The first pool url on the miner as a str.
|
||||
pool_1_user: The first pool user on the miner as a str.
|
||||
@@ -100,9 +100,9 @@ class MinerData:
|
||||
pool_1_user: str = "Unknown"
|
||||
pool_2_url: str = ""
|
||||
pool_2_user: str = ""
|
||||
errors: List[Union[WhatsminerError, BraiinsOSError, X19Error]] = field(
|
||||
default_factory=list
|
||||
)
|
||||
errors: List[
|
||||
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
|
||||
] = field(default_factory=list)
|
||||
fault_light: Union[bool, None] = None
|
||||
efficiency: int = field(init=False)
|
||||
|
||||
@@ -119,7 +119,7 @@ class MinerData:
|
||||
return setattr(self, key, value)
|
||||
|
||||
def __iter__(self):
|
||||
return iter([item for item in self.__dict__])
|
||||
return iter([item for item in self.asdict()])
|
||||
|
||||
@property
|
||||
def total_chips(self): # noqa - Skip PyCharm inspection
|
||||
@@ -208,6 +208,9 @@ class MinerData:
|
||||
if attribute == "fault_light" and not self[attribute]:
|
||||
field_data.append(f"{attribute}=false")
|
||||
continue
|
||||
if attribute == "errors":
|
||||
for idx, item in enumerate(self[attribute]):
|
||||
field_data.append(f'error_{idx+1}="{item.error_message}"')
|
||||
|
||||
tags_str = ",".join(tag_data)
|
||||
field_str = ",".join(field_data)
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
from .whatsminer import WhatsminerError
|
||||
from .bos import BraiinsOSError
|
||||
from .X19 import X19Error
|
||||
from .innosilicon import InnosiliconError
|
||||
|
||||
65
pyasic/data/error_codes/innosilicon.py
Normal file
65
pyasic/data/error_codes/innosilicon.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# 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 dataclasses import dataclass, field, asdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class InnosiliconError:
|
||||
"""A Dataclass to handle error codes of Innosilicon miners.
|
||||
|
||||
Attributes:
|
||||
error_code: The error code as an int.
|
||||
error_message: The error message as a string. Automatically found from the error code.
|
||||
"""
|
||||
|
||||
error_code: int
|
||||
error_message: str = field(init=False)
|
||||
|
||||
@property
|
||||
def error_message(self): # noqa - Skip PyCharm inspection
|
||||
if self.error_code in ERROR_CODES:
|
||||
return ERROR_CODES[self.error_code]
|
||||
return "Unknown error type."
|
||||
|
||||
@error_message.setter
|
||||
def error_message(self, val):
|
||||
pass
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
|
||||
ERROR_CODES = {
|
||||
21: "The PLUG signal of the hash board is not detected.",
|
||||
22: "Power I2C communication is abnormal.",
|
||||
23: "The SPI of all hash boards is blocked.",
|
||||
24: "Some of the hash boards fail to connect to the SPI'.",
|
||||
25: "Hashboard failed to set frequency.",
|
||||
26: "Hashboard failed to set voltage.",
|
||||
27: "Chip BIST test failed.",
|
||||
28: "Hashboard SPI communication is abnormal.",
|
||||
29: "Power I2C communication is abnormal.",
|
||||
30: "Pool connection failed.",
|
||||
31: "Individual chips are damaged.",
|
||||
32: "Over temperature protection.",
|
||||
33: "Hashboard fault.",
|
||||
34: "The data cables are not connected in the correct order.",
|
||||
35: "No power output.",
|
||||
36: "Hashboard fault.",
|
||||
37: "Control board and/or hashboard do not match.",
|
||||
40: "Power output is abnormal.",
|
||||
41: "Power output is abnormal.",
|
||||
42: "Hashboard fault.",
|
||||
}
|
||||
@@ -152,6 +152,7 @@ ERROR_CODES = {
|
||||
2020: "Pool 0 connection failed.",
|
||||
2021: "Pool 1 connection failed.",
|
||||
2022: "Pool 2 connection failed.",
|
||||
2023: "Pool 3 connection failed.",
|
||||
2030: "High rejection rate on pool.",
|
||||
2040: "The pool does not support asicboost mode.",
|
||||
2310: "Hashrate is too low.",
|
||||
|
||||
@@ -232,8 +232,12 @@ class BOSMiner(BaseMiner):
|
||||
await conn.run("/etc/init.d/bosminer start")
|
||||
|
||||
async def check_light(self) -> bool:
|
||||
if not self.light:
|
||||
self.light = False
|
||||
if self.light:
|
||||
return self.light
|
||||
data = await self.send_ssh_command("ls /sys/class/leds/'Red LED'/")
|
||||
self.light = False
|
||||
if "delay_on" in data:
|
||||
self.light = True
|
||||
return self.light
|
||||
|
||||
async def get_errors(self) -> list:
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
from .antminer import *
|
||||
from .avalonminer import *
|
||||
from .whatsminer import *
|
||||
from .innosilicon import *
|
||||
|
||||
24
pyasic/miners/_types/innosilicon/T3X/T3H_Plus.py
Normal file
24
pyasic/miners/_types/innosilicon/T3X/T3H_Plus.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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.base import BaseMiner
|
||||
|
||||
|
||||
class InnosiliconT3HPlus(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "T3H+"
|
||||
self.nominal_chips = 114
|
||||
self.fan_count = 4
|
||||
15
pyasic/miners/_types/innosilicon/T3X/__init__.py
Normal file
15
pyasic/miners/_types/innosilicon/T3X/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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 .T3H_Plus import InnosiliconT3HPlus
|
||||
15
pyasic/miners/_types/innosilicon/__init__.py
Normal file
15
pyasic/miners/_types/innosilicon/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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 .T3X import *
|
||||
@@ -28,7 +28,7 @@ class M30SPlusPlusVG30(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M30S++ V30"
|
||||
self.model = "M30S++ VG30"
|
||||
self.nominal_chips = 111
|
||||
self.fan_count = 2
|
||||
|
||||
@@ -37,6 +37,15 @@ class M30SPlusPlusVG40(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M30S++ V40"
|
||||
self.model = "M30S++ VG40"
|
||||
self.nominal_chips = 117
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
class M30SPlusPlusVH60(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M30S++ VH60"
|
||||
self.nominal_chips = 78
|
||||
self.fan_count = 2
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
|
||||
from .M30S import M30S, M30SVE10, M30SVE20, M30SVG20, M30SV50
|
||||
from .M30S_Plus import M30SPlus, M30SPlusVG60, M30SPlusVE40, M30SPlusVF20
|
||||
from .M30S_Plus_Plus import M30SPlusPlus, M30SPlusPlusVG30, M30SPlusPlusVG40
|
||||
from .M30S_Plus_Plus import (
|
||||
M30SPlusPlus,
|
||||
M30SPlusPlusVG30,
|
||||
M30SPlusPlusVG40,
|
||||
M30SPlusPlusVH60,
|
||||
)
|
||||
|
||||
from .M31S import M31S
|
||||
from .M31S_Plus import M31SPlus, M31SPlusVE20
|
||||
|
||||
15
pyasic/miners/innosilicon/__init__.py
Normal file
15
pyasic/miners/innosilicon/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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 *
|
||||
327
pyasic/miners/innosilicon/cgminer/T3X/T3H_Plus.py
Normal file
327
pyasic/miners/innosilicon/cgminer/T3X/T3H_Plus.py
Normal file
@@ -0,0 +1,327 @@
|
||||
# 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 CGMiner # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import InnosiliconT3HPlus # noqa - Ignore access to _module
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data.error_codes import InnosiliconError
|
||||
from pyasic.settings import PyasicSettings
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.API import APIError
|
||||
|
||||
import httpx
|
||||
import warnings
|
||||
from typing import Union
|
||||
import logging
|
||||
|
||||
|
||||
class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
self.uname = "admin"
|
||||
self.pwd = "admin"
|
||||
self.jwt = None
|
||||
|
||||
async def auth(self):
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
auth = await client.post(
|
||||
f"http://{self.ip}/api/auth",
|
||||
data={"username": self.uname, "password": self.pwd},
|
||||
)
|
||||
except Exception:
|
||||
warnings.warn(f"Could not authenticate web token with miner: {self}")
|
||||
else:
|
||||
json_auth = auth.json()
|
||||
self.jwt = json_auth.get("jwt")
|
||||
return self.jwt
|
||||
|
||||
async def send_web_command(self, command: str, data: Union[dict, None] = None):
|
||||
if not self.jwt:
|
||||
await self.auth()
|
||||
if not data:
|
||||
data = {}
|
||||
async with httpx.AsyncClient() as client:
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
response = await client.post(
|
||||
f"http://{self.ip}/api/{command}",
|
||||
headers={"Authorization": "Bearer " + self.jwt},
|
||||
timeout=5,
|
||||
data=data,
|
||||
)
|
||||
json_data = response.json()
|
||||
if (
|
||||
not json_data.get("success")
|
||||
and "token" in json_data
|
||||
and json_data.get("token") == "expired"
|
||||
):
|
||||
# refresh the token, retry
|
||||
await self.auth()
|
||||
continue
|
||||
if not json_data.get("success"):
|
||||
if json_data.get("msg"):
|
||||
raise APIError(json_data["msg"])
|
||||
elif json_data.get("message"):
|
||||
raise APIError(json_data["message"])
|
||||
raise APIError("Innosilicon web api command failed.")
|
||||
return json_data
|
||||
|
||||
async def fault_light_on(self) -> bool:
|
||||
return False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
return False
|
||||
|
||||
async def get_config(self) -> MinerConfig:
|
||||
pools = None
|
||||
cfg = MinerConfig()
|
||||
|
||||
try:
|
||||
pools = await self.api.pools()
|
||||
except APIError as e:
|
||||
logging.warning(e)
|
||||
|
||||
if pools:
|
||||
if "POOLS" in pools.keys():
|
||||
cfg = cfg.from_api(pools["POOLS"])
|
||||
return cfg
|
||||
|
||||
async def get_mac(self) -> Union[str, None]:
|
||||
try:
|
||||
data = await self.send_web_command("overview")
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
if data.get("version"):
|
||||
return data["version"].get("ethaddr").upper()
|
||||
|
||||
async def get_hostname(self) -> Union[str, None]:
|
||||
return None
|
||||
|
||||
async def get_model(self) -> Union[str, None]:
|
||||
try:
|
||||
data = await self.send_web_command("type")
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
return data["type"]
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
try:
|
||||
data = await self.send_web_command("reboot")
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
return data["success"]
|
||||
|
||||
async def restart_cgminer(self) -> bool:
|
||||
try:
|
||||
data = await self.send_web_command("restartCgMiner")
|
||||
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:
|
||||
await self.send_web_command(
|
||||
"updatePools", data=config.as_inno(user_suffix=user_suffix)
|
||||
)
|
||||
|
||||
async def get_errors(self) -> list:
|
||||
errors = []
|
||||
try:
|
||||
data = await self.send_web_command("getErrorDetail")
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
if "code" in data:
|
||||
err = data["code"]
|
||||
if isinstance(err, str):
|
||||
err = int(err)
|
||||
if not err == 0:
|
||||
errors.append(InnosiliconError(error_code=err))
|
||||
return errors
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
"""Get data from the miner.
|
||||
|
||||
Returns:
|
||||
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
||||
"""
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
|
||||
board_offset = -1
|
||||
fan_offset = -1
|
||||
|
||||
model = await self.get_model()
|
||||
hostname = await self.get_hostname()
|
||||
|
||||
if model:
|
||||
data.model = model
|
||||
|
||||
if hostname:
|
||||
data.hostname = hostname
|
||||
|
||||
data.errors = await self.get_errors()
|
||||
data.fault_light = await self.check_light()
|
||||
|
||||
miner_data = None
|
||||
all_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
miner_data = await self.api.multicommand(
|
||||
"summary", "pools", "stats", ignore_x19_error=True
|
||||
)
|
||||
|
||||
if miner_data:
|
||||
break
|
||||
|
||||
try:
|
||||
all_data = (await self.send_web_command("getAll"))["all"]
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if not (miner_data or all_data):
|
||||
return data
|
||||
|
||||
summary = miner_data.get("summary")
|
||||
pools = miner_data.get("pools")
|
||||
stats = miner_data.get("stats")
|
||||
|
||||
if summary:
|
||||
summary = summary[0]
|
||||
hr = summary.get("SUMMARY")
|
||||
if hr:
|
||||
if len(hr) > 0:
|
||||
hr = hr[0].get("MHS 1m")
|
||||
if hr:
|
||||
data.hashrate = round(hr / 1000000, 2)
|
||||
elif all_data:
|
||||
if all_data.get("total_hash"):
|
||||
print(all_data["total_hash"])
|
||||
hr = all_data["total_hash"].get("Hash Rate H")
|
||||
if hr:
|
||||
data.hashrate = round(hr / 1000000000000, 2)
|
||||
|
||||
if stats:
|
||||
stats = stats[0]
|
||||
if stats.get("STATS"):
|
||||
board_map = {0: "left", 1: "center", 2: "right"}
|
||||
for idx, board in enumerate(stats["STATS"]):
|
||||
chips = board.get("Num active chips")
|
||||
if chips:
|
||||
setattr(data, f"{board_map[idx]}_chips", chips)
|
||||
temp = board.get("Temp")
|
||||
if temp:
|
||||
setattr(data, f"{board_map[idx]}_board_chip_temp", temp)
|
||||
|
||||
if all_data:
|
||||
if all_data.get("chain"):
|
||||
board_map = {0: "left", 1: "center", 2: "right"}
|
||||
for idx, board in enumerate(all_data["chain"]):
|
||||
temp = board.get("Temp max")
|
||||
if temp:
|
||||
setattr(data, f"{board_map[idx]}_board_chip_temp", temp)
|
||||
temp_board = board.get("Temp min")
|
||||
if temp_board:
|
||||
setattr(data, f"{board_map[idx]}_board_temp", temp_board)
|
||||
hr = board.get("Hash Rate H")
|
||||
if hr:
|
||||
setattr(
|
||||
data,
|
||||
f"{board_map[idx]}_board_hashrate",
|
||||
round(hr / 1000000000000, 2),
|
||||
)
|
||||
if all_data.get("fansSpeed"):
|
||||
speed = round((all_data["fansSpeed"] * 6000) / 100)
|
||||
for fan in range(self.fan_count):
|
||||
setattr(data, f"fan_{fan+1}", speed)
|
||||
if all_data.get("mac"):
|
||||
data.mac = all_data["mac"].upper()
|
||||
else:
|
||||
mac = await self.get_mac()
|
||||
if mac:
|
||||
data.mac = mac
|
||||
if all_data.get("power"):
|
||||
data.wattage = all_data["power"]
|
||||
|
||||
if pools or all_data.get("pools_config"):
|
||||
pool_1 = None
|
||||
pool_2 = None
|
||||
pool_1_user = None
|
||||
pool_2_user = None
|
||||
pool_1_quota = 1
|
||||
pool_2_quota = 1
|
||||
quota = 0
|
||||
if pools:
|
||||
pools = pools[0]
|
||||
for pool in pools.get("POOLS"):
|
||||
if not pool_1_user:
|
||||
pool_1_user = pool.get("User")
|
||||
pool_1 = pool["URL"]
|
||||
if pool.get("Quota"):
|
||||
pool_2_quota = pool.get("Quota")
|
||||
elif not pool_2_user:
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
if pool.get("Quota"):
|
||||
pool_2_quota = pool.get("Quota")
|
||||
if not pool.get("User") == pool_1_user:
|
||||
if not pool_2_user == pool.get("User"):
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
if pool.get("Quota"):
|
||||
pool_2_quota = pool.get("Quota")
|
||||
elif all_data.get("pools_config"):
|
||||
print(all_data["pools_config"])
|
||||
for pool in all_data["pools_config"]:
|
||||
if not pool_1_user:
|
||||
pool_1_user = pool.get("user")
|
||||
pool_1 = pool["url"]
|
||||
elif not pool_2_user:
|
||||
pool_2_user = pool.get("user")
|
||||
pool_2 = pool["url"]
|
||||
if not pool.get("user") == pool_1_user:
|
||||
if not pool_2_user == pool.get("user"):
|
||||
pool_2_user = pool.get("user")
|
||||
pool_2 = pool["url"]
|
||||
|
||||
if pool_2_user and not pool_2_user == pool_1_user:
|
||||
quota = f"{pool_1_quota}/{pool_2_quota}"
|
||||
|
||||
if pool_1:
|
||||
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
|
||||
"stratum2+tcp://", ""
|
||||
)
|
||||
data.pool_1_url = pool_1
|
||||
|
||||
if pool_1_user:
|
||||
data.pool_1_user = pool_1_user
|
||||
|
||||
if pool_2:
|
||||
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
|
||||
"stratum2+tcp://", ""
|
||||
)
|
||||
data.pool_2_url = pool_2
|
||||
|
||||
if pool_2_user:
|
||||
data.pool_2_user = pool_2_user
|
||||
|
||||
if quota:
|
||||
data.pool_split = str(quota)
|
||||
|
||||
return data
|
||||
15
pyasic/miners/innosilicon/cgminer/T3X/__init__.py
Normal file
15
pyasic/miners/innosilicon/cgminer/T3X/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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 .T3H_Plus import CGMinerInnosiliconT3HPlus
|
||||
15
pyasic/miners/innosilicon/cgminer/__init__.py
Normal file
15
pyasic/miners/innosilicon/cgminer/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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 .T3X import *
|
||||
@@ -20,6 +20,7 @@ import httpx
|
||||
from pyasic.miners.antminer import *
|
||||
from pyasic.miners.avalonminer import *
|
||||
from pyasic.miners.whatsminer import *
|
||||
from pyasic.miners.innosilicon import *
|
||||
|
||||
from pyasic.miners._backends.cgminer import CGMiner # noqa - Ignore _module import
|
||||
from pyasic.miners._backends.bmminer import BMMiner # noqa - Ignore _module import
|
||||
@@ -243,6 +244,10 @@ MINER_CLASSES = {
|
||||
"Default": CGMinerAvalon1066,
|
||||
"CGMiner": CGMinerAvalon1066,
|
||||
},
|
||||
"T3H+": {
|
||||
"Default": CGMinerInnosiliconT3HPlus,
|
||||
"CGMiner": CGMinerInnosiliconT3HPlus,
|
||||
},
|
||||
"Unknown": {"Default": UnknownMiner},
|
||||
}
|
||||
|
||||
@@ -407,9 +412,13 @@ class MinerFactory(metaclass=Singleton):
|
||||
except asyncssh.misc.PermissionDenied:
|
||||
try:
|
||||
data = await self.__get_system_info_from_web(ip)
|
||||
if "minertype" in data.keys():
|
||||
if not data.get("success"):
|
||||
_model = await self.__get_dragonmint_version_from_web(ip)
|
||||
if _model:
|
||||
model = _model
|
||||
if "minertype" in data:
|
||||
model = data["minertype"].upper()
|
||||
if "bmminer" in "\t".join(data.keys()):
|
||||
if "bmminer" in "\t".join(data):
|
||||
api = "BMMiner"
|
||||
except Exception as e:
|
||||
logging.debug(f"Unable to get miner - {e}")
|
||||
@@ -488,8 +497,16 @@ class MinerFactory(metaclass=Singleton):
|
||||
if "PRO" in _model and " PRO" not in _model:
|
||||
_model = _model.replace("PRO", " PRO")
|
||||
model = _model
|
||||
else:
|
||||
_model = await self.__get_dragonmint_version_from_web(ip)
|
||||
if _model:
|
||||
model = _model
|
||||
|
||||
if model:
|
||||
if "DRAGONMINT" in model:
|
||||
_model = await self.__get_dragonmint_version_from_web(ip)
|
||||
if _model:
|
||||
model = _model
|
||||
if " HIVEON" in model:
|
||||
# do hiveon check before whatsminer as HIVEON contains a V
|
||||
model = model.split(" HIVEON")[0]
|
||||
@@ -573,6 +590,31 @@ class MinerFactory(metaclass=Singleton):
|
||||
data = data.json()
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def __get_dragonmint_version_from_web(
|
||||
ip: ipaddress.ip_address,
|
||||
) -> Union[str, None]:
|
||||
response = None
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
auth = (
|
||||
await client.post(
|
||||
f"http://{ip}/api/auth",
|
||||
data={"username": "admin", "password": "admin"},
|
||||
)
|
||||
).json()["jwt"]
|
||||
response = (
|
||||
await client.post(
|
||||
f"http://{ip}/api/type",
|
||||
headers={"Authorization": "Bearer " + auth},
|
||||
data={},
|
||||
)
|
||||
).json()
|
||||
except Exception as e:
|
||||
logging.info(e)
|
||||
if response:
|
||||
return response["type"]
|
||||
|
||||
@staticmethod
|
||||
async def _validate_command(data: dict) -> Tuple[bool, Union[str, None]]:
|
||||
"""Check if the returned command output is correctly formatted."""
|
||||
|
||||
@@ -17,6 +17,7 @@ from pyasic.miners._types import ( # noqa - Ignore access to _module
|
||||
M30SPlusPlus,
|
||||
M30SPlusPlusVG40,
|
||||
M30SPlusPlusVG30,
|
||||
M30SPlusPlusVH60,
|
||||
)
|
||||
|
||||
|
||||
@@ -36,3 +37,9 @@ class BTMinerM30SPlusPlusVG40(BTMiner, M30SPlusPlusVG40):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM30SPlusPlusVH60(BTMiner, M30SPlusPlusVH60):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
@@ -29,6 +29,7 @@ from .M30S_Plus_Plus import (
|
||||
BTMinerM30SPlusPlus,
|
||||
BTMinerM30SPlusPlusVG40,
|
||||
BTMinerM30SPlusPlusVG30,
|
||||
BTMinerM30SPlusPlusVH60,
|
||||
)
|
||||
|
||||
from .M31S import BTMinerM31S
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pyasic"
|
||||
version = "0.16.7"
|
||||
version = "0.17.0"
|
||||
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
|
||||
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
||||
repository = "https://github.com/UpstreamData/pyasic"
|
||||
|
||||
Reference in New Issue
Block a user