Compare commits

...

16 Commits

Author SHA1 Message Date
UpstreamData
88769e40ae version: bump version number. 2024-01-30 13:34:24 -07:00
UpstreamData
be45eb7400 bug: fix issues with bosminer multicommand, and update X17 to use BOSMiner instead of BOSer. 2024-01-30 13:34:00 -07:00
b-rowan
2f719a03a4 version: bump version number. 2024-01-29 20:57:01 -07:00
b-rowan
64196f9754 bug: update whatsminer set_target_freq to match docs. 2024-01-29 20:56:36 -07:00
UpstreamData
49a77f1b79 version: bump version number. 2024-01-29 12:47:54 -07:00
UpstreamData
3838c4f2f9 bug: fix missing validation import for BTMiner. 2024-01-29 12:47:30 -07:00
UpstreamData
80d89c95b5 version: bump version number. 2024-01-29 12:33:07 -07:00
UpstreamData
30cd8b5cfe bug: fix some issues with rpc renaming. 2024-01-29 12:32:54 -07:00
b-rowan
c443170f78 refactor: improve epic web send_command implementation. 2024-01-27 09:42:35 -07:00
b-rowan
a2c2aa2377 version: bump version number. 2024-01-27 09:26:13 -07:00
b-rowan
4f0eb49a02 bug: fix some issues with epic send config. 2024-01-27 09:25:20 -07:00
b-rowan
a821357b4f Merge pull request #102 from jpcomps/master
fix send_config for ePIC
2024-01-27 09:05:23 -07:00
John-Paul Compagnone
3c7679a22d fix send_config for ePIC 2024-01-27 10:50:37 -05:00
UpstreamData
a52737e236 refactor: add some type hints for epic config. 2024-01-26 13:58:08 -07:00
UpstreamData
7c96bbe153 version: bump version number. 2024-01-26 13:56:15 -07:00
UpstreamData
e8bbf22aa7 bug: fix a bug with epic mining mode configs. 2024-01-26 13:55:54 -07:00
18 changed files with 91 additions and 96 deletions

View File

@@ -111,21 +111,21 @@ class MiningModeHPM(MinerConfigValue):
class StandardPowerTuneAlgo(MinerConfigValue): class StandardPowerTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard") mode: str = field(init=False, default="standard")
def as_epic(self): def as_epic(self) -> str:
return VOptPowerTuneAlgo().as_epic() return VOptPowerTuneAlgo().as_epic()
class VOptPowerTuneAlgo(MinerConfigValue): class VOptPowerTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard") mode: str = field(init=False, default="standard")
def as_epic(self): def as_epic(self) -> str:
return "VoltageOptimizer" return "VoltageOptimizer"
class ChipTunePowerTuneAlgo(MinerConfigValue): class ChipTunePowerTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard") mode: str = field(init=False, default="standard")
def as_epic(self): def as_epic(self) -> str:
return "ChipTune" return "ChipTune"
@@ -143,7 +143,7 @@ class PowerTunerAlgo(MinerConfigOption):
class MiningModePowerTune(MinerConfigValue): class MiningModePowerTune(MinerConfigValue):
mode: str = field(init=False, default="power_tuning") mode: str = field(init=False, default="power_tuning")
power: int = None power: int = None
algo: PowerTunerAlgo = PowerTunerAlgo.default() algo: PowerTunerAlgo = field(default_factory=PowerTunerAlgo.default)
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune": def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune":
@@ -184,7 +184,7 @@ class MiningModePowerTune(MinerConfigValue):
return {"mode": {"mode": "custom", "tune": "power", "power": self.power}} return {"mode": {"mode": "custom", "tune": "power", "power": self.power}}
def as_epic(self) -> dict: def as_epic(self) -> dict:
return {"ptune": {**self.algo.as_epic(), "target": self.power}} return {"ptune": {"algo": self.algo.as_epic(), "target": self.power}}
@dataclass @dataclass

View File

