Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c50d55e87c | ||
|
|
5e5516bfb3 | ||
|
|
4b068c57c5 | ||
|
|
203f199aec | ||
|
|
895f17aaf9 | ||
|
|
8a64ff3559 | ||
|
|
4c45d356c4 | ||
|
|
4dec329f11 | ||
|
|
b563ed118e | ||
|
|
75b2ec40b1 | ||
|
|
d9adaf6667 | ||
|
|
9343308f41 | ||
|
|
88769e40ae | ||
|
|
be45eb7400 | ||
|
|
2f719a03a4 | ||
|
|
64196f9754 | ||
|
|
49a77f1b79 | ||
|
|
3838c4f2f9 | ||
|
|
80d89c95b5 | ||
|
|
30cd8b5cfe | ||
|
|
c443170f78 | ||
|
|
a2c2aa2377 | ||
|
|
4f0eb49a02 | ||
|
|
a821357b4f | ||
|
|
3c7679a22d | ||
|
|
a52737e236 |
@@ -108,31 +108,31 @@ class MiningModeHPM(MinerConfigValue):
|
||||
return {"mode": {"mode": "turbo"}}
|
||||
|
||||
|
||||
class StandardPowerTuneAlgo(MinerConfigValue):
|
||||
class StandardTuneAlgo(MinerConfigValue):
|
||||
mode: str = field(init=False, default="standard")
|
||||
|
||||
def as_epic(self):
|
||||
return VOptPowerTuneAlgo().as_epic()
|
||||
def as_epic(self) -> str:
|
||||
return VOptAlgo().as_epic()
|
||||
|
||||
|
||||
class VOptPowerTuneAlgo(MinerConfigValue):
|
||||
class VOptAlgo(MinerConfigValue):
|
||||
mode: str = field(init=False, default="standard")
|
||||
|
||||
def as_epic(self):
|
||||
def as_epic(self) -> str:
|
||||
return "VoltageOptimizer"
|
||||
|
||||
|
||||
class ChipTunePowerTuneAlgo(MinerConfigValue):
|
||||
class ChipTuneAlgo(MinerConfigValue):
|
||||
mode: str = field(init=False, default="standard")
|
||||
|
||||
def as_epic(self):
|
||||
def as_epic(self) -> str:
|
||||
return "ChipTune"
|
||||
|
||||
|
||||
class PowerTunerAlgo(MinerConfigOption):
|
||||
standard = StandardPowerTuneAlgo
|
||||
voltage_optimizer = VOptPowerTuneAlgo
|
||||
chip_tune = ChipTunePowerTuneAlgo
|
||||
class TunerAlgo(MinerConfigOption):
|
||||
standard = StandardTuneAlgo
|
||||
voltage_optimizer = VOptAlgo
|
||||
chip_tune = ChipTuneAlgo
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
@@ -143,7 +143,7 @@ class PowerTunerAlgo(MinerConfigOption):
|
||||
class MiningModePowerTune(MinerConfigValue):
|
||||
mode: str = field(init=False, default="power_tuning")
|
||||
power: int = None
|
||||
algo: PowerTunerAlgo = field(default_factory=PowerTunerAlgo.default)
|
||||
algo: TunerAlgo = field(default_factory=TunerAlgo.default)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune":
|
||||
@@ -183,14 +183,12 @@ class MiningModePowerTune(MinerConfigValue):
|
||||
def as_auradine(self) -> dict:
|
||||
return {"mode": {"mode": "custom", "tune": "power", "power": self.power}}
|
||||
|
||||
def as_epic(self) -> dict:
|
||||
return {"ptune": {**self.algo.as_epic(), "target": self.power}}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MiningModeHashrateTune(MinerConfigValue):
|
||||
mode: str = field(init=False, default="hashrate_tuning")
|
||||
hashrate: int = None
|
||||
algo: TunerAlgo = field(default_factory=TunerAlgo.default)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune":
|
||||
@@ -218,6 +216,9 @@ class MiningModeHashrateTune(MinerConfigValue):
|
||||
def as_auradine(self) -> dict:
|
||||
return {"mode": {"mode": "custom", "tune": "ths", "ths": self.hashrate}}
|
||||
|
||||
def as_epic(self) -> dict:
|
||||
return {"ptune": {"algo": self.algo.as_epic(), "target": self.hashrate}}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManualBoardSettings(MinerConfigValue):
|
||||
@@ -313,14 +314,14 @@ class MiningModeConfig(MinerConfigOption):
|
||||
if tuner_running:
|
||||
algo_info = web_conf["PerpetualTune"]["Algorithm"]
|
||||
if algo_info.get("VoltageOptimizer") is not None:
|
||||
return cls.power_tuning(
|
||||
power=algo_info["VoltageOptimizer"]["Target"],
|
||||
algo=PowerTunerAlgo.voltage_optimizer,
|
||||
return cls.hashrate_tuning(
|
||||
hashrate=algo_info["VoltageOptimizer"]["Target"],
|
||||
algo=TunerAlgo.voltage_optimizer,
|
||||
)
|
||||
else:
|
||||
return cls.power_tuning(
|
||||
power=algo_info["ChipTune"]["Target"],
|
||||
algo=PowerTunerAlgo.chip_tune,
|
||||
return cls.hashrate_tuning(
|
||||
hashrate=algo_info["ChipTune"]["Target"],
|
||||
algo=TunerAlgo.chip_tune,
|
||||
)
|
||||
else:
|
||||
return cls.normal()
|
||||
|
||||
@@ -265,15 +265,7 @@ class PoolGroup(MinerConfigValue):
|
||||
return [p.as_auradine(user_suffix=user_suffix) for p in self.pools]
|
||||
|
||||
def as_epic(self, user_suffix: str = None) -> dict:
|
||||
if len(self.pools) > 0:
|
||||
conf = {
|
||||
"name": self.name,
|
||||
"pool": [pool.as_epic(user_suffix=user_suffix) for pool in self.pools],
|
||||
}
|
||||
if self.quota is not None:
|
||||
conf["quota"] = self.quota
|
||||
return conf
|
||||
return {"name": self.name, "pool": []}
|
||||
return [p.as_epic(user_suffix=user_suffix) for p in self.pools]
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "PoolGroup":
|
||||
@@ -421,9 +413,7 @@ class PoolConfig(MinerConfigValue):
|
||||
return {
|
||||
"pools": {
|
||||
"coin": "Btc",
|
||||
"stratum_configs": [
|
||||
g.as_epic(user_suffix=user_suffix) for g in self.groups
|
||||
],
|
||||
"stratum_configs": self.groups[0].as_epic(user_suffix=user_suffix),
|
||||
"unique_id": False,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +43,9 @@ class TemperatureConfig(MinerConfigValue):
|
||||
def as_epic(self) -> dict:
|
||||
temps_config = {"temps": {}, "fans": {"Auto": {}}}
|
||||
if self.target is not None:
|
||||
temps_config["fans"]["Target Temperature"] = self.target
|
||||
temps_config["fans"]["Auto"]["Target Temperature"] = self.target
|
||||
else:
|
||||
temps_config["fans"]["Target Temperature"] = 60
|
||||
temps_config["fans"]["Auto"]["Target Temperature"] = 60
|
||||
if self.danger is not None:
|
||||
temps_config["temps"]["shutdown"] = self.danger
|
||||
return temps_config
|
||||
|
||||
@@ -14,21 +14,21 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pyasic.miners.backends import BOSer
|
||||
from pyasic.miners.backends import BOSMiner
|
||||
from pyasic.miners.models import S17, S17e, S17Plus, S17Pro
|
||||
|
||||
|
||||
class BOSMinerS17(BOSer, S17):
|
||||
class BOSMinerS17(BOSMiner, S17):
|
||||
pass
|
||||
|
||||
|
||||
class BOSMinerS17Plus(BOSer, S17Plus):
|
||||
class BOSMinerS17Plus(BOSMiner, S17Plus):
|
||||
pass
|
||||
|
||||
|
||||
class BOSMinerS17Pro(BOSer, S17Pro):
|
||||
class BOSMinerS17Pro(BOSMiner, S17Pro):
|
||||
pass
|
||||
|
||||
|
||||
class BOSMinerS17e(BOSer, S17e):
|
||||
class BOSMinerS17e(BOSMiner, S17e):
|
||||
pass
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pyasic.miners.backends import BOSer
|
||||
from pyasic.miners.backends import BOSMiner
|
||||
from pyasic.miners.models import T17, T17e, T17Plus
|
||||
|
||||
|
||||
class BOSMinerT17(BOSer, T17):
|
||||
class BOSMinerT17(BOSMiner, T17):
|
||||
pass
|
||||
|
||||
|
||||
class BOSMinerT17Plus(BOSer, T17Plus):
|
||||
class BOSMinerT17Plus(BOSMiner, T17Plus):
|
||||
pass
|
||||
|
||||
|
||||
class BOSMinerT17e(BOSer, T17e):
|
||||
class BOSMinerT17e(BOSMiner, T17e):
|
||||
pass
|
||||
|
||||
@@ -206,7 +206,7 @@ class AntminerModern(BMMiner):
|
||||
]
|
||||
|
||||
try:
|
||||
rpc_stats = await self.rpc.send_command("stats", new_rpc=True)
|
||||
rpc_stats = await self.rpc.send_command("stats", new_api=True)
|
||||
except APIError:
|
||||
return hashboards
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class BFGMiner(BaseMiner):
|
||||
except APIError:
|
||||
return self.config
|
||||
|
||||
self.config = MinerConfig.from_rpc(pools)
|
||||
self.config = MinerConfig.from_api(pools)
|
||||
return self.config
|
||||
|
||||
##################################################
|
||||
@@ -84,11 +84,11 @@ class BFGMiner(BaseMiner):
|
||||
|
||||
if rpc_version is not None:
|
||||
try:
|
||||
self.rpc_ver = rpc_version["VERSION"][0]["API"]
|
||||
self.api_ver = rpc_version["VERSION"][0]["API"]
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
return self.rpc_ver
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
if rpc_version is None:
|
||||
|
||||
@@ -72,7 +72,7 @@ class BMMiner(BaseMiner):
|
||||
except APIError:
|
||||
return self.config
|
||||
|
||||
self.config = MinerConfig.from_rpc(pools)
|
||||
self.config = MinerConfig.from_api(pools)
|
||||
return self.config
|
||||
|
||||
##################################################
|
||||
@@ -88,11 +88,11 @@ class BMMiner(BaseMiner):
|
||||
|
||||
if rpc_version is not None:
|
||||
try:
|
||||
self.rpc_ver = rpc_version["VERSION"][0]["API"]
|
||||
self.api_ver = rpc_version["VERSION"][0]["API"]
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
return self.rpc_ver
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
if rpc_version is None:
|
||||
|
||||
@@ -305,10 +305,10 @@ class BOSMiner(BaseMiner):
|
||||
rpc_ver = rpc_version["VERSION"][0]["API"]
|
||||
except LookupError:
|
||||
rpc_ver = None
|
||||
self.rpc_ver = rpc_ver
|
||||
self.rpc.rpc_ver = self.rpc_ver
|
||||
self.api_ver = rpc_ver
|
||||
self.rpc.rpc_ver = self.api_ver
|
||||
|
||||
return self.rpc_ver
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, web_bos_info: dict = None) -> Optional[str]:
|
||||
if web_bos_info is None:
|
||||
@@ -731,10 +731,10 @@ class BOSer(BaseMiner):
|
||||
rpc_ver = rpc_version["VERSION"][0]["API"]
|
||||
except LookupError:
|
||||
rpc_ver = None
|
||||
self.rpc_ver = rpc_ver
|
||||
self.rpc.rpc_ver = self.rpc_ver
|
||||
self.api_ver = rpc_ver
|
||||
self.rpc.rpc_ver = self.api_ver
|
||||
|
||||
return self.rpc_ver
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, grpc_miner_details: dict = None) -> Optional[str]:
|
||||
if grpc_miner_details is None:
|
||||
|
||||
@@ -234,7 +234,7 @@ class BTMiner(BaseMiner):
|
||||
pass
|
||||
|
||||
if pools is not None:
|
||||
cfg = MinerConfig.from_rpc(pools)
|
||||
cfg = MinerConfig.from_api(pools)
|
||||
else:
|
||||
cfg = MinerConfig()
|
||||
|
||||
@@ -325,14 +325,14 @@ class BTMiner(BaseMiner):
|
||||
rpc_ver = rpc_get_version["Msg"]
|
||||
if not isinstance(rpc_ver, str):
|
||||
rpc_ver = rpc_ver["rpc_ver"]
|
||||
self.rpc_ver = rpc_ver.replace("whatsminer v", "")
|
||||
self.api_ver = rpc_ver.replace("whatsminer v", "")
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
else:
|
||||
self.rpc.rpc_ver = self.rpc_ver
|
||||
return self.rpc_ver
|
||||
self.rpc.rpc_ver = self.api_ver
|
||||
return self.api_ver
|
||||
|
||||
return self.rpc_ver
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(
|
||||
self, rpc_get_version: dict = None, rpc_summary: dict = None
|
||||
|
||||
@@ -71,7 +71,7 @@ class CGMiner(BaseMiner):
|
||||
except APIError:
|
||||
return self.config
|
||||
|
||||
self.config = MinerConfig.from_rpc(pools)
|
||||
self.config = MinerConfig.from_api(pools)
|
||||
return self.config
|
||||
|
||||
##################################################
|
||||
@@ -87,11 +87,11 @@ class CGMiner(BaseMiner):
|
||||
|
||||
if rpc_version is not None:
|
||||
try:
|
||||
self.rpc_ver = rpc_version["VERSION"][0]["API"]
|
||||
self.api_ver = rpc_version["VERSION"][0]["API"]
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
return self.rpc_ver
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
if rpc_version is None:
|
||||
|
||||
@@ -116,6 +116,7 @@ class ePIC(BaseMiner):
|
||||
if not conf.get("temps", {}) == {}:
|
||||
await self.web.set_shutdown_temp(conf["temps"]["shutdown"])
|
||||
# Fans
|
||||
# set with sub-keys instead of conf["fans"] because sometimes both can be set
|
||||
if not conf["fans"].get("Manual", {}) == {}:
|
||||
await self.web.set_fan({"Manual": conf["fans"]["Manual"]})
|
||||
elif not conf["fans"].get("Auto", {}) == {}:
|
||||
|
||||
@@ -205,13 +205,14 @@ class VNish(BMMiner):
|
||||
if web_summary is None:
|
||||
web_summary = await self.web.summary()
|
||||
|
||||
fw_ver = None
|
||||
if web_summary is not None:
|
||||
try:
|
||||
fw_ver = web_summary["miner"]["miner_type"]
|
||||
fw_ver = fw_ver.split("(Vnish ")[1].replace(")", "")
|
||||
return fw_ver
|
||||
except KeyError:
|
||||
pass
|
||||
except LookupError:
|
||||
return fw_ver
|
||||
|
||||
async def get_config(self) -> MinerConfig:
|
||||
try:
|
||||
|
||||
@@ -289,7 +289,9 @@ MINER_CLASSES = {
|
||||
"M50S++VK30": BTMinerM50SPlusPlusVK30,
|
||||
"M53VH30": BTMinerM53VH30,
|
||||
"M53SVH30": BTMinerM53SVH30,
|
||||
"M53SVJ40": BTMinerM53SVJ40,
|
||||
"M53S+VJ30": BTMinerM53SPlusVJ30,
|
||||
"M53S++VK10": BTMinerM53SPlusPlusVK10,
|
||||
"M56VH30": BTMinerM56VH30,
|
||||
"M56SVH30": BTMinerM56SVH30,
|
||||
"M56S+VJ30": BTMinerM56SPlusVJ30,
|
||||
@@ -968,6 +970,7 @@ class MinerFactory:
|
||||
|
||||
miner_factory = MinerFactory()
|
||||
|
||||
|
||||
# abstracted version of get miner that is easier to access
|
||||
async def get_miner(ip: ipaddress.ip_address | str) -> AnyMiner:
|
||||
return await miner_factory.get_miner(ip)
|
||||
|
||||
@@ -20,3 +20,8 @@ from pyasic.miners.makes import WhatsMinerMake
|
||||
class M53SVH30(WhatsMinerMake):
|
||||
raw_model = "M53S VH30"
|
||||
expected_fans = 0
|
||||
|
||||
|
||||
class M53SVJ40(WhatsMinerMake):
|
||||
raw_model = "M53S VJ40"
|
||||
expected_fans = 0
|
||||
|
||||
22
pyasic/miners/models/whatsminer/M5X/M53S_Plus_Plus.py
Normal file
22
pyasic/miners/models/whatsminer/M5X/M53S_Plus_Plus.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright 2022 Upstream Data Inc -
|
||||
# -
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); -
|
||||
# you may not use this file except in compliance with the License. -
|
||||
# You may obtain a copy of the License at -
|
||||
# -
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 -
|
||||
# -
|
||||
# Unless required by applicable law or agreed to in writing, software -
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, -
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pyasic.miners.makes import WhatsMinerMake
|
||||
|
||||
|
||||
class M53SPlusPlusVK10(WhatsMinerMake):
|
||||
raw_model = "M53S++ VK10"
|
||||
expected_fans = 0
|
||||
@@ -42,8 +42,9 @@ from .M50S import (
|
||||
from .M50S_Plus import M50SPlusVH30, M50SPlusVH40, M50SPlusVJ30, M50SPlusVK20
|
||||
from .M50S_Plus_Plus import M50SPlusPlusVK10, M50SPlusPlusVK20, M50SPlusPlusVK30
|
||||
from .M53 import M53VH30
|
||||
from .M53S import M53SVH30
|
||||
from .M53S import M53SVH30, M53SVJ40
|
||||
from .M53S_Plus import M53SPlusVJ30
|
||||
from .M53S_Plus_Plus import M53SPlusPlusVK10
|
||||
from .M56 import M56VH30
|
||||
from .M56S import M56SVH30
|
||||
from .M56S_Plus import M56SPlusVJ30
|
||||
|
||||
@@ -15,8 +15,12 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pyasic.miners.backends import M5X
|
||||
from pyasic.miners.models import M53SVH30
|
||||
from pyasic.miners.models import M53SVH30, M53SVJ40
|
||||
|
||||
|
||||
class BTMinerM53SVH30(M5X, M53SVH30):
|
||||
pass
|
||||
|
||||
|
||||
class BTMinerM53SVJ40(M5X, M53SVJ40):
|
||||
pass
|
||||
|
||||
22
pyasic/miners/whatsminer/btminer/M5X/M53S_Plus_Plus.py
Normal file
22
pyasic/miners/whatsminer/btminer/M5X/M53S_Plus_Plus.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright 2022 Upstream Data Inc -
|
||||
# -
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); -
|
||||
# you may not use this file except in compliance with the License. -
|
||||
# You may obtain a copy of the License at -
|
||||
# -
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 -
|
||||
# -
|
||||
# Unless required by applicable law or agreed to in writing, software -
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, -
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pyasic.miners.backends import M5X
|
||||
from pyasic.miners.models import M53SPlusPlusVK10
|
||||
|
||||
|
||||
class BTMinerM53SPlusPlusVK10(M5X, M53SPlusPlusVK10):
|
||||
pass
|
||||
@@ -51,8 +51,9 @@ from .M50S_Plus_Plus import (
|
||||
BTMinerM50SPlusPlusVK30,
|
||||
)
|
||||
from .M53 import BTMinerM53VH30
|
||||
from .M53S import BTMinerM53SVH30
|
||||
from .M53S import BTMinerM53SVH30, BTMinerM53SVJ40
|
||||
from .M53S_Plus import BTMinerM53SPlusVJ30
|
||||
from .M53S_Plus_Plus import BTMinerM53SPlusPlusVK10
|
||||
from .M56 import BTMinerM56VH30
|
||||
from .M56S import BTMinerM56SVH30
|
||||
from .M56S_Plus import BTMinerM56SPlusVJ30
|
||||
|
||||
@@ -107,3 +107,4 @@ def validate_command_output(data: dict) -> tuple[bool, str | None]:
|
||||
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
|
||||
# this is an error
|
||||
return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"]
|
||||
return True, None
|
||||
|
||||
@@ -78,6 +78,9 @@ class BaseMinerRPCAPI:
|
||||
# send the command
|
||||
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
|
||||
|
||||
if data is None:
|
||||
raise APIError("No data returned from the API.")
|
||||
|
||||
if data == b"Socket connect failed: Connection refused\n":
|
||||
if not ignore_errors:
|
||||
raise APIError(data.decode("utf-8"))
|
||||
@@ -90,7 +93,7 @@ class BaseMinerRPCAPI:
|
||||
if not validation[0]:
|
||||
if not ignore_errors:
|
||||
# validate the command succeeded
|
||||
raise APIError(validation[1])
|
||||
raise APIError(f"{command}: {validation[1]}")
|
||||
if allow_warning:
|
||||
logging.warning(
|
||||
f"{self.ip}: API Command Error: {command}: {validation[1]}"
|
||||
@@ -193,12 +196,15 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
async def _send_bytes(
|
||||
self,
|
||||
data: bytes,
|
||||
port: int = None,
|
||||
timeout: int = 100,
|
||||
) -> bytes:
|
||||
if port is None:
|
||||
port = self.port
|
||||
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
|
||||
try:
|
||||
# get reader and writer streams
|
||||
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
||||
reader, writer = await asyncio.open_connection(str(self.ip), port)
|
||||
# handle OSError 121
|
||||
except OSError as e:
|
||||
if e.errno == 121:
|
||||
@@ -208,39 +214,14 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
return b"{}"
|
||||
|
||||
# send the command
|
||||
data_task = asyncio.create_task(self._read_bytes(reader, timeout=timeout))
|
||||
logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing")
|
||||
writer.write(data)
|
||||
logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
|
||||
await writer.drain()
|
||||
try:
|
||||
# TO address a situation where a whatsminer has an unknown PW -AND-
|
||||
# Fix for stupid whatsminer bug, reboot/restart seem to not load properly in the loop
|
||||
# have to receive, save the data, check if there is more data by reading with a short timeout
|
||||
# append that data if there is more, and then onto the main loop.
|
||||
# the password timeout might need to be longer than 1, but it seems to work for now.
|
||||
ret_data = await asyncio.wait_for(reader.read(1), timeout=1)
|
||||
except asyncio.TimeoutError:
|
||||
return b"{}"
|
||||
try:
|
||||
ret_data += await asyncio.wait_for(reader.read(4096), timeout=timeout)
|
||||
except ConnectionAbortedError:
|
||||
return b"{}"
|
||||
|
||||
# loop to receive all the data
|
||||
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
d = await asyncio.wait_for(reader.read(4096), timeout=timeout)
|
||||
if not d:
|
||||
break
|
||||
ret_data += d
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
|
||||
raise e
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
|
||||
await data_task
|
||||
ret_data = data_task.result()
|
||||
|
||||
# close the connection
|
||||
logging.debug(f"{self} - ([Hidden] Send Bytes) - Closing")
|
||||
@@ -249,6 +230,19 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
|
||||
return ret_data
|
||||
|
||||
async def _read_bytes(self, reader: asyncio.StreamReader, timeout: int) -> bytes:
|
||||
ret_data = b""
|
||||
|
||||
# loop to receive all the data
|
||||
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
|
||||
try:
|
||||
ret_data = await asyncio.wait_for(reader.read(), timeout=timeout)
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
|
||||
return ret_data
|
||||
|
||||
@staticmethod
|
||||
def _load_api_data(data: bytes) -> dict:
|
||||
# some json from the API returns with a null byte (\x00) on the end
|
||||
|
||||
@@ -24,12 +24,13 @@ import logging
|
||||
import re
|
||||
from typing import Literal, Union
|
||||
|
||||
import httpx
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from passlib.handlers.md5_crypt import md5_crypt
|
||||
|
||||
from pyasic import settings
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.misc import api_min_version
|
||||
from pyasic.misc import api_min_version, validate_command_output
|
||||
from pyasic.rpc.base import BaseMinerRPCAPI
|
||||
|
||||
### IMPORTANT ###
|
||||
@@ -240,6 +241,28 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
ignore_errors: bool = False,
|
||||
timeout: int = 10,
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
try:
|
||||
return await self._send_privileged_command(
|
||||
command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
|
||||
)
|
||||
except APIError as e:
|
||||
if not e.message == "can't access write cmd":
|
||||
raise
|
||||
try:
|
||||
await self.open_api()
|
||||
except Exception as e:
|
||||
raise APIError("Failed to open whatsminer API.") from e
|
||||
return await self._send_privileged_command(
|
||||
command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
|
||||
)
|
||||
|
||||
async def _send_privileged_command(
|
||||
self,
|
||||
command: Union[str, bytes],
|
||||
ignore_errors: bool = False,
|
||||
timeout: int = 10,
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
logging.debug(
|
||||
f"{self} - (Send Privileged Command) - {command} " + f"with args {kwargs}"
|
||||
@@ -272,7 +295,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
|
||||
if not ignore_errors:
|
||||
# if it fails to validate, it is likely an error
|
||||
validation = self._validate_command_output(data)
|
||||
validation = validate_command_output(data)
|
||||
if not validation[0]:
|
||||
raise APIError(validation[1])
|
||||
|
||||
@@ -321,6 +344,30 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
logging.debug(f"{self} - (Get Token) - Gathered token data: {self.token}")
|
||||
return self.token
|
||||
|
||||
async def open_api(self):
|
||||
async with httpx.AsyncClient() as c:
|
||||
stage1_req = (
|
||||
await c.post(
|
||||
"https://wmt.pyasic.org/v1/stage1",
|
||||
json={"ip": self.ip},
|
||||
follow_redirects=True,
|
||||
)
|
||||
).json()
|
||||
stage1_res = binascii.hexlify(
|
||||
await self._send_bytes(binascii.unhexlify(stage1_req), port=8889)
|
||||
)
|
||||
stage2_req = (
|
||||
await c.post(
|
||||
"https://wmt.pyasic.org/v1/stage2",
|
||||
json={"ip": self.ip, "stage1_result": stage1_res.decode("utf-8")},
|
||||
)
|
||||
).json()
|
||||
try:
|
||||
await self._send_bytes(binascii.unhexlify(stage2_req), timeout=3, port=8889)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
return True
|
||||
|
||||
#### PRIVILEGED COMMANDS ####
|
||||
# Please read the top of this file to learn
|
||||
# how to configure the Whatsminer API to
|
||||
@@ -607,10 +654,10 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
A reply informing of the status of setting the frequency.
|
||||
</details>
|
||||
"""
|
||||
if not -10 < percent < 100:
|
||||
if not -100 < percent < 100:
|
||||
raise APIError(
|
||||
f"Frequency % is outside of the allowed "
|
||||
f"range. Please set a % between -10 and "
|
||||
f"range. Please set a % between -100 and "
|
||||
f"100"
|
||||
)
|
||||
return await self.send_privileged_command(
|
||||
|
||||
@@ -70,13 +70,15 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
not func.startswith("__") and not func.startswith("_")
|
||||
]
|
||||
|
||||
async def multicommand(self, *commands: str) -> dict:
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
) -> dict:
|
||||
result = {"multicommand": True}
|
||||
tasks = {}
|
||||
for command in commands:
|
||||
try:
|
||||
tasks[command] = asyncio.create_task(getattr(self, command)())
|
||||
except AttributeError:
|
||||
except (APIError, AttributeError):
|
||||
result["command"] = {}
|
||||
|
||||
await asyncio.gather(*list(tasks.values()))
|
||||
|
||||
@@ -59,10 +59,14 @@ class BOSMinerWebAPI(BaseWebAPI):
|
||||
return {}
|
||||
raise APIError(f"LUCI web command failed: command={command}")
|
||||
|
||||
async def multicommand(self, *commands: str) -> dict:
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
) -> dict:
|
||||
data = {}
|
||||
for command in commands:
|
||||
data[command] = await self.send_command(command, ignore_errors=True)
|
||||
data[command] = await self.send_command(
|
||||
command, ignore_errors=ignore_errors
|
||||
)
|
||||
return data
|
||||
|
||||
async def auth(self, session: httpx.AsyncClient) -> None:
|
||||
|
||||
@@ -44,40 +44,37 @@ class ePICWebAPI(BaseWebAPI):
|
||||
post = privileged or not parameters == {}
|
||||
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
for i in range(settings.get("get_data_retries", 1) + 1):
|
||||
try:
|
||||
if post:
|
||||
response = await client.post(
|
||||
f"http://{self.ip}:{self.port}/{command}",
|
||||
timeout=5,
|
||||
json={
|
||||
**parameters,
|
||||
"password": self.pwd,
|
||||
},
|
||||
try:
|
||||
if post:
|
||||
response = await client.post(
|
||||
f"http://{self.ip}:{self.port}/{command}",
|
||||
timeout=5,
|
||||
json={
|
||||
**parameters,
|
||||
"password": self.pwd,
|
||||
},
|
||||
)
|
||||
else:
|
||||
response = await client.get(
|
||||
f"http://{self.ip}:{self.port}/{command}",
|
||||
timeout=5,
|
||||
)
|
||||
if not response.status_code == 200:
|
||||
if not ignore_errors:
|
||||
raise APIError(
|
||||
f"Web command {command} failed with status code {response.status_code}"
|
||||
)
|
||||
else:
|
||||
response = await client.get(
|
||||
f"http://{self.ip}:{self.port}/{command}",
|
||||
timeout=5,
|
||||
)
|
||||
if not response.status_code == 200:
|
||||
continue
|
||||
json_data = response.json()
|
||||
if json_data:
|
||||
# The API can return a fail status if the miner cannot return the requested data. Catch this and pass
|
||||
if (
|
||||
"result" in json_data
|
||||
and json_data["result"] is False
|
||||
and not post
|
||||
):
|
||||
if not i > settings.get("get_data_retries", 1):
|
||||
continue
|
||||
if not ignore_errors:
|
||||
raise APIError(json_data["error"])
|
||||
return json_data
|
||||
return {"success": True}
|
||||
except (httpx.HTTPError, json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
return {}
|
||||
json_data = response.json()
|
||||
if json_data:
|
||||
# The API can return a fail status if the miner cannot return the requested data. Catch this and pass
|
||||
if not json_data.get("result", True) and not post:
|
||||
if not ignore_errors:
|
||||
raise APIError(json_data["error"])
|
||||
return json_data
|
||||
return {"success": True}
|
||||
except (httpx.HTTPError, json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
@@ -95,19 +92,19 @@ class ePICWebAPI(BaseWebAPI):
|
||||
return await self.send_command("reboot", privileged=True)
|
||||
|
||||
async def set_shutdown_temp(self, params: int) -> dict:
|
||||
return await self.send_command("shutdowntemp", parameters=params)
|
||||
return await self.send_command("shutdowntemp", param=params)
|
||||
|
||||
async def set_fan(self, params: dict) -> dict:
|
||||
return await self.send_command("fanspeed", parameters=params)
|
||||
return await self.send_command("fanspeed", param=params)
|
||||
|
||||
async def set_ptune_enable(self, params: bool) -> dict:
|
||||
return await self.send_command("perpetualtune", parameters=params)
|
||||
return await self.send_command("perpetualtune", param=params)
|
||||
|
||||
async def set_ptune_algo(self, params: dict) -> dict:
|
||||
return await self.send_command("perpetualtune/algo", parameters=params)
|
||||
return await self.send_command("perpetualtune/algo", param=params)
|
||||
|
||||
async def set_pools(self, params: dict) -> dict:
|
||||
return await self.send_command("coin", parameters=params)
|
||||
return await self.send_command("coin", param=params)
|
||||
|
||||
async def pause_mining(self) -> dict:
|
||||
return await self.send_command("miner", param="Stop")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pyasic"
|
||||
version = "0.50.1"
|
||||
version = "0.51.1"
|
||||
description = "A simplified and standardized interface for Bitcoin ASICs."
|
||||
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
||||
repository = "https://github.com/UpstreamData/pyasic"
|
||||
|
||||
Reference in New Issue
Block a user