Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f59ef025d | ||
|
|
d6a153144f | ||
|
|
99001e2e13 | ||
|
|
92b847656e | ||
|
|
a41525e828 | ||
|
|
5e9588cc56 | ||
|
|
b8239703c1 | ||
|
|
5d49135b59 | ||
|
|
3a5a76080b | ||
|
|
f23e10d629 |
2
.github/workflows/python-publish.yml
vendored
2
.github/workflows/python-publish.yml
vendored
@@ -19,4 +19,4 @@ jobs:
|
||||
- name: Build using poetry and publish to PyPi
|
||||
uses: JRubics/poetry-publish@v1.11
|
||||
with:
|
||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||
|
||||
22
.pre-commit-config.yaml
Normal file
22
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: unittest
|
||||
name: unittest
|
||||
entry: python -m unittest discover
|
||||
language: system
|
||||
'types': [python]
|
||||
args: ["-p '*test.py'"] # Probably this option is absolutely not needed.
|
||||
pass_filenames: false
|
||||
stages: [commit]
|
||||
28
README.md
28
README.md
@@ -18,13 +18,15 @@ You can install pyasic directly from pip with the command `pip install pyasic`
|
||||
For those of you who aren't comfortable with code and developer tools, there are windows builds of GUI applications that use this library here -> (https://drive.google.com/drive/folders/1DjR8UOS_g0ehfiJcgmrV0FFoqFvE9akW?usp=sharing)
|
||||
|
||||
### Developers
|
||||
To use this repo, first download it, create a virtual environment, enter the virtual environment, and install relevant packages by navigating to this directory and running ```pip install -r requirements.txt``` on Windows or ```pip3 install -r requirements.txt``` on Mac or UNIX if the first command fails.
|
||||
To use this repo, first download it, create a virtual environment, enter the virtual environment, and install relevant packages by navigating to this directory and running ```pip install -r requirements-dev.txt``` on Windows or ```pip3 install -r requirements-dev.txt``` on Mac or UNIX if the first command fails.
|
||||
|
||||
You can also use poetry by initializing and running ```poetry install```
|
||||
You can also use poetry by initializing and running ```poetry install```, and you will have to install `pre-commit` (`pip install pre-commit`).
|
||||
|
||||
Finally, initialize pre-commit hooks with `pre-commit install`
|
||||
|
||||
### Interfacing with miners programmatically
|
||||
|
||||
##### Note: If you are trying to interface with Whatsminers, there is a bug in the way they are interacted with on Windows, so to fix that you need to change the event loop policy using this code:
|
||||
##### Note: If you are trying to interface with Whatsminers, there is a bug in the way they are interacted with on Windows, so to fix that you need to change the event loop policy using this code:
|
||||
```python
|
||||
# need to import these 2 libraries, you need asyncio anyway so make sure you have sys imported
|
||||
import sys
|
||||
@@ -66,18 +68,18 @@ async def scan_and_get_data():
|
||||
# Scan the network for miners
|
||||
# This function returns a list of miners of the correct type as a class
|
||||
miners: list = await net.scan_network_for_miners()
|
||||
|
||||
|
||||
# We can now get data from any of these miners
|
||||
# To do them all we have to create a list of tasks and gather them
|
||||
tasks = [miner.get_data() for miner in miners]
|
||||
# Gather all tasks asynchronously and run them
|
||||
data = await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
# Data is now a list of MinerData, and we can reference any part of that
|
||||
# Print out all data for now
|
||||
for item in data:
|
||||
print(item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(scan_and_get_data())
|
||||
```
|
||||
@@ -102,7 +104,7 @@ async def get_miner_data(miner_ip: str):
|
||||
# Use MinerFactory to get miner
|
||||
# MinerFactory is a singleton, so we can just get the instance in place
|
||||
miner = await MinerFactory().get_miner(miner_ip)
|
||||
|
||||
|
||||
# Get data from the miner
|
||||
data = await miner.get_data()
|
||||
print(data)
|
||||
@@ -131,11 +133,11 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
|
||||
async def get_api_commands(miner_ip: str):
|
||||
# Get the miner
|
||||
miner = await MinerFactory().get_miner(miner_ip)
|
||||
|
||||
|
||||
# List all available commands
|
||||
print(miner.api.get_commands())
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(get_api_commands("192.168.1.69"))
|
||||
```
|
||||
@@ -159,13 +161,13 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
|
||||
async def get_api_commands(miner_ip: str):
|
||||
# Get the miner
|
||||
miner = await MinerFactory().get_miner(miner_ip)
|
||||
|
||||
|
||||
# Run the devdetails command
|
||||
# This is equivalent to await miner.api.send_command("devdetails")
|
||||
devdetails: dict = await miner.api.devdetails()
|
||||
print(devdetails)
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(get_api_commands("192.168.1.69"))
|
||||
```
|
||||
|
||||
@@ -29,13 +29,13 @@ async def scan_miners(): # define async scan function to allow awaiting
|
||||
# create a miner network
|
||||
# you can pass in any IP and it will use that in a subnet with a /24 mask (255 IPs).
|
||||
network = MinerNetwork("192.168.1.50") # this uses the 192.168.1.0-255 network
|
||||
|
||||
|
||||
# scan for miners asynchronously
|
||||
# this will return the correct type of miners if they are supported with all functionality.
|
||||
miners = await network.scan_network_for_miners()
|
||||
print(miners)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(scan_miners()) # run the scan asynchronously with asyncio.run()
|
||||
```
|
||||
|
||||
@@ -56,8 +56,8 @@ async def get_miners(): # define async scan function to allow awaiting
|
||||
miner_1 = await MinerFactory().get_miner("192.168.1.75")
|
||||
miner_2 = await MinerFactory().get_miner("192.168.1.76")
|
||||
print(miner_1, miner_2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(get_miners()) # get the miners asynchronously with asyncio.run()
|
||||
```
|
||||
|
||||
@@ -78,7 +78,7 @@ async def gather_miner_data():
|
||||
print(miner_data) # all data from the dataclass
|
||||
print(miner_data.hashrate) # hashrate of the miner in TH/s
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(gather_miner_data())
|
||||
```
|
||||
|
||||
@@ -91,13 +91,13 @@ from pyasic.network import MinerNetwork # miner network handles the scanning
|
||||
async def gather_miner_data(): # define async scan function to allow awaiting
|
||||
network = MinerNetwork("192.168.1.50")
|
||||
miners = await network.scan_network_for_miners()
|
||||
|
||||
|
||||
# we need to asyncio.gather() all the miners get_data() functions to make them run together
|
||||
all_miner_data = await asyncio.gather(*[miner.get_data() for miner in miners])
|
||||
|
||||
for miner_data in all_miner_data:
|
||||
print(miner_data) # print out all the data one by one
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(gather_miner_data())
|
||||
```
|
||||
|
||||
@@ -183,7 +183,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
pwd: str = PyasicSettings().global_whatsminer_password,
|
||||
):
|
||||
super().__init__(ip, port)
|
||||
self.admin_pwd = pwd
|
||||
self.pwd = pwd
|
||||
self.current_token = None
|
||||
|
||||
async def send_command(
|
||||
@@ -260,7 +260,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
data = await self.send_command("get_token")
|
||||
|
||||
# encrypt the admin password with the salt
|
||||
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + "$")
|
||||
pwd = _crypt(self.pwd, "$1$" + data["Msg"]["salt"] + "$")
|
||||
pwd = pwd.split("$")
|
||||
|
||||
# take the 4th item from the pwd split
|
||||
@@ -437,6 +437,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
|
||||
async def set_led(
|
||||
self,
|
||||
auto: bool = True,
|
||||
color: str = "red",
|
||||
period: int = 2000,
|
||||
duration: int = 1000,
|
||||
@@ -450,6 +451,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
changing the password of the miner using the Whatsminer tool.
|
||||
|
||||
Parameters:
|
||||
auto: Whether or not to reset the LED to auto mode.
|
||||
color: The LED color to set, either 'red' or 'green'.
|
||||
period: The flash cycle in ms.
|
||||
duration: LED on time in the cycle in ms.
|
||||
@@ -459,16 +461,19 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of setting the LED.
|
||||
</details>
|
||||
"""
|
||||
command = {
|
||||
"cmd": "set_led",
|
||||
"color": color,
|
||||
"period": period,
|
||||
"duration": duration,
|
||||
"start": start,
|
||||
}
|
||||
if not auto:
|
||||
command = {
|
||||
"cmd": "set_led",
|
||||
"color": color,
|
||||
"period": period,
|
||||
"duration": duration,
|
||||
"start": start,
|
||||
}
|
||||
else:
|
||||
command = {"cmd": "set_led", "param": "auto"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_command(enc_command, ignore_errors=True)
|
||||
|
||||
async def set_low_power(self) -> dict:
|
||||
"""Set low power mode on the miner using the API.
|
||||
@@ -542,6 +547,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
|
||||
A reply informing of the status of setting the password.
|
||||
"""
|
||||
self.pwd = old_pwd
|
||||
# check if password length is greater than 8 bytes
|
||||
if len(new_pwd.encode("utf-8")) > 8:
|
||||
raise APIError(
|
||||
@@ -551,7 +557,12 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
try:
|
||||
data = await self.send_command(enc_command)
|
||||
except APIError as e:
|
||||
raise e
|
||||
self.pwd = new_pwd
|
||||
return data
|
||||
|
||||
async def set_target_freq(self, percent: int) -> dict:
|
||||
"""Update the target frequency.
|
||||
|
||||
@@ -52,6 +52,19 @@ class _Pool:
|
||||
self.password = data[key]
|
||||
return self
|
||||
|
||||
def as_wm(self, user_suffix: str = None) -> dict:
|
||||
"""Convert the data in this class to a dict usable by an Whatsminer 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_x19(self, user_suffix: str = None) -> dict:
|
||||
"""Convert the data in this class to a dict usable by an X19 device.
|
||||
|
||||
@@ -141,6 +154,19 @@ class _PoolGroup:
|
||||
pools.append(pool.as_x19(user_suffix=user_suffix))
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
pools = []
|
||||
for pool in self.pools[:3]:
|
||||
pools.append(pool.as_wm(user_suffix=user_suffix))
|
||||
while len(pools) < 3:
|
||||
pools.append({"url": None, "user": None, "pass": None})
|
||||
return pools
|
||||
|
||||
def as_avalon(self, user_suffix: str = None) -> str:
|
||||
"""Convert the data in this class to a dict usable by an Avalonminer device.
|
||||
|
||||
@@ -327,6 +353,14 @@ class MinerConfig:
|
||||
"""
|
||||
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
|
||||
|
||||
def as_wm(self, user_suffix: str = None) -> List[dict]:
|
||||
"""Convert the data in this class to a config usable by an Whatsminer device.
|
||||
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
return self.pool_groups[0].as_x19(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.
|
||||
|
||||
|
||||
@@ -282,7 +282,8 @@ class BMMiner(BaseMiner):
|
||||
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
|
||||
if not env_temp == 0:
|
||||
env_temp_list.append(int(env_temp))
|
||||
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
||||
if not env_temp_list == []:
|
||||
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
|
||||
@@ -72,11 +72,11 @@ class BOSMiner(BaseMiner):
|
||||
async def fault_light_on(self) -> bool:
|
||||
"""Sends command to turn on fault light on the miner."""
|
||||
logging.debug(f"{self}: Sending fault_light on command.")
|
||||
self.light = True
|
||||
_ret = await self.send_ssh_command("miner fault_light on")
|
||||
logging.debug(f"{self}: fault_light on command completed.")
|
||||
if isinstance(_ret, str):
|
||||
return True
|
||||
self.light = True
|
||||
return self.light
|
||||
return False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
@@ -86,6 +86,7 @@ class BOSMiner(BaseMiner):
|
||||
_ret = await self.send_ssh_command("miner fault_light off")
|
||||
logging.debug(f"{self}: fault_light off command completed.")
|
||||
if isinstance(_ret, str):
|
||||
self.light = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -100,15 +100,57 @@ class BTMiner(BaseMiner):
|
||||
|
||||
return str(mac).upper()
|
||||
|
||||
async def _reset_api_pwd_to_admin(self, pwd: str):
|
||||
try:
|
||||
data = await self.api.update_pwd(pwd, "admin")
|
||||
except APIError:
|
||||
return False
|
||||
if data:
|
||||
if "Code" in data.keys():
|
||||
if data["Code"] == 131:
|
||||
return True
|
||||
print(data)
|
||||
return False
|
||||
|
||||
async def check_light(self) -> bool:
|
||||
if not self.light:
|
||||
self.light = False
|
||||
data = None
|
||||
|
||||
try:
|
||||
data = await self.api.get_miner_info()
|
||||
except APIError:
|
||||
if not self.light:
|
||||
self.light = False
|
||||
if data:
|
||||
if "Msg" in data.keys():
|
||||
if "ledstat" in data["Msg"].keys():
|
||||
if not data["Msg"]["ledstat"] == "auto":
|
||||
self.light = True
|
||||
if data["Msg"]["ledstat"] == "auto":
|
||||
self.light = False
|
||||
return self.light
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
try:
|
||||
data = await self.api.set_led(auto=True)
|
||||
except APIError:
|
||||
return False
|
||||
if data:
|
||||
if "Code" in data.keys():
|
||||
if data["Code"] == 131:
|
||||
self.light = False
|
||||
return True
|
||||
return False
|
||||
|
||||
async def fault_light_on(self) -> bool:
|
||||
try:
|
||||
data = await self.api.set_led(auto=False)
|
||||
except APIError:
|
||||
return False
|
||||
if data:
|
||||
if "Code" in data.keys():
|
||||
if data["Code"] == 131:
|
||||
self.light = True
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_errors(self) -> list:
|
||||
@@ -120,6 +162,25 @@ class BTMiner(BaseMiner):
|
||||
async def restart_backend(self) -> bool:
|
||||
return False
|
||||
|
||||
async def send_config(self, yaml_config, ip_user: bool = False):
|
||||
if ip_user:
|
||||
suffix = str(self.ip).split(".")[-1]
|
||||
conf = MinerConfig().from_yaml(yaml_config).as_wm(user_suffix=suffix)
|
||||
else:
|
||||
conf = MinerConfig().from_yaml(yaml_config).as_wm()
|
||||
|
||||
await self.api.update_pools(
|
||||
conf[0]["url"],
|
||||
conf[0]["user"],
|
||||
conf[0]["pass"],
|
||||
conf[1]["url"],
|
||||
conf[1]["user"],
|
||||
conf[1]["pass"],
|
||||
conf[2]["url"],
|
||||
conf[2]["user"],
|
||||
conf[2]["pass"],
|
||||
)
|
||||
|
||||
async def get_config(self) -> MinerConfig:
|
||||
pools = None
|
||||
cfg = MinerConfig()
|
||||
|
||||
@@ -20,5 +20,5 @@ class T9(BaseMiner):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "T9"
|
||||
self.nominal_chips = 57
|
||||
self.nominal_chips = 54
|
||||
self.fan_count = 2
|
||||
|
||||
33
pyasic/miners/_types/whatsminer/M3X/M32.py
Normal file
33
pyasic/miners/_types/whatsminer/M3X/M32.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# 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 import BaseMiner
|
||||
|
||||
|
||||
class M32(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M32"
|
||||
self.nominal_chips = 74
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
class M32V20(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M32 V20"
|
||||
self.nominal_chips = 74
|
||||
self.fan_count = 2
|
||||
@@ -19,4 +19,5 @@ from .M30S_Plus_Plus import M30SPlusPlus, M30SPlusPlusVG30, M30SPlusPlusVG40
|
||||
from .M31S import M31S
|
||||
from .M31S_Plus import M31SPlus, M31SPlusVE20
|
||||
|
||||
from .M32 import M32, M32V20
|
||||
from .M32S import M32S
|
||||
|
||||
@@ -15,8 +15,180 @@
|
||||
from pyasic.miners._backends import Hiveon # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import T9 # noqa - Ignore access to _module
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.settings import PyasicSettings
|
||||
|
||||
|
||||
class HiveonT9(Hiveon, T9):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
self.pwd = "admin"
|
||||
|
||||
async def get_mac(self):
|
||||
mac = (
|
||||
(await self.send_ssh_command("cat /sys/class/net/eth0/address"))
|
||||
.strip()
|
||||
.upper()
|
||||
)
|
||||
return mac
|
||||
|
||||
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()
|
||||
mac = await self.get_mac()
|
||||
errors = await self.get_errors()
|
||||
|
||||
if model:
|
||||
data.model = model
|
||||
|
||||
if hostname:
|
||||
data.hostname = hostname
|
||||
|
||||
if mac:
|
||||
data.mac = mac
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
data.errors.append(error)
|
||||
|
||||
data.fault_light = await self.check_light()
|
||||
|
||||
miner_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
|
||||
|
||||
if not miner_data:
|
||||
return data
|
||||
|
||||
summary = miner_data.get("summary")[0]
|
||||
pools = miner_data.get("pools")[0]
|
||||
stats = miner_data.get("stats")[0]
|
||||
|
||||
if summary:
|
||||
hr = summary.get("SUMMARY")
|
||||
if hr:
|
||||
if len(hr) > 0:
|
||||
hr = hr[0].get("GHS 1m")
|
||||
if hr:
|
||||
data.hashrate = round(hr / 1000, 2)
|
||||
|
||||
if stats:
|
||||
boards = stats.get("STATS")
|
||||
if boards:
|
||||
if len(boards) > 0:
|
||||
if "chain_power" in boards[1].keys():
|
||||
data.wattage = round(
|
||||
float(boards[1]["chain_power"].split(" ")[0])
|
||||
)
|
||||
|
||||
board_map = {
|
||||
"left": [2, 9, 10],
|
||||
"center": [3, 11, 12],
|
||||
"right": [4, 13, 14],
|
||||
}
|
||||
|
||||
env_temp_list = []
|
||||
|
||||
for board in board_map.keys():
|
||||
chips = 0
|
||||
hashrate = 0
|
||||
chip_temp = 0
|
||||
board_temp = 0
|
||||
for chipset in board_map[board]:
|
||||
if chip_temp == 0:
|
||||
if f"temp{chipset}" in boards[1].keys():
|
||||
board_temp = boards[1][f"temp{chipset}"]
|
||||
chip_temp = boards[1][f"temp2_{chipset}"]
|
||||
if f"temp3_{chipset}" in boards[1].keys():
|
||||
env_temp = boards[1][f"temp3_{chipset}"]
|
||||
if not env_temp == 0:
|
||||
env_temp_list.append(int(env_temp))
|
||||
|
||||
hashrate += boards[1][f"chain_rate{chipset}"]
|
||||
chips += boards[1][f"chain_acn{chipset}"]
|
||||
setattr(data, f"{board}_chips", chips)
|
||||
setattr(data, f"{board}_board_hashrate", hashrate)
|
||||
setattr(data, f"{board}_board_temp", board_temp)
|
||||
setattr(data, f"{board}_board_chip_temp", chip_temp)
|
||||
if not env_temp_list == []:
|
||||
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
||||
|
||||
if stats:
|
||||
temp = stats.get("STATS")
|
||||
if temp:
|
||||
if len(temp) > 1:
|
||||
for fan_num in range(1, 8, 4):
|
||||
for _f_num in range(4):
|
||||
f = temp[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):
|
||||
setattr(
|
||||
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
|
||||
)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
pool_2 = None
|
||||
pool_1_user = None
|
||||
pool_2_user = None
|
||||
pool_1_quota = 1
|
||||
pool_2_quota = 1
|
||||
quota = 0
|
||||
for pool in pools.get("POOLS"):
|
||||
if not pool.get("User") == "*":
|
||||
if not pool_1_user:
|
||||
pool_1_user = pool.get("User")
|
||||
pool_1 = pool["URL"]
|
||||
pool_1_quota = pool["Quota"]
|
||||
elif not pool_2_user:
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
pool_2_quota = pool["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"]
|
||||
pool_2_quota = pool["Quota"]
|
||||
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
|
||||
|
||||
@@ -56,6 +56,11 @@ MINER_CLASSES = {
|
||||
"Default": BMMinerS9i,
|
||||
"BMMiner": BMMinerS9i,
|
||||
},
|
||||
"ANTMINER T9": {
|
||||
"Default": BMMinerT9,
|
||||
"BMMiner": BMMinerT9,
|
||||
"Hiveon": HiveonT9,
|
||||
},
|
||||
"ANTMINER S17": {
|
||||
"Default": BMMinerS17,
|
||||
"BOSMiner+": BOSMinerS17,
|
||||
@@ -195,6 +200,11 @@ MINER_CLASSES = {
|
||||
"Default": BTMinerM32S,
|
||||
"BTMiner": BTMinerM32S,
|
||||
},
|
||||
"M32": {
|
||||
"Default": BTMinerM32,
|
||||
"BTMiner": BTMinerM32,
|
||||
"20": BTMinerM32V20,
|
||||
},
|
||||
"AvalonMiner 721": {
|
||||
"Default": CGMinerAvalon721,
|
||||
"CGMiner": CGMinerAvalon721,
|
||||
@@ -548,6 +558,9 @@ class MinerFactory(metaclass=Singleton):
|
||||
model = _model.replace("PRO", " PRO")
|
||||
|
||||
if model:
|
||||
if " HIVEON" in model:
|
||||
model = model.split(" HIVEON")[0]
|
||||
api = "Hiveon"
|
||||
# whatsminer have a V in their version string (M20SV41), remove everything after it
|
||||
if "V" in model:
|
||||
_ver = model.split("V")
|
||||
|
||||
28
pyasic/miners/whatsminer/btminer/M3X/M32.py
Normal file
28
pyasic/miners/whatsminer/btminer/M3X/M32.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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 BTMiner # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import M32, M32V20 # noqa - Ignore access to _module
|
||||
|
||||
|
||||
class BTMinerM32(BTMiner, M32):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM32V20(BTMiner, M32V20):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
@@ -13,7 +13,9 @@
|
||||
# limitations under the License.
|
||||
|
||||
from pyasic.miners._backends import BTMiner # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import M32S # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import (
|
||||
M32S,
|
||||
) # noqa - Ignore access to _module
|
||||
|
||||
|
||||
class BTMinerM32S(BTMiner, M32S):
|
||||
|
||||
@@ -34,4 +34,5 @@ from .M30S_Plus_Plus import (
|
||||
from .M31S import BTMinerM31S
|
||||
from .M31S_Plus import BTMinerM31SPlus, BTMinerM31SPlusVE20
|
||||
|
||||
from .M32 import BTMinerM32, BTMinerM32V20
|
||||
from .M32S import BTMinerM32S
|
||||
|
||||
@@ -208,8 +208,8 @@ async def ping_miner(
|
||||
ip: ipaddress.ip_address, port=4028
|
||||
) -> Union[None, ipaddress.ip_address]:
|
||||
for i in range(PyasicSettings().network_ping_retries):
|
||||
connection_fut = asyncio.open_connection(str(ip), port)
|
||||
try:
|
||||
connection_fut = asyncio.open_connection(str(ip), port)
|
||||
# get the read and write streams from the connection
|
||||
reader, writer = await asyncio.wait_for(
|
||||
connection_fut, timeout=PyasicSettings().network_ping_timeout
|
||||
@@ -253,10 +253,10 @@ async def ping_and_get_miner(
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
# ping failed if we time out
|
||||
continue
|
||||
except ConnectionRefusedError:
|
||||
except ConnectionRefusedError as e:
|
||||
# handle for other connection errors
|
||||
logging.debug(f"{str(ip)}: Connection Refused.")
|
||||
raise ConnectionRefusedError
|
||||
raise e
|
||||
# ping failed, likely with an exception
|
||||
except Exception as e:
|
||||
logging.warning(f"{str(ip)}: {e}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pyasic"
|
||||
version = "0.15.0"
|
||||
version = "0.15.2"
|
||||
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"
|
||||
|
||||
2
requirements-dev.txt
Normal file
2
requirements-dev.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
-r requirements.txt
|
||||
pre-commit
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Reference in New Issue
Block a user