@@ -265,15 +265,7 @@ class PoolGroup(MinerConfigValue):
return [p.as_auradine(user_suffix=user_suffix) for p in self.pools] return [p.as_auradine(user_suffix=user_suffix) for p in self.pools]
def as_epic(self, user_suffix: str = None) -> dict: def as_epic(self, user_suffix: str = None) -> dict:
if len(self.pools) > 0: return [p.as_epic(user_suffix=user_suffix) for p in self.pools]
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": []}
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "PoolGroup": def from_dict(cls, dict_conf: dict | None) -> "PoolGroup":
@@ -421,9 +413,7 @@ class PoolConfig(MinerConfigValue):
return { return {
"pools": { "pools": {
"coin": "Btc", "coin": "Btc",
"stratum_configs": [ "stratum_configs": self.groups[0].as_epic(user_suffix=user_suffix),
g.as_epic(user_suffix=user_suffix) for g in self.groups
],
"unique_id": False, "unique_id": False,
} }
} }

View File

@@ -43,9 +43,9 @@ class TemperatureConfig(MinerConfigValue):
def as_epic(self) -> dict: def as_epic(self) -> dict:
temps_config = {"temps": {}, "fans": {"Auto": {}}} temps_config = {"temps": {}, "fans": {"Auto": {}}}
if self.target is not None: if self.target is not None:
temps_config["fans"]["Target Temperature"] = self.target temps_config["fans"]["Auto"]["Target Temperature"] = self.target
else: else:
temps_config["fans"]["Target Temperature"] = 60 temps_config["fans"]["Auto"]["Target Temperature"] = 60
if self.danger is not None: if self.danger is not None:
temps_config["temps"]["shutdown"] = self.danger temps_config["temps"]["shutdown"] = self.danger
return temps_config return temps_config

View File

@@ -14,21 +14,21 @@
# limitations under the License. - # 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 from pyasic.miners.models import S17, S17e, S17Plus, S17Pro
class BOSMinerS17(BOSer, S17): class BOSMinerS17(BOSMiner, S17):
pass pass
class BOSMinerS17Plus(BOSer, S17Plus): class BOSMinerS17Plus(BOSMiner, S17Plus):
pass pass
class BOSMinerS17Pro(BOSer, S17Pro): class BOSMinerS17Pro(BOSMiner, S17Pro):
pass pass
class BOSMinerS17e(BOSer, S17e): class BOSMinerS17e(BOSMiner, S17e):
pass pass

View File

@@ -14,17 +14,17 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import BOSer from pyasic.miners.backends import BOSMiner
from pyasic.miners.models import T17, T17e, T17Plus from pyasic.miners.models import T17, T17e, T17Plus
class BOSMinerT17(BOSer, T17): class BOSMinerT17(BOSMiner, T17):
pass pass
class BOSMinerT17Plus(BOSer, T17Plus): class BOSMinerT17Plus(BOSMiner, T17Plus):
pass pass
class BOSMinerT17e(BOSer, T17e): class BOSMinerT17e(BOSMiner, T17e):
pass pass

View File

@@ -68,7 +68,7 @@ class BFGMiner(BaseMiner):
except APIError: except APIError:
return self.config return self.config
self.config = MinerConfig.from_rpc(pools) self.config = MinerConfig.from_api(pools)
return self.config return self.config
################################################## ##################################################
@@ -84,11 +84,11 @@ class BFGMiner(BaseMiner):
if rpc_version is not None: if rpc_version is not None:
try: try:
self.rpc_ver = rpc_version["VERSION"][0]["API"] self.api_ver = rpc_version["VERSION"][0]["API"]
except LookupError: except LookupError:
pass pass
return self.rpc_ver return self.api_ver
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]: async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
if rpc_version is None: if rpc_version is None:

View File

@@ -72,7 +72,7 @@ class BMMiner(BaseMiner):
except APIError: except APIError:
return self.config return self.config
self.config = MinerConfig.from_rpc(pools) self.config = MinerConfig.from_api(pools)
return self.config return self.config
################################################## ##################################################
@@ -88,11 +88,11 @@ class BMMiner(BaseMiner):
if rpc_version is not None: if rpc_version is not None:
try: try:
self.rpc_ver = rpc_version["VERSION"][0]["API"] self.api_ver = rpc_version["VERSION"][0]["API"]
except LookupError: except LookupError:
pass pass
return self.rpc_ver return self.api_ver
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]: async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
if rpc_version is None: if rpc_version is None:

