diff --git a/docs/generate_miners.py b/docs/generate_miners.py
index 87f23886..d7c67587 100644
--- a/docs/generate_miners.py
+++ b/docs/generate_miners.py
@@ -51,6 +51,8 @@ def backend_str(backend: MinerTypes) -> str:
return "Mara Firmware Miners"
case MinerTypes.BITAXE:
return "Stock Firmware BitAxe Miners"
+ case MinerTypes.ICERIVER:
+ return "Stock Firmware IceRiver Miners"
def create_url_str(mtype: str):
diff --git a/docs/miners/antminer/X19.md b/docs/miners/antminer/X19.md
index f01ba1ba..80a8af38 100644
--- a/docs/miners/antminer/X19.md
+++ b/docs/miners/antminer/X19.md
@@ -225,6 +225,13 @@
show_root_heading: false
heading_level: 4
+## S19 Pro+ Hydro (BOS+)
+::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19ProPlusHydro
+ handler: python
+ options:
+ show_root_heading: false
+ heading_level: 4
+
## T19 (BOS+)
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
handler: python
@@ -281,6 +288,13 @@
show_root_heading: false
heading_level: 4
+## S19 Pro Hydro (VNish)
+::: pyasic.miners.antminer.vnish.X19.S19.VNishS19ProHydro
+ handler: python
+ options:
+ show_root_heading: false
+ heading_level: 4
+
## T19 (VNish)
::: pyasic.miners.antminer.vnish.X19.T19.VNishT19
handler: python
diff --git a/docs/miners/antminer/X21.md b/docs/miners/antminer/X21.md
index fc7539d8..6f38803c 100644
--- a/docs/miners/antminer/X21.md
+++ b/docs/miners/antminer/X21.md
@@ -8,6 +8,13 @@
show_root_heading: false
heading_level: 4
+## S21 Pro (Stock)
+::: pyasic.miners.antminer.bmminer.X21.S21.BMMinerS21Pro
+ handler: python
+ options:
+ show_root_heading: false
+ heading_level: 4
+
## T21 (Stock)
::: pyasic.miners.antminer.bmminer.X21.T21.BMMinerT21
handler: python
@@ -36,6 +43,13 @@
show_root_heading: false
heading_level: 4
+## S21 Pro (ePIC)
+::: pyasic.miners.antminer.epic.X21.S21.ePICS21Pro
+ handler: python
+ options:
+ show_root_heading: false
+ heading_level: 4
+
## T21 (ePIC)
::: pyasic.miners.antminer.epic.X21.T21.ePICT21
handler: python
diff --git a/docs/miners/iceriver/KSX.md b/docs/miners/iceriver/KSX.md
new file mode 100644
index 00000000..3218ff87
--- /dev/null
+++ b/docs/miners/iceriver/KSX.md
@@ -0,0 +1,10 @@
+# pyasic
+## KSX Models
+
+## KS2 (Stock)
+::: pyasic.miners.iceriver.iceminer.KSX.KS2.IceRiverKS2
+ handler: python
+ options:
+ show_root_heading: false
+ heading_level: 4
+
diff --git a/docs/miners/supported_types.md b/docs/miners/supported_types.md
index 27cd9120..cec1f5b3 100644
--- a/docs/miners/supported_types.md
+++ b/docs/miners/supported_types.md
@@ -89,6 +89,7 @@ details {
X21 Series:
@@ -461,6 +462,7 @@ details {
S19k Pro No PIC (BOS+)
S19k Pro No PIC (BOS+)
S19 XP (BOS+)
+ S19 Pro+ Hydro (BOS+)
T19 (BOS+)
@@ -505,6 +507,7 @@ details {
S19j Pro (VNish)
S19a (VNish)
S19a Pro (VNish)
+ S19 Pro Hydro (VNish)
T19 (VNish)
@@ -535,6 +538,7 @@ details {
X21 Series:
@@ -650,4 +654,15 @@ details {
+
+
+Stock Firmware IceRiver Miners:
+
\ No newline at end of file
diff --git a/pyasic/device/models.py b/pyasic/device/models.py
index 24678d72..81a53427 100644
--- a/pyasic/device/models.py
+++ b/pyasic/device/models.py
@@ -339,6 +339,13 @@ class BitAxeModels(str, Enum):
return self.value
+class IceRiverModels(str, Enum):
+ KS2 = "KS2"
+
+ def __str__(self):
+ return self.value
+
+
class MinerModel:
ANTMINER = AntminerModels
WHATSMINER = WhatsminerModels
@@ -348,3 +355,4 @@ class MinerModel:
AURADINE = AuradineModels
EPIC = ePICModels
BITAXE = BitAxeModels
+ ICERIVER = IceRiverModels
diff --git a/pyasic/miners/backends/__init__.py b/pyasic/miners/backends/__init__.py
index 283a598c..a510d001 100644
--- a/pyasic/miners/backends/__init__.py
+++ b/pyasic/miners/backends/__init__.py
@@ -24,6 +24,7 @@ from .cgminer import CGMiner
from .epic import ePIC
from .goldshell import GoldshellMiner
from .hiveon import Hiveon
+from .iceriver import IceRiver
from .innosilicon import Innosilicon
from .luxminer import LUXMiner
from .marathon import MaraMiner
diff --git a/pyasic/miners/backends/iceriver.py b/pyasic/miners/backends/iceriver.py
new file mode 100644
index 00000000..feb6c756
--- /dev/null
+++ b/pyasic/miners/backends/iceriver.py
@@ -0,0 +1,198 @@
+from typing import List, Optional
+
+from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
+from pyasic.device import MinerAlgo
+from pyasic.errors import APIError
+from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
+from pyasic.miners.device.firmware import StockFirmware
+from pyasic.web.iceriver import IceRiverWebAPI
+
+ICERIVER_DATA_LOC = DataLocations(
+ **{
+ str(DataOptions.MAC): DataFunction(
+ "_get_mac",
+ [WebAPICommand("web_userpanel", "userpanel")],
+ ),
+ str(DataOptions.FANS): DataFunction(
+ "_get_fans",
+ [WebAPICommand("web_userpanel", "userpanel")],
+ ),
+ str(DataOptions.HOSTNAME): DataFunction(
+ "_get_hostname",
+ [WebAPICommand("web_userpanel", "userpanel")],
+ ),
+ str(DataOptions.HASHRATE): DataFunction(
+ "_get_hashrate",
+ [WebAPICommand("web_userpanel", "userpanel")],
+ ),
+ str(DataOptions.IS_MINING): DataFunction(
+ "_is_mining",
+ [WebAPICommand("web_userpanel", "userpanel")],
+ ),
+ str(DataOptions.FAULT_LIGHT): DataFunction(
+ "_get_fault_light",
+ [WebAPICommand("web_userpanel", "userpanel")],
+ ),
+ str(DataOptions.HASHBOARDS): DataFunction(
+ "_get_hashboards",
+ [WebAPICommand("web_userpanel", "userpanel")],
+ ),
+ str(DataOptions.UPTIME): DataFunction(
+ "_get_uptime",
+ [WebAPICommand("web_userpanel", "userpanel")],
+ ),
+ }
+)
+
+
+class IceRiver(StockFirmware):
+ """Handler for IceRiver miners"""
+
+ _web_cls = IceRiverWebAPI
+ web: IceRiverWebAPI
+
+ data_locations = ICERIVER_DATA_LOC
+
+ async def fault_light_off(self) -> bool:
+ try:
+ await self.web.locate(False)
+ except APIError:
+ return False
+ return True
+
+ async def fault_light_on(self) -> bool:
+ try:
+ await self.web.locate(True)
+ except APIError:
+ return False
+ return True
+
+ async def _get_fans(self, web_userpanel: dict = None) -> List[Fan]:
+ if web_userpanel is None:
+ try:
+ web_userpanel = await self.web.userpanel()
+ except APIError:
+ pass
+
+ if web_userpanel is not None:
+ try:
+ return [Fan(spd) for spd in web_userpanel["fans"]]
+ except (LookupError, ValueError, TypeError):
+ pass
+
+ async def _get_mac(self, web_userpanel: dict = None) -> Optional[str]:
+ if web_userpanel is None:
+ try:
+ web_userpanel = await self.web.userpanel()
+ except APIError:
+ pass
+
+ if web_userpanel is not None:
+ try:
+ return web_userpanel["mac"].upper().replace("-", ":")
+ except (LookupError, ValueError, TypeError):
+ pass
+
+ async def _get_hostname(self, web_userpanel: dict = None) -> Optional[str]:
+ if web_userpanel is None:
+ try:
+ web_userpanel = await self.web.userpanel()
+ except APIError:
+ pass
+
+ if web_userpanel is not None:
+ try:
+ return web_userpanel["host"]
+ except (LookupError, ValueError, TypeError):
+ pass
+
+ async def _get_hashrate(self, web_userpanel: dict = None) -> Optional[AlgoHashRate]:
+ if web_userpanel is None:
+ try:
+ web_userpanel = await self.web.userpanel()
+ except APIError:
+ pass
+
+ if web_userpanel is not None:
+ try:
+ base_unit = web_userpanel["unit"]
+ return AlgoHashRate.SHA256(
+ float(web_userpanel["rtpow"].replace(base_unit, "")),
+ unit=MinerAlgo.SHA256.unit.from_str(base_unit + "H"),
+ ).into(MinerAlgo.SHA256.unit.default)
+ except (LookupError, ValueError, TypeError):
+ pass
+
+ async def _get_fault_light(self, web_userpanel: dict = None) -> bool:
+ if web_userpanel is None:
+ try:
+ web_userpanel = await self.web.userpanel()
+ except APIError:
+ pass
+
+ if web_userpanel is not None:
+ try:
+ return web_userpanel["locate"]
+ except (LookupError, ValueError, TypeError):
+ pass
+ return False
+
+ async def _is_mining(self, web_userpanel: dict = None) -> Optional[bool]:
+ if web_userpanel is None:
+ try:
+ web_userpanel = await self.web.userpanel()
+ except APIError:
+ pass
+
+ if web_userpanel is not None:
+ try:
+ return web_userpanel["powstate"]
+ except (LookupError, ValueError, TypeError):
+ pass
+
+ async def _get_hashboards(self, web_userpanel: dict = None) -> List[HashBoard]:
+ if web_userpanel is None:
+ try:
+ web_userpanel = await self.web.userpanel()
+ except APIError:
+ pass
+
+ hb_list = [
+ HashBoard(slot=i, expected_chips=self.expected_chips)
+ for i in range(self.expected_hashboards)
+ ]
+
+ if web_userpanel is not None:
+ try:
+ for board in web_userpanel["boards"]:
+ idx = board["no"] - 1
+ hb_list[idx].chip_temp = round(board["outtmp"])
+ hb_list[idx].temp = round(board["intmp"])
+ hb_list[idx].hashrate = AlgoHashRate.SHA256(
+ float(board["rtpow"].replace("G", "")), HashUnit.SHA256.GH
+ ).into(self.algo.unit.default)
+ hb_list[idx].chips = board["chipnum"]
+ hb_list[idx].missing = False
+ except LookupError:
+ pass
+ return hb_list
+
+ async def _get_uptime(self, web_userpanel: dict = None) -> Optional[int]:
+ if web_userpanel is None:
+ try:
+ web_userpanel = await self.web.userpanel()
+ except APIError:
+ pass
+
+ if web_userpanel is not None:
+ try:
+ runtime = web_userpanel["runtime"]
+ days, hours, minutes, seconds = runtime.split(":")
+ return (
+ (int(days) * 24 * 60 * 60)
+ + (int(hours) * 60 * 60)
+ + (int(minutes) * 60)
+ + int(seconds)
+ )
+ except (LookupError, ValueError, TypeError):
+ pass
diff --git a/pyasic/miners/device/makes.py b/pyasic/miners/device/makes.py
index 6bfbe40b..615b98e3 100644
--- a/pyasic/miners/device/makes.py
+++ b/pyasic/miners/device/makes.py
@@ -48,3 +48,7 @@ class ePICMake(BaseMiner):
class BitAxeMake(BaseMiner):
make = MinerMake.BITAXE
+
+
+class IceRiverMake(BaseMiner):
+ make = MinerMake.BITAXE
diff --git a/pyasic/miners/device/models/__init__.py b/pyasic/miners/device/models/__init__.py
index a74bf0a9..666a9c8f 100644
--- a/pyasic/miners/device/models/__init__.py
+++ b/pyasic/miners/device/models/__init__.py
@@ -19,5 +19,6 @@ from .auradine import *
from .avalonminer import *
from .epic import *
from .goldshell import *
+from .iceriver import *
from .innosilicon import *
from .whatsminer import *
diff --git a/pyasic/miners/device/models/iceriver/KSX/KS2.py b/pyasic/miners/device/models/iceriver/KSX/KS2.py
new file mode 100644
index 00000000..888067e6
--- /dev/null
+++ b/pyasic/miners/device/models/iceriver/KSX/KS2.py
@@ -0,0 +1,23 @@
+# ------------------------------------------------------------------------------
+# 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 pyasic.device.models import MinerModel
+from pyasic.miners.device.makes import IceRiverMake
+
+
+class KS2(IceRiverMake):
+ raw_model = MinerModel.ICERIVER.KS2
+
+ expected_fans = 4
diff --git a/pyasic/miners/device/models/iceriver/KSX/__init__.py b/pyasic/miners/device/models/iceriver/KSX/__init__.py
new file mode 100644
index 00000000..2f2a4251
--- /dev/null
+++ b/pyasic/miners/device/models/iceriver/KSX/__init__.py
@@ -0,0 +1 @@
+from .KS2 import KS2
diff --git a/pyasic/miners/device/models/iceriver/__init__.py b/pyasic/miners/device/models/iceriver/__init__.py
new file mode 100644
index 00000000..54c0f2b8
--- /dev/null
+++ b/pyasic/miners/device/models/iceriver/__init__.py
@@ -0,0 +1 @@
+from .KSX import *
diff --git a/pyasic/miners/factory.py b/pyasic/miners/factory.py
index bf5d25b2..2795d2d7 100644
--- a/pyasic/miners/factory.py
+++ b/pyasic/miners/factory.py
@@ -38,6 +38,7 @@ from pyasic.miners.bitaxe import *
from pyasic.miners.blockminer import *
from pyasic.miners.device.makes import *
from pyasic.miners.goldshell import *
+from pyasic.miners.iceriver import *
from pyasic.miners.innosilicon import *
from pyasic.miners.whatsminer import *
@@ -56,6 +57,7 @@ class MinerTypes(enum.Enum):
AURADINE = 10
MARATHON = 11
BITAXE = 12
+ ICERIVER = 13
MINER_CLASSES = {
@@ -451,6 +453,10 @@ MINER_CLASSES = {
"BM1366": BitAxeUltra,
"BM1397": BitAxeMax,
},
+ MinerTypes.ICERIVER: {
+ None: type("IceRiverUnknown", (IceRiver, IceRiverMake), {}),
+ "KS2": IceRiverKS2,
+ },
}
@@ -623,6 +629,8 @@ class MinerFactory:
return MinerTypes.INNOSILICON
if "Miner UI" in web_text:
return MinerTypes.AURADINE
+ if "用户界面" in web_text:
+ return MinerTypes.ICERIVER
async def _get_miner_socket(self, ip: str) -> MinerTypes | None:
commands = ["version", "devdetails"]
diff --git a/pyasic/miners/iceriver/__init__.py b/pyasic/miners/iceriver/__init__.py
new file mode 100644
index 00000000..91378053
--- /dev/null
+++ b/pyasic/miners/iceriver/__init__.py
@@ -0,0 +1 @@
+from .iceminer import *
diff --git a/pyasic/miners/iceriver/iceminer/KSX/KS2.py b/pyasic/miners/iceriver/iceminer/KSX/KS2.py
new file mode 100644
index 00000000..629f8095
--- /dev/null
+++ b/pyasic/miners/iceriver/iceminer/KSX/KS2.py
@@ -0,0 +1,6 @@
+from pyasic.miners.backends.iceriver import IceRiver
+from pyasic.miners.device.models import KS2
+
+
+class IceRiverKS2(IceRiver, KS2):
+ pass
diff --git a/pyasic/miners/iceriver/iceminer/KSX/__init__.py b/pyasic/miners/iceriver/iceminer/KSX/__init__.py
new file mode 100644
index 00000000..4d6be18f
--- /dev/null
+++ b/pyasic/miners/iceriver/iceminer/KSX/__init__.py
@@ -0,0 +1 @@
+from .KS2 import IceRiverKS2
diff --git a/pyasic/miners/iceriver/iceminer/__init__.py b/pyasic/miners/iceriver/iceminer/__init__.py
new file mode 100644
index 00000000..54c0f2b8
--- /dev/null
+++ b/pyasic/miners/iceriver/iceminer/__init__.py
@@ -0,0 +1 @@
+from .KSX import *
diff --git a/pyasic/settings/__init__.py b/pyasic/settings/__init__.py
index 5fbc0b5e..e4e9b684 100644
--- a/pyasic/settings/__init__.py
+++ b/pyasic/settings/__init__.py
@@ -37,6 +37,7 @@ _settings = { # defaults
"default_auradine_web_password": "admin",
"default_epic_web_password": "letmein",
"default_hive_web_password": "admin",
+ "default_iceriver_web_password": "12345678",
"default_antminer_ssh_password": "miner",
"default_bosminer_ssh_password": "root",
}
diff --git a/pyasic/web/__init__.py b/pyasic/web/__init__.py
index 5e030e65..f6576a0a 100644
--- a/pyasic/web/__init__.py
+++ b/pyasic/web/__init__.py
@@ -19,5 +19,6 @@ from .base import BaseWebAPI
from .braiins_os import BOSerWebAPI, BOSMinerWebAPI
from .epic import ePICWebAPI
from .goldshell import GoldshellWebAPI
+from .iceriver import IceRiverWebAPI
from .innosilicon import InnosiliconWebAPI
from .vnish import VNishWebAPI
diff --git a/pyasic/web/iceriver.py b/pyasic/web/iceriver.py
new file mode 100644
index 00000000..1ba1e555
--- /dev/null
+++ b/pyasic/web/iceriver.py
@@ -0,0 +1,77 @@
+# ------------------------------------------------------------------------------
+# 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 IceRiverWebAPI(BaseWebAPI):
+ def __init__(self, ip: str) -> None:
+ super().__init__(ip)
+ self.username = "admin"
+ self.pwd = settings.get("default_iceriver_web_password", "12345678")
+
+ 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}/user/loginpost",
+ params={"post": "6", "user": self.username, "pwd": 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}/user/{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 locate(self, enable: bool):
+ return await self.send_command(
+ "userpanel", post="5", locate="1" if enable else "0"
+ )
+
+ async def userpanel(self):
+ return await self.send_command("userpanel", post="4")