Compare commits

...

21 Commits

Author SHA1 Message Date
UpstreamData
62b14a78b7 bump version number 2022-09-06 10:49:16 -06:00
UpstreamData
0ff505bbb4 add support for innosilicon T3H+ 2022-09-06 10:48:04 -06:00
UpstreamData
b6c8c930a2 bump version number 2022-08-30 16:01:48 -06:00
UpstreamData
903bb93c4e add check_light() for bosminers by checking if delay_on exists in the Red LED directory 2022-08-30 10:51:22 -06:00
UpstreamData
59667cf104 bump version number 2022-08-29 09:53:01 -06:00
UpstreamData
3fd1b41bec add support for whatsminer VH60 2022-08-29 09:52:35 -06:00
UpstreamData
6569107f64 bump version number 2022-08-29 09:07:02 -06:00
UpstreamData
9d746a6dcb add errors to MinerData().as_influxdb() 2022-08-29 09:06:20 -06:00
UpstreamData
fce4c07c32 bump version number 2022-08-25 15:34:28 -06:00
UpstreamData
094857758a update MinerData().as_influxdb() to include properties. 2022-08-25 15:34:04 -06:00
UpstreamData
2a49b89849 bump version number 2022-08-25 13:09:19 -06:00
UpstreamData
4ecd135734 fix a bug with incorrect types in miner data 2022-08-25 13:09:03 -06:00
UpstreamData
836defc216 bump version number 2022-08-25 13:04:06 -06:00
UpstreamData
f8f777b5b5 fix tag data to be escaped properly 2022-08-25 13:03:46 -06:00
UpstreamData
b15e0a7363 bump version number 2022-08-25 12:50:55 -06:00
UpstreamData
5c1d06f743 attempt to fix a bug with influx db miner data 2022-08-25 12:50:30 -06:00
UpstreamData
51de56feb3 bump version number 2022-08-25 12:43:05 -06:00
UpstreamData
256a4ac909 fix boolean bug in miner data 2022-08-25 12:39:57 -06:00
UpstreamData
09800c8ad2 bump version number 2022-08-24 15:18:04 -06:00
UpstreamData
83a7d8c60f add MinerData().as_json() 2022-08-24 15:17:36 -06:00
UpstreamData
ee2698be50 update poetry.lock 2022-08-24 13:56:05 -06:00
25 changed files with 659 additions and 26 deletions

View 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

View File

@@ -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]: * [M30S++][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlus]:
* [VG30][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30] * [VG30][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30]
* [VG40][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40] * [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.BTMinerM31S]
* [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]: * [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]:
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20] * [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] * [A1026][pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026]
* [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047] * [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047]
* [A1066][pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066] * [A1066][pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066]
* Stock Firmware Innosilicon Miners:
* T3X Series:
* [T3H+][pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus]

View File

@@ -89,7 +89,7 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## M30S+VG40 ## M30S++VG40
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40 ::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
handler: python handler: python
@@ -97,6 +97,14 @@
show_root_heading: false show_root_heading: false
heading_level: 4 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 ## M31S
@@ -130,7 +138,7 @@
show_root_heading: false show_root_heading: false
heading_level: 4 heading_level: 4
## M32 ## M32V20
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20 ::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20
handler: python handler: python

View File

@@ -22,6 +22,7 @@ nav:
- Avalon 10X: "miners/avalonminer/A10X.md" - Avalon 10X: "miners/avalonminer/A10X.md"
- Whatsminer M2X: "miners/whatsminer/M2X.md" - Whatsminer M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md" - Whatsminer M3X: "miners/whatsminer/M3X.md"
- Innosilicon T3X: "miners/innosilicon/T3X.md"
- Network: - Network:
- Miner Network: "network/miner_network.md" - Miner Network: "network/miner_network.md"

6
poetry.lock generated
View File