View File

@@ -305,10 +305,10 @@ class BOSMiner(BaseMiner):
rpc_ver = rpc_version["VERSION"][0]["API"] rpc_ver = rpc_version["VERSION"][0]["API"]
except LookupError: except LookupError:
rpc_ver = None rpc_ver = None
self.rpc_ver = rpc_ver self.api_ver = rpc_ver
self.rpc.rpc_ver = self.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]: async def _get_fw_ver(self, web_bos_info: dict = None) -> Optional[str]:
if web_bos_info is None: if web_bos_info is None:
@@ -731,10 +731,10 @@ class BOSer(BaseMiner):
rpc_ver = rpc_version["VERSION"][0]["API"] rpc_ver = rpc_version["VERSION"][0]["API"]
except LookupError: except LookupError:
rpc_ver = None rpc_ver = None
self.rpc_ver = rpc_ver self.api_ver = rpc_ver
self.rpc.rpc_ver = self.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]: async def _get_fw_ver(self, grpc_miner_details: dict = None) -> Optional[str]:
if grpc_miner_details is None: if grpc_miner_details is None:

View File

@@ -234,7 +234,7 @@ class BTMiner(BaseMiner):
pass pass
if pools is not None: if pools is not None:
cfg = MinerConfig.from_rpc(pools) cfg = MinerConfig.from_api(pools)
else: else:
cfg = MinerConfig() cfg = MinerConfig()
@@ -325,14 +325,14 @@ class BTMiner(BaseMiner):
rpc_ver = rpc_get_version["Msg"] rpc_ver = rpc_get_version["Msg"]
if not isinstance(rpc_ver, str): if not isinstance(rpc_ver, str):
rpc_ver = rpc_ver["rpc_ver"] 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): except (KeyError, TypeError):
pass pass
else: else:
self.rpc.rpc_ver = self.rpc_ver self.rpc.rpc_ver = self.api_ver
return self.rpc_ver return self.api_ver
return self.rpc_ver return self.api_ver
async def _get_fw_ver( async def _get_fw_ver(
self, rpc_get_version: dict = None, rpc_summary: dict = None self, rpc_get_version: dict = None, rpc_summary: dict = None

View File

@@ -71,7 +71,7 @@ class CGMiner(BaseMiner):
except APIError: except APIError:
return self.config return self.config
self.config = MinerConfig.from_rpc(pools) self.config = MinerConfig.from_api(pools)
return self.config return self.config
################################################## ##################################################
@@ -87,11 +87,11 @@ class CGMiner(BaseMiner):
if rpc_version is not None: if rpc_version is not None:
try: try:
self.rpc_ver = rpc_version["VERSION"][0]["API"] self.api_ver = rpc_version["VERSION"][0]["API"]
except LookupError: except LookupError:
pass pass
return self.rpc_ver return self.api_ver
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]: async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
if rpc_version is None: if rpc_version is None:

View File

@@ -116,6 +116,7 @@ class ePIC(BaseMiner):
if not conf.get("temps", {}) == {}: if not conf.get("temps", {}) == {}:
await self.web.set_shutdown_temp(conf["temps"]["shutdown"]) await self.web.set_shutdown_temp(conf["temps"]["shutdown"])
# Fans # Fans
# set with sub-keys instead of conf["fans"] because sometimes both can be set
if not conf["fans"].get("Manual", {}) == {}: if not conf["fans"].get("Manual", {}) == {}:
await self.web.set_fan({"Manual": conf["fans"]["Manual"]}) await self.web.set_fan({"Manual": conf["fans"]["Manual"]})
elif not conf["fans"].get("Auto", {}) == {}: elif not conf["fans"].get("Auto", {}) == {}:

View File

@@ -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"]: if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
# this is an error # this is an error
return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"] return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"]
return True, None

View File

