Compare commits

..

13 Commits

Author SHA1 Message Date
Upstream Data
c4697728e4 version: bump version number 2025-02-13 15:38:53 -07:00
Upstream Data
de64172073 feature: add support for Hiveon S19kPro 2025-02-13 15:38:28 -07:00
Upstream Data
4d7a13433b version: bump version number 2025-02-12 16:08:23 -07:00
Adrian
e14a4791b2 it turns out there are 3 hashboards per miner (#293) 2025-02-12 16:07:46 -07:00
Upstream Data
0e76d4550b version: bump version number 2025-02-11 15:47:56 -07:00
Upstream Data
2d424025e9 feature: add MAC address to mskminer 2025-02-11 15:47:37 -07:00
Upstream Data
bf4903ce4b refactor: remove some unused imports 2025-02-11 09:38:31 -07:00
Upstream Data
4f7f6bf045 version: bump version number 2025-02-11 09:34:57 -07:00
Upstream Data
824890ec97 feature: add very basic support for MSKminer 2025-02-11 09:34:23 -07:00
Upstream Data
ce9d7ffb0f bug: fix issue with vnish preset parsing on some miners 2025-02-11 09:00:59 -07:00
Upstream Data
183b4934c1 refactor: remote unused import in test 2025-02-06 11:44:15 -07:00
Upstream Data
3d2b260b17 version: bump version number 2025-02-06 11:42:33 -07:00
Upstream Data
f88c1734eb bug: fix some issues with avalon 1566, and add tests 2025-02-06 11:42:17 -07:00
29 changed files with 1365 additions and 12 deletions

View File

@@ -62,6 +62,8 @@ def backend_str(backend: MinerTypes) -> str:
return "Stock Firmware Volcminers"
case MinerTypes.ELPHAPEX:
return "Stock Firmware Elphapex Miners"
case MinerTypes.MSKMINER:
return "MSKMiner Firmware Miners"
raise TypeError("Unknown miner backend, cannot generate docs")

View File

@@ -703,6 +703,19 @@
show_root_heading: false
heading_level: 0
## S19K Pro (Hive)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.hiveon.X19.S19.HiveonS19kPro
handler: python
options:
show_root_heading: false
heading_level: 0
## S19 No PIC (Hive)
- [ ] Shutdowns
@@ -716,6 +729,19 @@
show_root_heading: false
heading_level: 0
## S19 No PIC (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.mskminer.X19.S19.MSKMinerS19NoPIC
handler: python
options:
show_root_heading: false
heading_level: 0
## S19 (LuxOS)
- [x] Shutdowns

View File

@@ -769,12 +769,24 @@ details {
<ul>
<li><a href="../antminer/X19#s19j-pro-hive">S19j Pro (Hive)</a></li>
<li><a href="../antminer/X19#s19-hive">S19 (Hive)</a></li>
<li><a href="../antminer/X19#s19k-pro-hive">S19K Pro (Hive)</a></li>
<li><a href="../antminer/X19#s19-no-pic-hive">S19 No PIC (Hive)</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>MSKMiner Firmware Miners:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-no-pic-stock">S19 No PIC (Stock)</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>LuxOS Firmware Miners:</summary>
<ul>
<details>

View File

@@ -56,6 +56,16 @@ class MinerConfig(BaseModel):
**self.temperature.as_am_modern(),
}
def as_hiveon_modern(self, user_suffix: str | None = None) -> dict:
"""Generates the configuration in the format suitable for modern Hiveon."""
return {
**self.fan_mode.as_hiveon_modern(),
"freq-level": "100",
**self.mining_mode.as_hiveon_modern(),
**self.pools.as_hiveon_modern(user_suffix=user_suffix),
**self.temperature.as_hiveon_modern(),
}
def as_elphapex(self, user_suffix: str | None = None) -> dict:
"""Generates the configuration in the format suitable for modern Elphapex."""
return {
@@ -209,6 +219,15 @@ class MinerConfig(BaseModel):
fan_mode=FanModeConfig.from_am_modern(web_conf),
)
@classmethod
def from_hiveon_modern(cls, web_conf: dict) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for Hiveon."""
return cls(
pools=PoolConfig.from_hiveon_modern(web_conf),
mining_mode=MiningModeConfig.from_hiveon_modern(web_conf),
fan_mode=FanModeConfig.from_hiveon_modern(web_conf),
)
@classmethod
def from_elphapex(cls, web_conf: dict) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for modern Antminers."""
@@ -327,7 +346,3 @@ class MinerConfig(BaseModel):
@classmethod
def from_hammer(cls, *args, **kwargs) -> "MinerConfig":
return cls.from_am_modern(*args, **kwargs)
@classmethod
def from_hiveon_modern(cls, web_conf: dict) -> "MinerConfig":
return cls.from_am_modern(web_conf)

View File

@@ -28,6 +28,9 @@ class MinerConfigOption(Enum):
def as_am_modern(self) -> dict:
return self.value.as_am_modern()
def as_hiveon_modern(self) -> dict:
return self.value.as_hiveon_modern()
def as_am_old(self) -> dict:
return self.value.as_am_old()
@@ -95,6 +98,9 @@ class MinerConfigValue(BaseModel):
def as_am_modern(self) -> dict:
return {}
def as_hiveon_modern(self) -> dict:
return {}
def as_am_old(self) -> dict:
return {}

View File

@@ -55,6 +55,9 @@ class FanModeNormal(MinerConfigValue):
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": False, "bitmain-fan-pwn": "100"}
def as_hiveon_modern(self) -> dict:
return {"bitmain-fan-ctrl": False, "bitmain-fan-pwn": "100"}
def as_elphapex(self) -> dict:
return {"fc-fan-ctrl": False, "fc-fan-pwn": "100"}
@@ -138,6 +141,9 @@ class FanModeManual(MinerConfigValue):
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": str(self.speed)}
def as_hiveon_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": str(self.speed)}
def as_elphapex(self) -> dict:
return {"fc-fan-ctrl": True, "fc-fan-pwm": str(self.speed)}
@@ -191,6 +197,9 @@ class FanModeImmersion(MinerConfigValue):
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": "0"}
def as_hiveon_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": "0"}
def as_elphapex(self) -> dict:
return {"fc-fan-ctrl": True, "fc-fan-pwm": "0"}
@@ -248,6 +257,20 @@ class FanModeConfig(MinerConfigOption):
else:
return cls.default()
@classmethod
def from_hiveon_modern(cls, web_conf: dict):
if web_conf.get("bitmain-fan-ctrl") is not None:
fan_manual = web_conf["bitmain-fan-ctrl"]
if fan_manual:
speed = int(web_conf["bitmain-fan-pwm"])
if speed == 0:
return cls.immersion()
return cls.manual(speed=speed)
else:
return cls.normal()
else:
return cls.default()
@classmethod
def from_elphapex(cls, web_conf: dict):
if web_conf.get("fc-fan-ctrl") is not None:

View File

@@ -52,6 +52,11 @@ class MiningModeNormal(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
@@ -90,6 +95,11 @@ class MiningModeSleep(MinerConfigValue):
return {"miner-mode": "1"}
return {"miner-mode": 1}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "1"}
return {"miner-mode": 1}
def as_elphapex(self) -> dict:
return {"miner-mode": 1}
@@ -125,6 +135,11 @@ class MiningModeLPM(MinerConfigValue):
return {"miner-mode": "3"}
return {"miner-mode": 3}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "3"}
return {"miner-mode": 3}
def as_elphapex(self) -> dict:
return {"miner-mode": 3}
@@ -150,6 +165,11 @@ class MiningModeHPM(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
@@ -186,6 +206,11 @@ class MiningModePowerTune(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
@@ -288,6 +313,11 @@ class MiningModeHashrateTune(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
@@ -422,6 +452,11 @@ class ManualBoardSettings(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_hiveon_modern(self) -> dict:
if settings.get("antminer_mining_mode_as_str", False):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
@@ -549,6 +584,20 @@ class MiningModeConfig(MinerConfigOption):
return cls.low()
return cls.default()
@classmethod
def from_hiveon_modern(cls, web_conf: dict):
if web_conf.get("bitmain-work-mode") is not None:
work_mode = web_conf["bitmain-work-mode"]
if work_mode == "":
return cls.default()
if int(work_mode) == 0:
return cls.normal()
elif int(work_mode) == 1:
return cls.sleep()
elif int(work_mode) == 3:
return cls.low()
return cls.default()
@classmethod
def from_elphapex(cls, web_conf: dict):
if web_conf.get("fc-work-mode") is not None:

View File

@@ -24,7 +24,14 @@ class MiningPreset(MinerConfigValue):
hashrate = None
else:
power = hr_power_split[0].replace("watt", "").strip()
hashrate = hr_power_split[1].replace("TH", "").replace(" LC", "").strip()
hashrate = (
hr_power_split[1]
.replace("TH", "")
.replace("GH", "")
.replace("MH", "")
.replace(" LC", "")
.strip()
)
tuned = web_preset["status"] == "tuned"
modded_psu = web_preset["modded_psu_required"]
return cls(

View File

@@ -43,6 +43,13 @@ class Pool(MinerConfigValue):
"pass": self.password,
}
def as_hiveon_modern(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"pass": self.password,
}
def as_elphapex(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
@@ -153,6 +160,12 @@ class Pool(MinerConfigValue):
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_hiveon_modern(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_elphapex(cls, web_pool: dict) -> "Pool":
return cls(
@@ -248,6 +261,17 @@ class PoolGroup(MinerConfigValue):
idx += 1
return pools
def as_hiveon_modern(self, user_suffix: str | None = None) -> list:
pools = []
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.append(self.pools[idx].as_hiveon_modern(user_suffix=user_suffix))
else:
pools.append(Pool(url="", user="", password="").as_hiveon_modern())
idx += 1
return pools
def as_elphapex(self, user_suffix: str | None = None) -> list:
pools = []
idx = 0
@@ -375,6 +399,13 @@ class PoolGroup(MinerConfigValue):
pools.append(Pool.from_am_modern(pool))
return cls(pools=pools)
@classmethod
def from_hiveon_modern(cls, web_pool_list: list) -> "PoolGroup":
pools = []
for pool in web_pool_list:
pools.append(Pool.from_hiveon_modern(pool))
return cls(pools=pools)
@classmethod
def from_elphapex(cls, web_pool_list: list) -> "PoolGroup":
pools = []
@@ -467,6 +498,11 @@ class PoolConfig(MinerConfigValue):
return {"pools": self.groups[0].as_am_modern(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_am_modern()}
def as_hiveon_modern(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_hiveon_modern(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_hiveon_modern()}
def as_elphapex(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_elphapex(user_suffix=user_suffix)}
@@ -569,10 +605,22 @@ class PoolConfig(MinerConfigValue):
@classmethod
def from_am_modern(cls, web_conf: dict) -> "PoolConfig":
pool_data = web_conf["pools"]
try:
pool_data = web_conf["pools"]
except KeyError:
return cls(groups=[])
return cls(groups=[PoolGroup.from_am_modern(pool_data)])
@classmethod
def from_hiveon_modern(cls, web_conf: dict) -> "PoolConfig":
try:
pool_data = web_conf["pools"]
except KeyError:
return cls(groups=[])
return cls(groups=[PoolGroup.from_hiveon_modern(pool_data)])
@classmethod
def from_elphapex(cls, web_conf: dict) -> "PoolConfig":
pool_data = web_conf["pools"]

View File

@@ -21,4 +21,5 @@ from .epic import *
from .hiveon import *
from .luxos import *
from .marathon import *
from .mskminer import *
from .vnish import *

View File

@@ -100,5 +100,5 @@ class HiveonS19ProPlusHydro(HiveonModern, S19ProPlusHydro):
pass
class HiveonS19KPro(HiveonModern, S19KPro):
class HiveonS19kPro(HiveonModern, S19KPro):
pass

View File

@@ -23,7 +23,7 @@ from .S19 import (
HiveonS19j,
HiveonS19jNoPIC,
HiveonS19jPro,
HiveonS19KPro,
HiveonS19kPro,
HiveonS19L,
HiveonS19NoPIC,
HiveonS19Plus,

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.backends.mskminer import MSKMiner
from pyasic.miners.device.models import (
S19NoPIC,
)
class MSKMinerS19NoPIC(MSKMiner, S19NoPIC):
pass

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# 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 .S19 import MSKMinerS19NoPIC

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# 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 .X19 import *

View File

@@ -32,6 +32,7 @@ from .innosilicon import Innosilicon
from .luckyminer import LuckyMiner
from .luxminer import LUXMiner
from .marathon import MaraMiner
from .mskminer import MSKMiner
from .unknown import UnknownMiner
from .vnish import VNish
from .whatsminer import M2X, M3X, M5X, M6X, M7X

View File

@@ -0,0 +1,110 @@
from typing import Optional
from pyasic import APIError
from pyasic.device.algorithm import AlgoHashRate
from pyasic.miners.backends import BMMiner
from pyasic.miners.data import (
DataFunction,
DataLocations,
DataOptions,
RPCAPICommand,
WebAPICommand,
)
from pyasic.web.mskminer import MSKMinerWebAPI
MSKMINER_DATA_LOC = DataLocations(
**{
str(DataOptions.API_VERSION): DataFunction(
"_get_api_ver",
[RPCAPICommand("rpc_version", "version")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[RPCAPICommand("rpc_version", "version")],
),
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_info_v1", "info_v1")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
),
}
)
class MSKMiner(BMMiner):
"""Handler for MSKMiner"""
data_locations = MSKMINER_DATA_LOC
web: MSKMinerWebAPI
_web_cls = MSKMinerWebAPI
async def _get_hashrate(self, rpc_stats: dict = None) -> Optional[AlgoHashRate]:
# get hr from API
if rpc_stats is None:
try:
rpc_stats = await self.rpc.stats()
except APIError:
pass
if rpc_stats is not None:
try:
return self.algo.hashrate(
rate=float(rpc_stats["STATS"][0]["total_rate"]),
unit=self.algo.unit.GH,
).into(self.algo.unit.default)
except (LookupError, ValueError, TypeError):
pass
async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]:
if rpc_stats is None:
try:
rpc_stats = await self.rpc.stats()
except APIError:
pass
if rpc_stats is not None:
try:
return rpc_stats["STATS"][0]["total_power"]
except (LookupError, ValueError, TypeError):
pass
async def _get_mac(self, web_info_v1: dict = None) -> Optional[str]:
if web_info_v1 is None:
try:
web_info_v1 = await self.web.info_v1()
except APIError:
pass
if web_info_v1 is not None:
try:
return web_info_v1["network_info"]["result"]["macaddr"].upper()
except (LookupError, ValueError, TypeError):
pass

View File

@@ -40,6 +40,6 @@ class S21Hydro(AntMinerMake):
raw_model = MinerModel.ANTMINER.S21Hydro
expected_chips = 216
expected_hashboards = 2
expected_hashboards = 3
expected_fans = 0
algo = MinerAlgo.SHA256

View File

@@ -22,6 +22,6 @@ class Avalon1566(AvalonMinerMake):
raw_model = MinerModel.AVALONMINER.Avalon1566
expected_chips = 160
expected_fans = 4
expected_fans = 2
expected_hashboards = 3
algo = MinerAlgo.SHA256

View File

@@ -66,6 +66,7 @@ class MinerTypes(enum.Enum):
VOLCMINER = 15
LUCKYMINER = 16
ELPHAPEX = 17
MSKMINER = 18
MINER_CLASSES = {
@@ -599,8 +600,13 @@ MINER_CLASSES = {
"ANTMINER T9": HiveonT9,
"ANTMINER S19JPRO": HiveonS19jPro,
"ANTMINER S19": HiveonS19,
"ANTMINER S19K PRO": HiveonS19kPro,
"ANTMINER S19X88": HiveonS19NoPIC,
},
MinerTypes.MSKMINER: {
None: MSKMiner,
"S19-88": MSKMinerS19NoPIC,
},
MinerTypes.LUX_OS: {
None: LUXMiner,
"ANTMINER S9": LUXMinerS9,
@@ -870,6 +876,8 @@ class MinerFactory:
return MinerTypes.INNOSILICON
if "Miner UI" in web_text:
return MinerTypes.AURADINE
if "<title>Antminer</title>" in web_text:
return MinerTypes.MSKMINER
async def _get_miner_socket(self, ip: str) -> MinerTypes | None:
commands = ["version", "devdetails"]
@@ -946,6 +954,8 @@ class MinerFactory:
return MinerTypes.HIVEON
if "KAONSU" in upper_data:
return MinerTypes.MARATHON
if "RWGLR" in upper_data:
return MinerTypes.MSKMINER
if "ANTMINER" in upper_data and "DEVDETAILS" not in upper_data:
return MinerTypes.ANTMINER
if (
@@ -1411,6 +1421,13 @@ class MinerFactory:
except (TypeError, LookupError):
pass
async def get_miner_model_mskminer(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "version")
try:
return sock_json_data["VERSION"][0]["Type"].split(" ")[0]
except LookupError:
pass
miner_factory = MinerFactory()

View File

@@ -41,6 +41,7 @@ _settings = { # defaults
"default_hive_web_password": "root",
"default_iceriver_web_password": "12345678",
"default_elphapex_web_password": "root",
"default_mskminer_web_password": "root",
"default_antminer_ssh_password": "miner",
"default_bosminer_ssh_password": "root",
}

72
pyasic/web/mskminer.py Normal file
View File

@@ -0,0 +1,72 @@
# ------------------------------------------------------------------------------
# Copyright 2024 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 __future__ import annotations
import asyncio
import warnings
from typing import Any
import httpx
from pyasic import settings
from pyasic.errors import APIError
from pyasic.web.base import BaseWebAPI
class MSKMinerWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.username = "admin"
self.pwd = settings.get("default_mskminer_web_password", "root")
async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict:
tasks = {c: asyncio.create_task(getattr(self, c)()) for c in commands}
await asyncio.gather(*[t for t in tasks.values()])
return {t: tasks[t].result() for t in tasks}
async def send_command(
self,
command: str | bytes,
ignore_errors: bool = False,
allow_warning: bool = True,
privileged: bool = False,
**parameters: Any,
) -> dict:
async with httpx.AsyncClient(transport=settings.transport()) as client:
try:
# auth
await client.post(
f"http://{self.ip}:{self.port}/admin/login",
data={"username": self.username, "password": self.pwd},
)
except httpx.HTTPError:
warnings.warn(f"Could not authenticate with miner web: {self}")
try:
resp = await client.post(
f"http://{self.ip}:{self.port}/api/{command}", params=parameters
)
if not resp.status_code == 200:
if not ignore_errors:
raise APIError(f"Command failed: {command}")
warnings.warn(f"Command failed: {command}")
return resp.json()
except httpx.HTTPError:
raise APIError(f"Command failed: {command}")
async def info_v1(self):
return await self.send_command("info_v1")

View File

@@ -1,6 +1,6 @@
[project]
name = "pyasic"
version = "0.71.7"
version = "0.71.12"
description = "A simplified and standardized interface for Bitcoin ASICs."
authors = [{name = "UpstreamData", email = "brett@upstreamdata.ca"}]

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------
from tests.config_tests import TestConfig
from tests.miners_tests import MinersTest, TestElphapexMiners, TestHammerMiners
from tests.miners_tests import *
from tests.network_tests import NetworkTest
from tests.rpc_tests import *

View File

@@ -1,2 +1,3 @@
from .avalonminer_tests import *
from .elphapex_tests import *
from .hammer_tests import *

View File

@@ -0,0 +1 @@
from .version_24102401_25462b2_9ddf522 import TestAvalonMiners

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
from .version_2_6_0_39 import TestMSKMiners

View File

@@ -0,0 +1,500 @@
"""Tests for MSK miner firmware version 2.6.0.39"""
import unittest
from dataclasses import fields
from unittest.mock import patch
from pyasic import APIError, MinerData
from pyasic.data import Fan, HashBoard
from pyasic.device.algorithm import SHA256Unit
from pyasic.miners.antminer import MSKMinerS19NoPIC
POOLS = [
{
"url": "stratum+tcp://stratum.pool.io:3333",
"user": "pool_username.real_worker",
"pwd": "123",
},
{
"url": "stratum+tcp://stratum.pool.io:3334",
"user": "pool_username.real_worker",
"pwd": "123",
},
{
"url": "stratum+tcp://stratum.pool.io:3335",
"user": "pool_username.real_worker",
"pwd": "123",
},
]
data = {
MSKMinerS19NoPIC: {
"web_info_v1": {
# needs updates with real data
"network_info": {
"result": {
"address": "192.168.1.10",
"macaddr": "12:34:56:78:90:12",
"netmask": "255.255.255.0",
}
}
},
"rpc_version": {
"STATUS": [
{
"STATUS": "S",
"When": 1738856891,
"Code": 22,
"Msg": "BMMiner versions",
"Description": "bmminer 1.0.0",
}
],
"VERSION": [
{
"BMMiner": "4.11.1 rwglr",
"API": "3.1",
"Miner": "0.0.1.3",
"CompileTime": "10 Dec 2024 14:34:31 GMT",
"Type": "S19-88 v.2.6.0.39 ",
}
],
"id": 1,
},
"rpc_stats": {
"STATUS": [
{
"STATUS": "S",
"When": 1738856891,
"Code": 70,
"Msg": "BMMiner stats",
"Description": "bmminer 1.0.0",
}
],
"STATS": [
{
"BMMiner": "4.11.1 rwglr",
"Miner": "0.0.1.3",
"CompileTime": "10 Dec 2024 14:34:31 GMT",
"Type": "S19-88 v.2.6.0.39 ",
},
{
"STATS": 0,
"ID": "BC50",
"Elapsed": 1926,
"Calls": 0,
"Wait": 0.000000,
"Max": 0.000000,
"Min": 99999999.000000,
"GHS 5s": 99989.59,
"GHS av": 99761.40,
"miner_count": 3,
"frequency": "",
"fan_num": 4,
"fan1": 5010,
"fan2": 5160,
"fan3": 5070,
"fan4": 5040,
"fan5": 0,
"fan6": 0,
"fan7": 0,
"fan8": 0,
"temp_num": 3,
"temp1": 45,
"temp2": 45,
"temp3": 47,
"temp4": 0,
"temp5": 0,
"temp6": 0,
"temp7": 0,
"temp8": 0,
"temp9": 0,
"temp10": 0,
"temp11": 0,
"temp12": 0,
"temp13": 0,
"temp14": 0,
"temp15": 0,
"temp16": 0,
"temp2_1": 59,
"temp2_2": 57,
"temp2_3": 58,
"temp2_4": 0,
"temp2_5": 0,
"temp2_6": 0,
"temp2_7": 0,
"temp2_8": 0,
"temp2_9": 0,
"temp2_10": 0,
"temp2_11": 0,
"temp2_12": 0,
"temp2_13": 0,
"temp2_14": 0,
"temp2_15": 0,
"temp2_16": 0,
"temp3_1": 59,
"temp3_2": 56,
"temp3_3": 57,
"temp3_4": 0,
"temp3_5": 0,
"temp3_6": 0,
"temp3_7": 0,
"temp3_8": 0,
"temp3_9": 0,
"temp3_10": 0,
"temp3_11": 0,
"temp3_12": 0,
"temp3_13": 0,
"temp3_14": 0,
"temp3_15": 0,
"temp3_16": 0,
"temp_pcb1": "45-42-45-42",
"temp_pcb2": "45-42-45-42",
"temp_pcb3": "47-43-47-43",
"temp_pcb4": "0-0-0-0",
"temp_pcb5": "0-0-0-0",
"temp_pcb6": "0-0-0-0",
"temp_pcb7": "0-0-0-0",
"temp_pcb8": "0-0-0-0",
"temp_pcb9": "0-0-0-0",
"temp_pcb10": "0-0-0-0",
"temp_pcb11": "0-0-0-0",
"temp_pcb12": "0-0-0-0",
"temp_pcb13": "0-0-0-0",
"temp_pcb14": "0-0-0-0",
"temp_pcb15": "0-0-0-0",
"temp_pcb16": "0-0-0-0",
"temp_chip1": "59-59-59-59",
"temp_chip2": "57-56-57-56",
"temp_chip3": "58-57-58-57",
"temp_chip4": "0-0-0-0",
"temp_chip5": "0-0-0-0",
"temp_chip6": "0-0-0-0",
"temp_chip7": "0-0-0-0",
"temp_chip8": "0-0-0-0",
"temp_chip9": "0-0-0-0",
"temp_chip10": "0-0-0-0",
"temp_chip11": "0-0-0-0",
"temp_chip12": "0-0-0-0",
"temp_chip13": "0-0-0-0",
"temp_chip14": "0-0-0-0",
"temp_chip15": "0-0-0-0",
"temp_chip16": "0-0-0-0",
"total_rateideal": 99674.88,
"total_freqavg": 0.00,
"total_acn": 264,
"total_rate": 99989.59,
"chain_rateideal1": 33677.28,
"chain_rateideal2": 32788.06,
"chain_rateideal3": 33209.54,
"chain_rateideal4": 31436.24,
"chain_rateideal5": 31436.24,
"chain_rateideal6": 31436.24,
"chain_rateideal7": 31436.24,
"chain_rateideal8": 31436.24,
"chain_rateideal9": 31436.24,
"chain_rateideal10": 31436.24,
"chain_rateideal11": 31436.24,
"chain_rateideal12": 31436.24,
"chain_rateideal13": 31436.24,
"chain_rateideal14": 31436.24,
"chain_rateideal15": 31436.24,
"chain_rateideal16": 31436.24,
"temp_max": 47,
"no_matching_work": 0,
"chain_acn1": 88,
"chain_acn2": 88,
"chain_acn3": 88,
"chain_acn4": 0,
"chain_acn5": 0,
"chain_acn6": 0,
"chain_acn7": 0,
"chain_acn8": 0,
"chain_acn9": 0,
"chain_acn10": 0,
"chain_acn11": 0,
"chain_acn12": 0,
"chain_acn13": 0,
"chain_acn14": 0,
"chain_acn15": 0,
"chain_acn16": 0,
"chain_acs1": " oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo",
"chain_acs2": " oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo",
"chain_acs3": " oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo oo",
"chain_acs4": "",
"chain_acs5": "",
"chain_acs6": "",
"chain_acs7": "",
"chain_acs8": "",
"chain_acs9": "",
"chain_acs10": "",
"chain_acs11": "",
"chain_acs12": "",
"chain_acs13": "",
"chain_acs14": "",
"chain_acs15": "",
"chain_acs16": "",
"chain_hw1": 0,
"chain_hw2": 0,
"chain_hw3": 0,
"chain_hw4": 0,
"chain_hw5": 0,
"chain_hw6": 0,
"chain_hw7": 0,
"chain_hw8": 0,
"chain_hw9": 0,
"chain_hw10": 0,
"chain_hw11": 0,
"chain_hw12": 0,
"chain_hw13": 0,
"chain_hw14": 0,
"chain_hw15": 0,
"chain_hw16": 0,
"chain_rate1": 34084.86,
"chain_rate2": 32303.65,
"chain_rate3": 33601.08,
"chain_rate4": 0.00,
"chain_rate5": 0.00,
"chain_rate6": 0.00,
"chain_rate7": 0.00,
"chain_rate8": 0.00,
"chain_rate9": 0.00,
"chain_rate10": 0.00,
"chain_rate11": 0.00,
"chain_rate12": 0.00,
"chain_rate13": 0.00,
"chain_rate14": 0.00,
"chain_rate15": 0.00,
"chain_rate16": 0.00,
"chain_xtime1": "{}",
"chain_xtime2": "{}",
"chain_xtime3": "{}",
"chain_offside_1": "",
"chain_offside_2": "",
"chain_offside_3": "",
"chain_opencore_0": "1",
"chain_opencore_1": "1",
"chain_opencore_2": "1",
"freq1": 744,
"freq2": 724,
"freq3": 734,
"freq4": 0,
"freq5": 0,
"freq6": 0,
"freq7": 0,
"freq8": 0,
"freq9": 0,
"freq10": 0,
"freq11": 0,
"freq12": 0,
"freq13": 0,
"freq14": 0,
"freq15": 0,
"freq16": 0,
"chain_avgrate1": 33585.34,
"chain_avgrate2": 32788.97,
"chain_avgrate3": 33336.44,
"chain_avgrate4": 0.00,
"chain_avgrate5": 0.00,
"chain_avgrate6": 0.00,
"chain_avgrate7": 0.00,
"chain_avgrate8": 0.00,
"chain_avgrate9": 0.00,
"chain_avgrate10": 0.00,
"chain_avgrate11": 0.00,
"chain_avgrate12": 0.00,
"chain_avgrate13": 0.00,
"chain_avgrate14": 0.00,
"chain_avgrate15": 0.00,
"chain_avgrate16": 0.00,
"miner_version": "0.0.1.3",
"miner_id": "",
"chain_power1": 1135,
"chain_power2": 1103,
"chain_power3": 1118,
"total_power": 3358,
"chain_voltage1": 15.70,
"chain_voltage2": 15.70,
"chain_voltage3": 15.70,
"chain_voltage4": 15.70,
"chain_voltage5": 15.70,
"chain_voltage6": 15.70,
"chain_voltage7": 15.70,
"chain_voltage8": 15.70,
"chain_voltage9": 15.70,
"chain_voltage10": 15.70,
"chain_voltage11": 15.70,
"chain_voltage12": 15.70,
"chain_voltage13": 15.70,
"chain_voltage14": 15.70,
"chain_voltage15": 15.70,
"chain_voltage16": 15.70,
"fan_pwm": 82,
"bringup_temp": 16,
"has_pic": "1",
"tune_running": "0",
"psu_status": "PSU OK",
"downscale_mode": "0",
"has_hotel_fee": "0",
},
],
"id": 1,
},
"rpc_pools": {
"STATUS": [
{
"Code": 7,
"Description": "cgminer 1.0.0",
"Msg": "3 Pool(s)",
"STATUS": "S",
"When": 1732121693,
}
],
"POOLS": [
{
"Accepted": 10000,
"Best Share": 1000000000.0,
"Diff": "100K",
"Diff1 Shares": 0,
"Difficulty Accepted": 1000000000.0,
"Difficulty Rejected": 1000000.0,
"Difficulty Stale": 0.0,
"Discarded": 100000,
"Get Failures": 3,
"Getworks": 9000,
"Has GBT": False,
"Has Stratum": True,
"Last Share Difficulty": 100000.0,
"Last Share Time": "0:00:02",
"Long Poll": "N",
"POOL": 0,
"Pool Rejected%": 0.0,
"Pool Stale%%": 0.0,
"Priority": 0,
"Proxy": "",
"Proxy Type": "",
"Quota": 1,
"Rejected": 100,
"Remote Failures": 0,
"Stale": 0,
"Status": "Alive",
"Stratum Active": True,
"Stratum URL": "stratum.pool.io",
"URL": "stratum+tcp://stratum.pool.io:3333",
"User": "pool_username.real_worker",
},
{
"Accepted": 10000,
"Best Share": 1000000000.0,
"Diff": "100K",
"Diff1 Shares": 0,
"Difficulty Accepted": 1000000000.0,
"Difficulty Rejected": 1000000.0,
"Difficulty Stale": 0.0,
"Discarded": 100000,
"Get Failures": 3,
"Getworks": 9000,
"Has GBT": False,
"Has Stratum": True,
"Last Share Difficulty": 100000.0,
"Last Share Time": "0:00:02",
"Long Poll": "N",
"POOL": 1,
"Pool Rejected%": 0.0,
"Pool Stale%%": 0.0,
"Priority": 0,
"Proxy": "",
"Proxy Type": "",
"Quota": 1,
"Rejected": 100,
"Remote Failures": 0,
"Stale": 0,
"Status": "Alive",
"Stratum Active": True,
"Stratum URL": "stratum.pool.io",
"URL": "stratum+tcp://stratum.pool.io:3333",
"User": "pool_username.real_worker",
},
{
"Accepted": 10000,
"Best Share": 1000000000.0,
"Diff": "100K",
"Diff1 Shares": 0,
"Difficulty Accepted": 1000000000.0,
"Difficulty Rejected": 1000000.0,
"Difficulty Stale": 0.0,
"Discarded": 100000,
"Get Failures": 3,
"Getworks": 9000,
"Has GBT": False,
"Has Stratum": True,
"Last Share Difficulty": 100000.0,
"Last Share Time": "0:00:02",
"Long Poll": "N",
"POOL": 2,
"Pool Rejected%": 0.0,
"Pool Stale%%": 0.0,
"Priority": 0,
"Proxy": "",
"Proxy Type": "",
"Quota": 1,
"Rejected": 100,
"Remote Failures": 0,
"Stale": 0,
"Status": "Alive",
"Stratum Active": True,
"Stratum URL": "stratum.pool.io",
"URL": "stratum+tcp://stratum.pool.io:3333",
"User": "pool_username.real_worker",
},
],
"id": 1,
},
}
}
class TestMSKMiners(unittest.IsolatedAsyncioTestCase):
@patch("pyasic.rpc.base.BaseMinerRPCAPI._send_bytes")
async def test_all_data_gathering(self, mock_send_bytes):
mock_send_bytes.raises = APIError()
for m_type in data:
gathered_data = {}
miner = m_type("127.0.0.1")
for data_name in fields(miner.data_locations):
if data_name.name == "config":
# skip
continue
data_func = getattr(miner.data_locations, data_name.name)
fn_args = data_func.kwargs
args_to_send = {k.name: data[m_type][k.name] for k in fn_args}
function = getattr(miner, data_func.cmd)
gathered_data[data_name.name] = await function(**args_to_send)
result = MinerData(
ip=str(miner.ip),
device_info=miner.device_info,
expected_chips=(
miner.expected_chips * miner.expected_hashboards
if miner.expected_chips is not None
else 0
),
expected_hashboards=miner.expected_hashboards,
expected_fans=miner.expected_fans,
hashboards=[
HashBoard(slot=i, expected_chips=miner.expected_chips)
for i in range(miner.expected_hashboards)
],
)
for item in gathered_data:
if gathered_data[item] is not None:
setattr(result, item, gathered_data[item])
self.assertEqual(result.mac, "12:34:56:78:90:12")
self.assertEqual(result.api_ver, "3.1")
self.assertEqual(result.fw_ver, "10 Dec 2024 14:34:31 GMT")
self.assertEqual(round(result.hashrate.into(SHA256Unit.TH)), 100)
self.assertEqual(
result.fans,
[Fan(speed=5010), Fan(speed=5160), Fan(speed=5070), Fan(speed=5040)],
)
self.assertEqual(result.total_chips, result.expected_chips)