@@ -17,7 +17,7 @@ trio = ["trio (>=0.16)"]
[[package]] [[package]]
name = "asyncssh" name = "asyncssh"
version = "2.11.0" version = "2.12.0"
description = "AsyncSSH: Asynchronous SSHv2 client and server library" description = "AsyncSSH: Asynchronous SSHv2 client and server library"
category = "main" category = "main"
optional = false optional = false
@@ -218,8 +218,8 @@ anyio = [
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
] ]
asyncssh = [ asyncssh = [
{file = "asyncssh-2.11.0-py3-none-any.whl", hash = "sha256:7302348cbd54c58d3259da17f13e77912de1b005e366b15c8b183d948c8a91a8"}, {file = "asyncssh-2.12.0-py3-none-any.whl", hash = "sha256:6841c4242c606fd51188c974ec2f4887efeec67ecdfa5b84140711dacd985ab3"},
{file = "asyncssh-2.11.0.tar.gz", hash = "sha256:59c36ce77ba9dda8dd57ad875776e7105ddb1fa851bc039bb3aeadeac4f67b56"}, {file = "asyncssh-2.12.0.tar.gz", hash = "sha256:274101322c4b941823aeed8e1ab6e7be5191686c6db2d2bd35afeba30505e780"},
] ]
certifi = [ certifi = [
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},

View File

@@ -78,6 +78,23 @@ class _Pool:
pool = {"url": self.url, "user": username, "pass": self.password} pool = {"url": self.url, "user": username, "pass": self.password}
return pool 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: def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a string usable by an Avalonminer device. """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)) pools.append(pool.as_x19(user_suffix=user_suffix))
return pools 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]: def as_wm(self, user_suffix: str = None) -> List[dict]:
"""Convert the data in this class to a list usable by an Whatsminer device. """Convert the data in this class to a list usable by an Whatsminer device.
@@ -359,7 +389,15 @@ class MinerConfig:
Parameters: Parameters:
user_suffix: The suffix to append to username. 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: def as_x19(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an X19 device. """Convert the data in this class to a config usable by an X19 device.

View File

@@ -16,8 +16,9 @@ from typing import Union, List
from dataclasses import dataclass, field, asdict from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone from datetime import datetime, timezone
import time import time
import json
from .error_codes import X19Error, WhatsminerError, BraiinsOSError from .error_codes import X19Error, WhatsminerError, BraiinsOSError, InnosiliconError
@dataclass @dataclass
@@ -53,7 +54,7 @@ class MinerData:
total_chips: The total number of chips on all boards. Calculated automatically. 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. 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. 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_split: The pool split as a str.
pool_1_url: The first pool url on the miner 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. pool_1_user: The first pool user on the miner as a str.
@@ -70,11 +71,11 @@ class MinerData:
model: str = "Unknown" model: str = "Unknown"
hostname: str = "Unknown" hostname: str = "Unknown"
hashrate: float = 0 hashrate: float = 0
left_board_hashrate: float = 0 left_board_hashrate: float = 0.0
center_board_hashrate: float = 0 center_board_hashrate: float = 0.0
right_board_hashrate: float = 0 right_board_hashrate: float = 0.0
temperature_avg: int = field(init=False) temperature_avg: int = field(init=False)
env_temp: float = 0 env_temp: float = 0.0
left_board_temp: int = 0 left_board_temp: int = 0
left_board_chip_temp: int = 0 left_board_chip_temp: int = 0
center_board_temp: int = 0 center_board_temp: int = 0
@@ -99,9 +100,9 @@ class MinerData:
pool_1_user: str = "Unknown" pool_1_user: str = "Unknown"
pool_2_url: str = "" pool_2_url: str = ""
pool_2_user: str = "" pool_2_user: str = ""
errors: List[Union[WhatsminerError, BraiinsOSError, X19Error]] = field( errors: List[
default_factory=list Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
) ] = field(default_factory=list)
fault_light: Union[bool, None] = None fault_light: Union[bool, None] = None
efficiency: int = field(init=False) efficiency: int = field(init=False)
@@ -118,7 +119,7 @@ class MinerData:
return setattr(self, key, value) return setattr(self, key, value)
def __iter__(self): def __iter__(self):
return iter([item for item in self.__dict__]) return iter([item for item in self.asdict()])
@property @property
def total_chips(self): # noqa - Skip PyCharm inspection def total_chips(self): # noqa - Skip PyCharm inspection
@@ -177,6 +178,11 @@ class MinerData:
def asdict(self): def asdict(self):
return asdict(self) return asdict(self)
def as_json(self):
data = self.asdict()
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
return json.dumps(data)
def as_influxdb(self, measurement_name: str = "miner_data"): def as_influxdb(self, measurement_name: str = "miner_data"):
tag_data = [measurement_name] tag_data = [measurement_name]
field_data = [] field_data = []
@@ -184,7 +190,8 @@ class MinerData:
tags = ["ip", "mac", "model", "hostname"] tags = ["ip", "mac", "model", "hostname"]
for attribute in self: for attribute in self:
if attribute in tags: if attribute in tags:
tag_data.append(f"{attribute}={self[attribute]}") escaped_data = self[attribute].replace(" ", "\\ ")
tag_data.append(f"{attribute}={escaped_data}")
continue continue
if isinstance(self[attribute], str): if isinstance(self[attribute], str):
field_data.append(f'{attribute}="{self[attribute]}"') field_data.append(f'{attribute}="{self[attribute]}"')
@@ -193,7 +200,7 @@ class MinerData:
field_data.append(f"{attribute}={str(self[attribute]).lower()}") field_data.append(f"{attribute}={str(self[attribute]).lower()}")
continue continue
if isinstance(self[attribute], int): if isinstance(self[attribute], int):
field_data.append(f"{attribute}={self[attribute]}i") field_data.append(f"{attribute}={self[attribute]}")
continue continue
if isinstance(self[attribute], float): if isinstance(self[attribute], float):
field_data.append(f"{attribute}={self[attribute]}") field_data.append(f"{attribute}={self[attribute]}")
@@ -201,6 +208,9 @@ class MinerData:
if attribute == "fault_light" and not self[attribute]: if attribute == "fault_light" and not self[attribute]:
field_data.append(f"{attribute}=false") field_data.append(f"{attribute}=false")
continue 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) tags_str = ",".join(tag_data)
field_str = ",".join(field_data) field_str = ",".join(field_data)