@@ -90,7 +90,7 @@ class BaseMinerRPCAPI:
if not validation[0]: if not validation[0]:
if not ignore_errors: if not ignore_errors:
# validate the command succeeded # validate the command succeeded
raise APIError(validation[1]) raise APIError(f"{command}: {validation[1]}")
if allow_warning: if allow_warning:
logging.warning( logging.warning(
f"{self.ip}: API Command Error: {command}: {validation[1]}" f"{self.ip}: API Command Error: {command}: {validation[1]}"

View File

@@ -29,7 +29,7 @@ from passlib.handlers.md5_crypt import md5_crypt
from pyasic import settings from pyasic import settings
from pyasic.errors import APIError 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 from pyasic.rpc.base import BaseMinerRPCAPI
### IMPORTANT ### ### IMPORTANT ###
@@ -272,7 +272,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
if not ignore_errors: if not ignore_errors:
# if it fails to validate, it is likely an error # 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]: if not validation[0]:
raise APIError(validation[1]) raise APIError(validation[1])
@@ -607,10 +607,10 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
A reply informing of the status of setting the frequency. A reply informing of the status of setting the frequency.
</details> </details>
""" """
if not -10 < percent < 100: if not -100 < percent < 100:
raise APIError( raise APIError(
f"Frequency % is outside of the allowed " 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" f"100"
) )
return await self.send_privileged_command( return await self.send_privileged_command(

View File

@@ -70,13 +70,15 @@ class BOSerWebAPI(BaseWebAPI):
not func.startswith("__") and not func.startswith("_") 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} result = {"multicommand": True}
tasks = {} tasks = {}
for command in commands: for command in commands:
try: try:
tasks[command] = asyncio.create_task(getattr(self, command)()) tasks[command] = asyncio.create_task(getattr(self, command)())
except AttributeError: except (APIError, AttributeError):
result["command"] = {} result["command"] = {}
await asyncio.gather(*list(tasks.values())) await asyncio.gather(*list(tasks.values()))

View File

@@ -59,10 +59,14 @@ class BOSMinerWebAPI(BaseWebAPI):
return {} return {}
raise APIError(f"LUCI web command failed: command={command}") 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 = {} data = {}
for command in commands: 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 return data
async def auth(self, session: httpx.AsyncClient) -> None: async def auth(self, session: httpx.AsyncClient) -> None:

View File

@@ -44,40 +44,37 @@ class ePICWebAPI(BaseWebAPI):
post = privileged or not parameters == {} post = privileged or not parameters == {}
async with httpx.AsyncClient(transport=settings.transport()) as client: async with httpx.AsyncClient(transport=settings.transport()) as client:
for i in range(settings.get("get_data_retries", 1) + 1): try:
try: if post:
if post: response = await client.post(
response = await client.post( f"http://{self.ip}:{self.port}/{command}",
f"http://{self.ip}:{self.port}/{command}", timeout=5,
timeout=5, json={
json={ **parameters,
**parameters, "password": self.pwd,
"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: return {}
response = await client.get( json_data = response.json()
f"http://{self.ip}:{self.port}/{command}", if json_data:
timeout=5, # 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 response.status_code == 200: if not ignore_errors:
continue raise APIError(json_data["error"])
json_data = response.json() return json_data
if json_data: return {"success": True}
# The API can return a fail status if the miner cannot return the requested data. Catch this and pass except (httpx.HTTPError, json.JSONDecodeError, AttributeError):
if ( pass
"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
async def multicommand( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True 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) return await self.send_command("reboot", privileged=True)
async def set_shutdown_temp(self, params: int) -> dict: 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: 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: 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: 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: 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: async def pause_mining(self) -> dict:
return await self.send_command("miner", param="Stop") return await self.send_command("miner", param="Stop")

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pyasic" name = "pyasic"
version = "0.50.0" version = "0.50.6"
description = "A simplified and standardized interface for Bitcoin ASICs." description = "A simplified and standardized interface for Bitcoin ASICs."
authors = ["UpstreamData <brett@upstreamdata.ca>"] authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic" repository = "https://github.com/UpstreamData/pyasic"