Compare commits

..

13 Commits

Author SHA1 Message Date
UpstreamData
dcf37481bd version: bump version number. 2022-12-05 09:35:44 -07:00
UpstreamData
1a9cca84d5 bug: fix pool split not being found correctly with braiinsOS. 2022-12-05 09:34:43 -07:00
UpstreamData
c5272d67de version: bump version number 2022-12-03 14:20:57 -07:00
UpstreamData
3bcfb14177 feature: add support for Whatsminer M31SV20, and fix a bug with miner factory not identifying the miners properly by removing a V prefix. 2022-12-03 14:20:37 -07:00
UpstreamData
566280f280 docs: fix some missing data in the docs. 2022-12-02 16:10:30 -07:00
UpstreamData
a814f7eefb Update README.md 2022-12-02 16:06:22 -07:00
UpstreamData
097b8ed534 version: bump version number. 2022-12-02 15:57:52 -07:00
UpstreamData
da47d72749 feature: add wattage limit in get_config when getting config from whatsminers. 2022-12-02 15:57:31 -07:00
UpstreamData
abd4d18a01 feature: add whatsminer M31SV10 and V60. 2022-12-02 15:51:27 -07:00
UpstreamData
2adbce3c21 version: bump version number. 2022-12-02 09:26:53 -07:00
UpstreamData
c41324b324 bug: fix a bug with MinerAPI.commands causing an infinite recursion loop when checking a list of commands with get_commands, and ficx some weirdness where BTMiner doesnt return any data. 2022-12-02 09:26:17 -07:00
UpstreamData
151a4f6c2d version: bump version number. 2022-12-01 16:18:07 -07:00
UpstreamData
d23777a83f feature: Switch to using semaphores in miner network to rate limit as they are much more friendly. 2022-12-01 16:17:46 -07:00
15 changed files with 145 additions and 75 deletions

View File