View File

@@ -15,3 +15,4 @@
from .whatsminer import WhatsminerError from .whatsminer import WhatsminerError
from .bos import BraiinsOSError from .bos import BraiinsOSError
from .X19 import X19Error from .X19 import X19Error
from .innosilicon import InnosiliconError

View 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.",
}

View File

@@ -152,6 +152,7 @@ ERROR_CODES = {
2020: "Pool 0 connection failed.", 2020: "Pool 0 connection failed.",
2021: "Pool 1 connection failed.", 2021: "Pool 1 connection failed.",
2022: "Pool 2 connection failed.", 2022: "Pool 2 connection failed.",
2023: "Pool 3 connection failed.",
2030: "High rejection rate on pool.", 2030: "High rejection rate on pool.",
2040: "The pool does not support asicboost mode.", 2040: "The pool does not support asicboost mode.",
2310: "Hashrate is too low.", 2310: "Hashrate is too low.",

View File

@@ -232,8 +232,12 @@ class BOSMiner(BaseMiner):
await conn.run("/etc/init.d/bosminer start") await conn.run("/etc/init.d/bosminer start")
async def check_light(self) -> bool: async def check_light(self) -> bool:
if not self.light: if self.light:
return self.light
data = await self.send_ssh_command("ls /sys/class/leds/'Red LED'/")
self.light = False self.light = False
if "delay_on" in data:
self.light = True
return self.light return self.light
async def get_errors(self) -> list: async def get_errors(self) -> list:

View File

@@ -15,3 +15,4 @@
from .antminer import * from .antminer import *
from .avalonminer import * from .avalonminer import *
from .whatsminer import * from .whatsminer import *
from .innosilicon import *

View 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

View 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

View 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 *

View File

@@ -28,7 +28,7 @@ class M30SPlusPlusVG30(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip
self.model = "M30S++ V30" self.model = "M30S++ VG30"
self.nominal_chips = 111 self.nominal_chips = 111
self.fan_count = 2 self.fan_count = 2
@@ -37,6 +37,15 @@ class M30SPlusPlusVG40(BaseMiner):
def __init__(self, ip: str): def __init__(self, ip: str):
super().__init__() super().__init__()
self.ip = ip self.ip = ip
self.model = "M30S++ V40" self.model = "M30S++ VG40"
self.nominal_chips = 117 self.nominal_chips = 117
self.fan_count = 2 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

View File

@@ -14,7 +14,12 @@
from .M30S import M30S, M30SVE10, M30SVE20, M30SVG20, M30SV50 from .M30S import M30S, M30SVE10, M30SVE20, M30SVG20, M30SV50
from .M30S_Plus import M30SPlus, M30SPlusVG60, M30SPlusVE40, M30SPlusVF20 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 import M31S
from .M31S_Plus import M31SPlus, M31SPlusVE20 from .M31S_Plus import M31SPlus, M31SPlusVE20

View 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 *

View 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

View 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

View 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 *

View File

@@ -20,6 +20,7 @@ import httpx
from pyasic.miners.antminer import * from pyasic.miners.antminer import *
from pyasic.miners.avalonminer import * from pyasic.miners.avalonminer import *
from pyasic.miners.whatsminer 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.cgminer import CGMiner # noqa - Ignore _module import
from pyasic.miners._backends.bmminer import BMMiner # noqa - Ignore _module import from pyasic.miners._backends.bmminer import BMMiner # noqa - Ignore _module import
@@ -243,6 +244,10 @@ MINER_CLASSES = {
"Default": CGMinerAvalon1066, "Default": CGMinerAvalon1066,
"CGMiner": CGMinerAvalon1066, "CGMiner": CGMinerAvalon1066,
}, },
"T3H+": {
"Default": CGMinerInnosiliconT3HPlus,
"CGMiner": CGMinerInnosiliconT3HPlus,
},
"Unknown": {"Default": UnknownMiner}, "Unknown": {"Default": UnknownMiner},
} }
@@ -407,9 +412,13 @@ class MinerFactory(metaclass=Singleton):
except asyncssh.misc.PermissionDenied: except asyncssh.misc.PermissionDenied:
try: try:
data = await self.__get_system_info_from_web(ip) 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() model = data["minertype"].upper()
if "bmminer" in "\t".join(data.keys()): if "bmminer" in "\t".join(data):
api = "BMMiner" api = "BMMiner"
except Exception as e: except Exception as e:
logging.debug(f"Unable to get miner - {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: if "PRO" in _model and " PRO" not in _model:
_model = _model.replace("PRO", " PRO") _model = _model.replace("PRO", " PRO")
model = _model model = _model
else:
_model = await self.__get_dragonmint_version_from_web(ip)
if _model:
model = _model
if model: if model:
if "DRAGONMINT" in model:
_model = await self.__get_dragonmint_version_from_web(ip)
if _model:
model = _model
if " HIVEON" in model: if " HIVEON" in model:
# do hiveon check before whatsminer as HIVEON contains a V # do hiveon check before whatsminer as HIVEON contains a V
model = model.split(" HIVEON")[0] model = model.split(" HIVEON")[0]
@@ -573,6 +590,31 @@ class MinerFactory(metaclass=Singleton):
data = data.json() data = data.json()
return data 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 @staticmethod
async def _validate_command(data: dict) -> Tuple[bool, Union[str, None]]: async def _validate_command(data: dict) -> Tuple[bool, Union[str, None]]:
"""Check if the returned command output is correctly formatted.""" """Check if the returned command output is correctly formatted."""

View File

@@ -17,6 +17,7 @@ from pyasic.miners._types import ( # noqa - Ignore access to _module
M30SPlusPlus, M30SPlusPlus,
M30SPlusPlusVG40, M30SPlusPlusVG40,
M30SPlusPlusVG30, M30SPlusPlusVG30,
M30SPlusPlusVH60,
) )
@@ -36,3 +37,9 @@ class BTMinerM30SPlusPlusVG40(BTMiner, M30SPlusPlusVG40):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.ip = ip self.ip = ip
class BTMinerM30SPlusPlusVH60(BTMiner, M30SPlusPlusVH60):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -29,6 +29,7 @@ from .M30S_Plus_Plus import (
BTMinerM30SPlusPlus, BTMinerM30SPlusPlus,
BTMinerM30SPlusPlusVG40, BTMinerM30SPlusPlusVG40,
BTMinerM30SPlusPlusVG30, BTMinerM30SPlusPlusVG30,
BTMinerM30SPlusPlusVH60,
) )
from .M31S import BTMinerM31S from .M31S import BTMinerM31S

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pyasic" name = "pyasic"
version = "0.16.2" 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." 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>"] authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic" repository = "https://github.com/UpstreamData/pyasic"