@@ -46,7 +46,7 @@ from pyasic.network import MinerNetwork
async def scan_and_get_data():
# Define network range to be used for scanning
# This can take a list of IPs, a constructor string, or an IP and subnet mask
# The standard mask is /24, and you can pass any IP address in the subnet
# The standard mask is /24 (x.x.x.0-255), and you can pass any IP address in the subnet
net = MinerNetwork("192.168.1.69", mask=24)
# Scan the network for miners
# This function returns a list of miners of the correct type as a class
@@ -93,6 +93,10 @@ if __name__ == "__main__":
If needed, this library exposes a wrapper for the miner API that can be used for advanced data gathering.
You can see more information on basic usage of the APIs past this example in the docs [here](https://pyasic.readthedocs.io/en/latest/API/api/).
Please see the appropriate API documentation page (pyasic docs -> Advanced -> Miner APIs -> your API type) for a link to that specific miner's API documentation page and more information.
#### List available API commands
```python
import asyncio
@@ -105,7 +109,8 @@ async def get_api_commands(miner_ip: str):
miner = await get_miner(miner_ip)
# List all available commands
print(miner.api.get_commands())
# Can also be called explicitly with the function miner.api.get_commands()
print(miner.api.commands)
if __name__ == "__main__":

View File

@@ -90,6 +90,9 @@ details {
</details>
<details>
<summary><a href="../whatsminer/M3X/#m31s">M31S</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv10">M31SV10</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv20">M31SV20</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv60">M31SV60</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv70">M31SV70</a></summary>
</details>
<details>

View File

@@ -114,9 +114,33 @@
show_root_heading: false
heading_level: 4
## M31SV10
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M31SV20
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV20
handler: python
options:
show_root_heading: false
heading_level: 4
## M31SV60
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV60
handler: python
options:
show_root_heading: false
heading_level: 4
## M31SV70
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV70
handler: python
options:
show_root_heading: false

View File

@@ -121,6 +121,7 @@ class BaseMinerAPI:
for func in
# each function in self
dir(self)
if not func == "commands"
if callable(getattr(self, func)) and
# no __ or _ methods
not func.startswith("__") and not func.startswith("_") and

View File

@@ -198,6 +198,8 @@ class BTMinerAPI(BaseMinerAPI):
enc_command = create_privileged_cmd(token_data, command)
data = await self._send_bytes(enc_command)
if not data:
raise APIError("No data was returned from the API.")
data = self._load_api_data(data)
try:

View File

@@ -28,7 +28,7 @@ from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
#TODO: Fix quota splitting in get data
class BOSMiner(BaseMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
@@ -471,6 +471,12 @@ class BOSMiner(BaseMiner):
data.pool_2_user = pool_2_user
if quota:
if not quota == "0":
cfg = await self.get_config()
if cfg:
if len(cfg.pool_groups) > 1:
quota = str(cfg.pool_groups[0].quota) + "/" + str(cfg.pool_groups[1].quota)
data.pool_split = str(quota)
if tunerstatus:
@@ -662,7 +668,7 @@ class BOSMiner(BaseMiner):
except (TypeError, KeyError, ValueError, IndexError):
pass
if groups[0]["strategy"].get("quota"):
data.quota = str(groups[0]["strategy"]["quota"]) + "/" + str(groups[1]["strategy"]["quota"])
data.pool_split = str(groups[0]["strategy"]["quota"]) + "/" + str(groups[1]["strategy"]["quota"])
data.fault_light = await self.check_light()

View File

@@ -238,16 +238,24 @@ class BTMiner(BaseMiner):
async def get_config(self) -> MinerConfig:
pools = None
summary = None
cfg = MinerConfig()
try:
pools = await self.api.pools()
data = await self.api.multicommand("pools", "summary")
pools = data["pools"][0]
summary = data["summary"][0]
except APIError as e:
logging.warning(e)
if pools:
if "POOLS" in pools.keys():
if "POOLS" in pools:
cfg = cfg.from_api(pools["POOLS"])
if summary:
if "SUMMARY" in summary:
if wattage := summary["SUMMARY"][0].get("Power Limit"):
cfg.autotuning_wattage = wattage
return cfg
async def get_data(self, allow_warning: bool = True) -> MinerData:

View File

@@ -24,6 +24,31 @@ class M31S(BaseMiner): # noqa - ignore ABC method implementation
self.fan_count = 2
class M31SV10(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V10"
self.nominal_chips = 105
self.fan_count = 2
class M31SV20(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V20"
self.nominal_chips = 111
self.fan_count = 2
class M31SV60(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V60"
self.nominal_chips = 105
self.fan_count = 2
class M31SV70(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()

View File

@@ -20,7 +20,7 @@ from .M30S_Plus_Plus import (
M30SPlusPlusVG40,
M30SPlusPlusVH60,
)
from .M31S import M31S, M31SV70
from .M31S import M31S, M31SV10, M31SV20, M31SV60, M31SV70
from .M31S_Plus import (
M31SPlus,
M31SPlusV30,

View File

@@ -218,6 +218,9 @@ class BaseMiner(ABC):
async def set_power_limit(self, wattage: int) -> bool:
"""Set the power limit to be used by the miner.
Parameters:
wattage: The power limit to set on the miner.
Returns:
A boolean value of the success of setting the power limit.
"""

View File

@@ -195,7 +195,10 @@ MINER_CLASSES = {
"M31S": {
"Default": BTMinerM31S,
"BTMiner": BTMinerM31S,
"V70": BTMinerM31SV70,
"10": BTMinerM31SV10,
"20": BTMinerM31SV20,
"60": BTMinerM31SV60,
"70": BTMinerM31SV70,
},
"M31S+": {
"Default": BTMinerM31SPlus,

View File

@@ -13,7 +13,7 @@
# limitations under the License.
from pyasic.miners._backends import BTMiner # noqa - Ignore access to _module
from pyasic.miners._types import M31S, M31SV70 # noqa - Ignore access to _module
from pyasic.miners._types import M31S, M31SV10, M31SV20, M31SV60, M31SV70 # noqa - Ignore access to _module
class BTMinerM31S(BTMiner, M31S):
@@ -21,6 +21,23 @@ class BTMinerM31S(BTMiner, M31S):
super().__init__(ip)
self.ip = ip
class BTMinerM31SV20(BTMiner, M31SV20):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM31SV10(BTMiner, M31SV10):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM31SV60(BTMiner, M31SV60):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM31SV70(BTMiner, M31SV70):
def __init__(self, ip: str) -> None:

View File

@@ -31,7 +31,7 @@ from .M30S_Plus_Plus import (
BTMinerM30SPlusPlusVG40,
BTMinerM30SPlusPlusVH60,
)
from .M31S import BTMinerM31S, BTMinerM31SV70
from .M31S import BTMinerM31S, BTMinerM31SV10, BTMinerM31SV20, BTMinerM31SV60, BTMinerM31SV70
from .M31S_Plus import (
BTMinerM31SPlus,
BTMinerM31SPlusV30,

View File

@@ -110,23 +110,9 @@ class MinerNetwork:
scan_tasks = []
miners = []
# for each IP in the network
for host in local_network.hosts():
# make sure we don't exceed the allowed async tasks
if len(scan_tasks) < round(PyasicSettings().network_scan_threads):
# add the task to the list
scan_tasks.append(self.ping_and_get_miner(host))
else:
# run the scan tasks
miners_scan = await asyncio.gather(*scan_tasks)
# add scanned miners to the list of found miners
miners.extend(miners_scan)
# empty the task list
scan_tasks = []
# do a final scan to empty out the list
miners_scan = await asyncio.gather(*scan_tasks)
miners.extend(miners_scan)
limit = asyncio.Semaphore(PyasicSettings().network_scan_threads)
miners = await asyncio.gather(*[self.ping_and_get_miner(host, limit) for host in local_network.hosts()])
# remove all None from the miner list
miners = list(filter(None, miners))
@@ -151,60 +137,47 @@ class MinerNetwork:
# create a list of scan tasks
scan_tasks = []
# for each ip on the network, loop through and scan it
for host in local_network.hosts():
# make sure we don't exceed the allowed async tasks
if len(scan_tasks) >= round(PyasicSettings().network_scan_threads):
# scanned is a loopable list of awaitables
scanned = asyncio.as_completed(scan_tasks)
# when we scan, empty the scan tasks
scan_tasks = []
# yield miners as they are scanned
for miner in scanned:
yield await miner
# add the ping to the list of tasks if we dont scan
scan_tasks.append(loop.create_task(self.ping_and_get_miner(host)))
# do one last scan at the end to close out the list
scanned = asyncio.as_completed(scan_tasks)
for miner in scanned:
limit = asyncio.Semaphore(PyasicSettings().network_scan_threads)
miners = asyncio.as_completed([loop.create_task(self.ping_and_get_miner(host, limit)) for host in local_network.hosts()])
for miner in miners:
yield await miner
@staticmethod
async def ping_miner(ip: ipaddress.ip_address) -> Union[None, ipaddress.ip_address]:
try:
miner = await ping_miner(ip)
if miner:
return miner
except ConnectionRefusedError:
tasks = [ping_miner(ip, port=port) for port in [4029, 8889]]
for miner in asyncio.as_completed(tasks):
try:
miner = await miner
if miner:
return miner
except ConnectionRefusedError:
pass
async def ping_miner(ip: ipaddress.ip_address, semaphore: asyncio.Semaphore) -> Union[None, ipaddress.ip_address]:
async with semaphore:
try:
miner = await ping_miner(ip)
if miner:
return miner
except ConnectionRefusedError:
tasks = [ping_miner(ip, port=port) for port in [4029, 8889]]
for miner in asyncio.as_completed(tasks):
try:
miner = await miner
if miner:
return miner
except ConnectionRefusedError:
pass
@staticmethod
async def ping_and_get_miner(
ip: ipaddress.ip_address,
ip: ipaddress.ip_address, semaphore: asyncio.Semaphore
) -> Union[None, AnyMiner]:
try:
miner = await ping_and_get_miner(ip)
if miner:
return miner
except ConnectionRefusedError:
tasks = [ping_and_get_miner(ip, port=port) for port in [4029, 8889]]
for miner in asyncio.as_completed(tasks):
try:
miner = await miner
if miner:
return miner
except ConnectionRefusedError:
pass
async with semaphore:
try:
miner = await ping_and_get_miner(ip)
if miner:
return miner
except ConnectionRefusedError:
tasks = [ping_and_get_miner(ip, port=port) for port in [4029, 8889]]
for miner in asyncio.as_completed(tasks):
try:
miner = await miner
if miner:
return miner
except ConnectionRefusedError:
pass
async def ping_miner(

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyasic"
version = "0.23.1"
version = "0.24.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"