Compare commits

..

74 Commits

Author SHA1 Message Date
Upstream Data
ccb5eb73db version: bump version number 2024-10-30 08:49:25 -06:00
Upstream Data
d143667bd6 feature: add warning message when instantiating an unsupported miner type 2024-10-30 08:49:01 -06:00
Upstream Data
87d809abc0 bug: update KS3 fan count 2024-10-30 08:44:38 -06:00
Upstream Data
4dc5b1a541 feature: add chip count for WM M60VK20 2024-10-30 08:44:08 -06:00
Upstream Data
ddd3e867f9 feature: add stratum+ssl pool url scheme 2024-10-30 08:41:35 -06:00
Upstream Data
77480d3d69 feature: add support for antminer KS5 2024-10-30 08:39:05 -06:00
Upstream Data
0767c93002 feature: add support for antminer KS3 2024-10-30 08:33:59 -06:00
Upstream Data
e690e6dd3b version: bump version number 2024-10-29 16:16:13 -06:00
James Hilliard
d4665ed768 Update betterproto and regenerate protoc files
Signed-off-by: James Hilliard <james.hilliard1@gmail.com>
2024-10-29 16:15:30 -06:00
James Hilliard
b90a92c0df Update betterproto and regenerate protoc files
Signed-off-by: James Hilliard <james.hilliard1@gmail.com>
2024-10-29 16:15:11 -06:00
Upstream Data
50cfcf9796 version: bump version number 2024-10-29 09:56:30 -06:00
Brett Rowan
5d204f09da Merge pull request #213 from jameshilliard/update-deps
Update dependencices
2024-10-29 09:56:07 -06:00
James Hilliard
4c0410322f Update dependencices 2024-10-29 09:53:33 -06:00
Upstream Data
fbb2b3f6e7 version: bump version number 2024-10-29 09:46:03 -06:00
Upstream Data
0f09fb49fc bug: fix inf and nan parsing issues by replacing them with 0 2024-10-29 09:35:19 -06:00
Upstream Data
b0d063d6ed feature: add support for Inno A11MX (no chip counts) 2024-10-29 09:15:57 -06:00
Upstream Data
a68fe70af4 bug: fix pool parsing failing with no scheme 2024-10-29 08:57:43 -06:00
Upstream Data
43c7ac281b feature: add support for KA3 2024-10-29 08:29:09 -06:00
Upstream Data
a97ae55a06 version: bump version number 2024-10-28 08:18:52 -06:00
Upstream Data
4a3a6f4186 feature: add get_mac for bitaxe 2024-10-28 08:18:15 -06:00
Brett Rowan
f976724ada version: bump version number 2024-10-25 14:12:34 -06:00
Brett Rowan
2632bdaa30 Merge pull request #208 from eandersson/braiin_fix 2024-10-25 14:11:33 -06:00
Erik Olof Gunnar Andersson
91016d7b8c Fix issue with BraiinsOS health check failing 2024-10-25 22:04:34 +02:00
Upstream Data
2b00e741ca version: bump version number
Fixes: #206
2024-10-08 08:16:25 -06:00
Upstream Data
d496c11d67 bug: fix some cases where Antminer online status couldnt be parsed
Re: #206
2024-10-08 08:16:25 -06:00
Upstream Data
5880223517 bug: fix goldshell issues with pools data 2024-10-08 08:16:25 -06:00
Brett Rowan
394a5dcd0d Merge pull request #204 from Ytemiloluwa/BFGMiner
feat: Add _get_pools method for BFGMiner(StockFirmware)
2024-10-01 08:49:40 -06:00
Upstream Data
7365275f46 version: bump version number. 2024-09-24 12:51:17 -06:00
Upstream Data
0ecab5fdd4 bug: fix an issue with moving board slots on BOS+. 2024-09-24 12:50:56 -06:00
ytemiloluwa
ed0d9f73e4 backends: add _get_pools method to bfgminer 2024-09-19 09:05:29 +01:00
Upstream Data
28f4e16662 version: bump version number. 2024-09-18 13:16:51 -06:00
Upstream Data
b9b0bff946 bug: pin betterproto version to avoid errors with unset oneof variants 2024-09-18 13:16:28 -06:00
Upstream Data
790718a5df bug: fix some issues with BOS+ calls. 2024-09-18 13:05:08 -06:00
Brett Rowan
96a0301f5e Merge pull request #203 from Ytemiloluwa/BTMiner
feat: Add _get_pools method for BTMiner(StockFirmware)
2024-09-17 11:30:14 -06:00
ytemiloluwa
c57b019b7d backends: add _get_pools to BTMiner 2024-09-17 08:55:09 +01:00
Brett Rowan
af920c4dda version: bump version number 2024-09-12 17:28:19 -06:00
Brett Rowan
f3d11788ed bug: fix missing await calls
Fixes #201
2024-09-12 17:27:57 -06:00
Brett Rowan
fd0e02af59 feature: add support for BOSMinerT21 2024-09-12 17:19:08 -06:00
Brett Rowan
2a6c51d52c Merge pull request #188 from Ytemiloluwa/marathon
feat: add _get_pools method for marathon miner
2024-09-04 08:38:29 -06:00
ytemiloluwa
2d62e2070b pool: highest priority 2024-09-04 15:21:39 +01:00
ytemiloluwa
b143bd70f0 updated keys 2024-09-03 22:49:55 +01:00
ytemiloluwa
605509c57c updated keys 2024-09-03 21:35:14 +01:00
Brett Rowan
7036137b23 Merge pull request #189 from 1e9abhi1e10/luxminer_firmware 2024-09-02 22:14:15 -06:00
1e9abhi1e10
7a9ff535b4 Refactor upgrade_firmware to maintain bool return type 2024-09-03 09:42:08 +05:30
Brett Rowan
f185bafe2a version: bump version number. 2024-09-02 21:12:56 -06:00
Brett Rowan
ab81d5d020 feature: add some more whatsminer chip counts. 2024-09-02 21:12:37 -06:00
1e9abhi1e10
0965e6489b return status message in upgrade_firmware function 2024-09-02 23:43:17 +05:30
ytemiloluwa
792e1c9cad corrected parsing 2024-09-02 09:27:30 +01:00
Brett Rowan
a6721f971a version: bump version number. 2024-09-01 16:49:26 -06:00
Brett Rowan
8113d0e4e0 bug: remove print statement. 2024-09-01 16:49:07 -06:00
Brett Rowan
e3c7d3f8a2 version: bump version number. 2024-09-01 16:48:15 -06:00
Brett Rowan
6415de8c73 bug: fix more parsing issues. 2024-09-01 16:47:53 -06:00
Brett Rowan
f2838cf31d bug: fix avalon nano parsing. 2024-09-01 16:41:28 -06:00
Brett Rowan
fbd49b370d version: bump version number. 2024-09-01 16:23:33 -06:00
Brett Rowan
79f7296576 bug: fix some issues with avalonminer parsing. 2024-09-01 16:22:59 -06:00
Brett Rowan
76f4ca5f89 version: bump version number. 2024-09-01 13:22:32 -06:00
Brett Rowan
477acda1c1 feature: add support for Avalon Nano 3. 2024-09-01 13:21:53 -06:00
Brett Rowan
a57f343dcc version: bump version number. 2024-09-01 10:05:01 -06:00
Brett Rowan
36e9201ed4 bug: fix false positives as VNish before checking other firmware types. 2024-09-01 10:04:31 -06:00
Brett Rowan
c1525501d4 Merge pull request #190 from UpstreamData/dev_iceriver
feature: Add iceriver support
2024-09-01 09:53:39 -06:00
Brett Rowan
e4bb90a569 Merge pull request #187 from 1e9abhi1e10/antminer_firmware
feat: Add update firmware for Antminer
2024-09-01 09:52:02 -06:00
1e9abhi1e10
28642cc521 Refactor firmware upgrade process 2024-08-27 02:34:23 +05:30
Brett Rowan
beae79ddec version: bump version number. 2024-08-25 10:36:32 -06:00
Brett Rowan
f02e10ab3d bug: fix token not needing to be passed to upgrade wm firmware. 2024-08-25 10:36:11 -06:00
1e9abhi1e10
21636a75fa made upgrade_firmware to return boolean result 2024-08-23 12:45:20 +05:30
1e9abhi1e10
6fdd156fa3 Added upgraderun in rpc/luxminer 2024-08-21 01:06:50 +05:30
Upstream Data
e9fcf25ad3 docs: update docs with iceriver support. 2024-08-20 10:24:06 -06:00
Upstream Data
0ea5ee8239 feature: add more iceriver functionality. 2024-08-20 10:16:35 -06:00
Upstream Data
fba25cba61 feature: add a couple iceriver data gathering functions. 2024-08-20 09:59:33 -06:00
Upstream Data
343b5a1c50 feature: add basic iceriver framework. 2024-08-20 09:45:31 -06:00
1e9abhi1e10
b957aa7fba feat: Add update firmware for LuxOS Miner 2024-08-20 16:12:33 +05:30
ytemiloluwa
a71aa6868a backends: add _get_pools to marathon 2024-08-19 07:08:22 +01:00
1e9abhi1e10
6b50bf0cf7 Fix some minor issues 2024-08-18 01:45:03 +05:30
1e9abhi1e10
d00444ec56 feat: Add update firmware for Antminer 2024-08-18 01:18:42 +05:30
70 changed files with 1652 additions and 659 deletions

View File

@@ -51,6 +51,8 @@ def backend_str(backend: MinerTypes) -> str:
return "Mara Firmware Miners" return "Mara Firmware Miners"
case MinerTypes.BITAXE: case MinerTypes.BITAXE:
return "Stock Firmware BitAxe Miners" return "Stock Firmware BitAxe Miners"
case MinerTypes.ICERIVER:
return "Stock Firmware IceRiver Miners"
def create_url_str(mtype: str): def create_url_str(mtype: str):

View File

@@ -225,6 +225,13 @@
show_root_heading: false show_root_heading: false
heading_level: 4 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+) ## T19 (BOS+)
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19 ::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
handler: python handler: python
@@ -281,6 +288,13 @@
show_root_heading: false show_root_heading: false
heading_level: 4 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) ## T19 (VNish)
::: pyasic.miners.antminer.vnish.X19.T19.VNishT19 ::: pyasic.miners.antminer.vnish.X19.T19.VNishT19
handler: python handler: python

View File

@@ -8,6 +8,13 @@
show_root_heading: false show_root_heading: false
heading_level: 4 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) ## T21 (Stock)
::: pyasic.miners.antminer.bmminer.X21.T21.BMMinerT21 ::: pyasic.miners.antminer.bmminer.X21.T21.BMMinerT21
handler: python handler: python
@@ -36,6 +43,13 @@
show_root_heading: false show_root_heading: false
heading_level: 4 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) ## T21 (ePIC)
::: pyasic.miners.antminer.epic.X21.T21.ePICT21 ::: pyasic.miners.antminer.epic.X21.T21.ePICT21
handler: python handler: python

View File

@@ -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

View File

@@ -89,6 +89,7 @@ details {
<summary>X21 Series:</summary> <summary>X21 Series:</summary>
<ul> <ul>
<li><a href="../antminer/X21#s21-stock">S21 (Stock)</a></li> <li><a href="../antminer/X21#s21-stock">S21 (Stock)</a></li>
<li><a href="../antminer/X21#s21-pro-stock">S21 Pro (Stock)</a></li>
<li><a href="../antminer/X21#t21-stock">T21 (Stock)</a></li> <li><a href="../antminer/X21#t21-stock">T21 (Stock)</a></li>
</ul> </ul>
</details> </details>
@@ -461,6 +462,7 @@ details {
<li><a href="../antminer/X19#s19k-pro-no-pic-bos_1">S19k Pro No PIC (BOS+)</a></li> <li><a href="../antminer/X19#s19k-pro-no-pic-bos_1">S19k Pro No PIC (BOS+)</a></li>
<li><a href="../antminer/X19#s19k-pro-no-pic-bos_1">S19k Pro No PIC (BOS+)</a></li> <li><a href="../antminer/X19#s19k-pro-no-pic-bos_1">S19k Pro No PIC (BOS+)</a></li>
<li><a href="../antminer/X19#s19-xp-bos_1">S19 XP (BOS+)</a></li> <li><a href="../antminer/X19#s19-xp-bos_1">S19 XP (BOS+)</a></li>
<li><a href="../antminer/X19#s19-pro_1-hydro-bos_1">S19 Pro+ Hydro (BOS+)</a></li>
<li><a href="../antminer/X19#t19-bos_1">T19 (BOS+)</a></li> <li><a href="../antminer/X19#t19-bos_1">T19 (BOS+)</a></li>
</ul> </ul>
</details> </details>
@@ -505,6 +507,7 @@ details {
<li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li> <li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19a-vnish">S19a (VNish)</a></li> <li><a href="../antminer/X19#s19a-vnish">S19a (VNish)</a></li>
<li><a href="../antminer/X19#s19a-pro-vnish">S19a Pro (VNish)</a></li> <li><a href="../antminer/X19#s19a-pro-vnish">S19a Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19-pro-hydro-vnish">S19 Pro Hydro (VNish)</a></li>
<li><a href="../antminer/X19#t19-vnish">T19 (VNish)</a></li> <li><a href="../antminer/X19#t19-vnish">T19 (VNish)</a></li>
</ul> </ul>
</details> </details>
@@ -535,6 +538,7 @@ details {
<summary>X21 Series:</summary> <summary>X21 Series:</summary>
<ul> <ul>
<li><a href="../antminer/X21#s21-epic">S21 (ePIC)</a></li> <li><a href="../antminer/X21#s21-epic">S21 (ePIC)</a></li>
<li><a href="../antminer/X21#s21-pro-epic">S21 Pro (ePIC)</a></li>
<li><a href="../antminer/X21#t21-epic">T21 (ePIC)</a></li> <li><a href="../antminer/X21#t21-epic">T21 (ePIC)</a></li>
</ul> </ul>
</details> </details>
@@ -650,4 +654,15 @@ details {
</ul> </ul>
</details> </details>
</ul> </ul>
</details>
<details>
<summary>Stock Firmware IceRiver Miners:</summary>
<ul>
<details>
<summary>KSX Series:</summary>
<ul>
<li><a href="../iceriver/KSX#ks2-stock">KS2 (Stock)</a></li>
</ul>
</details>
</ul>
</details> </details>

806
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -198,7 +198,7 @@ class MiningModePowerTune(MinerConfigValue):
def as_boser(self) -> dict: def as_boser(self) -> dict:
cfg = { cfg = {
"set_performance_mode": SetPerformanceModeRequest( "set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action=SaveAction.SAVE_AND_APPLY,
mode=PerformanceMode( mode=PerformanceMode(
tuner_mode=TunerPerformanceMode( tuner_mode=TunerPerformanceMode(
power_target=PowerTargetMode( power_target=PowerTargetMode(
@@ -275,7 +275,7 @@ class MiningModeHashrateTune(MinerConfigValue):
def as_boser(self) -> dict: def as_boser(self) -> dict:
cfg = { cfg = {
"set_performance_mode": SetPerformanceModeRequest( "set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action=SaveAction.SAVE_AND_APPLY,
mode=PerformanceMode( mode=PerformanceMode(
tuner_mode=TunerPerformanceMode( tuner_mode=TunerPerformanceMode(
hashrate_target=HashrateTargetMode( hashrate_target=HashrateTargetMode(

View File

@@ -467,7 +467,7 @@ class PoolConfig(MinerConfigValue):
def as_boser(self, user_suffix: str = None) -> dict: def as_boser(self, user_suffix: str = None) -> dict:
return { return {
"set_pool_groups": SetPoolGroupsRequest( "set_pool_groups": SetPoolGroupsRequest(
save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action=SaveAction.SAVE_AND_APPLY,
pool_groups=[g.as_boser(user_suffix=user_suffix) for g in self.groups], pool_groups=[g.as_boser(user_suffix=user_suffix) for g in self.groups],
) )
} }

View File

@@ -7,6 +7,7 @@ from urllib.parse import urlparse
class Scheme(Enum): class Scheme(Enum):
STRATUM_V1 = "stratum+tcp" STRATUM_V1 = "stratum+tcp"
STRATUM_V2 = "stratum2+tcp" STRATUM_V2 = "stratum2+tcp"
STRATUM_V1_SSL = "stratum+ssl"
@dataclass @dataclass
@@ -25,7 +26,10 @@ class PoolUrl:
@classmethod @classmethod
def from_str(cls, url: str) -> "PoolUrl": def from_str(cls, url: str) -> "PoolUrl":
parsed_url = urlparse(url) parsed_url = urlparse(url)
scheme = Scheme(parsed_url.scheme) if not parsed_url.scheme.strip() == "":
scheme = Scheme(parsed_url.scheme)
else:
scheme = Scheme.STRATUM_V1
host = parsed_url.hostname host = parsed_url.hostname
port = parsed_url.port port = parsed_url.port
pubkey = parsed_url.path.lstrip("/") if scheme == Scheme.STRATUM_V2 else None pubkey = parsed_url.path.lstrip("/") if scheme == Scheme.STRATUM_V2 else None

View File

@@ -5,7 +5,10 @@ class AntminerModels(str, Enum):
D3 = "D3" D3 = "D3"
HS3 = "HS3" HS3 = "HS3"
L3Plus = "L3+" L3Plus = "L3+"
KA3 = "KA3"
KS3 = "KS3"
DR5 = "DR5" DR5 = "DR5"
KS5 = "KS5"
L7 = "L7" L7 = "L7"
E9Pro = "E9Pro" E9Pro = "E9Pro"
S9 = "S9" S9 = "S9"
@@ -284,6 +287,7 @@ class AvalonminerModels(str, Enum):
Avalon1066 = "Avalon 1066" Avalon1066 = "Avalon 1066"
Avalon1166Pro = "Avalon 1166 Pro" Avalon1166Pro = "Avalon 1166 Pro"
Avalon1246 = "Avalon 1246" Avalon1246 = "Avalon 1246"
AvalonNano3 = "Avalon Nano 3"
def __str__(self): def __str__(self):
return self.value return self.value
@@ -292,6 +296,7 @@ class AvalonminerModels(str, Enum):
class InnosiliconModels(str, Enum): class InnosiliconModels(str, Enum):
T3HPlus = "T3H+" T3HPlus = "T3H+"
A10X = "A10X" A10X = "A10X"
A11MX = "A11MX"
def __str__(self): def __str__(self):
return self.value return self.value
@@ -339,6 +344,13 @@ class BitAxeModels(str, Enum):
return self.value return self.value
class IceRiverModels(str, Enum):
KS2 = "KS2"
def __str__(self):
return self.value
class MinerModel: class MinerModel:
ANTMINER = AntminerModels ANTMINER = AntminerModels
WHATSMINER = WhatsminerModels WHATSMINER = WhatsminerModels
@@ -348,3 +360,4 @@ class MinerModel:
AURADINE = AuradineModels AURADINE = AuradineModels
EPIC = ePICModels EPIC = ePICModels
BITAXE = BitAxeModels BITAXE = BitAxeModels
ICERIVER = IceRiverModels

View 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 AntminerModern
from pyasic.miners.device.models import KA3
class BMMinerKA3(AntminerModern, KA3):
pass

View 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 AntminerModern
from pyasic.miners.device.models import KS3
class BMMinerKS3(AntminerModern, KS3):
pass

View File

@@ -14,4 +14,6 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .HS3 import BMMinerHS3 from .HS3 import BMMinerHS3
from .KA3 import BMMinerKA3
from .KS3 import BMMinerKS3
from .L3 import BMMinerL3Plus from .L3 import BMMinerL3Plus

View File

@@ -0,0 +1,21 @@
# ------------------------------------------------------------------------------
# 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 AntminerModern
from pyasic.miners.device.models import KS5
class BMMinerKS5(AntminerModern, KS5):
supports_shutdown = False

View File

@@ -0,0 +1,16 @@
# ------------------------------------------------------------------------------
# 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 .KS5 import BMMinerKS5

View File

@@ -14,6 +14,7 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .X3 import * from .X3 import *
from .X5 import *
from .X7 import * from .X7 import *
from .X9 import * from .X9 import *
from .X17 import * from .X17 import *

View 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 BOSer
from pyasic.miners.device.models import T21
class BOSMinerT21(BOSer, T21):
pass

View File

@@ -15,3 +15,4 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .S21 import BOSMinerS21 from .S21 import BOSMinerS21
from .T21 import BOSMinerT21

View File

@@ -20,3 +20,4 @@ from .A9X import *
from .A10X import * from .A10X import *
from .A11X import * from .A11X import *
from .A12X import * from .A12X import *
from .nano import *

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 .nano3 import CGMinerAvalonNano3

View 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 AvalonMiner
from pyasic.miners.device.models import AvalonNano3
class CGMinerAvalonNano3(AvalonMiner, AvalonNano3):
pass

View File

@@ -24,6 +24,7 @@ from .cgminer import CGMiner
from .epic import ePIC from .epic import ePIC
from .goldshell import GoldshellMiner from .goldshell import GoldshellMiner
from .hiveon import Hiveon from .hiveon import Hiveon
from .iceriver import IceRiver
from .innosilicon import Innosilicon from .innosilicon import Innosilicon
from .luxminer import LUXMiner from .luxminer import LUXMiner
from .marathon import MaraMiner from .marathon import MaraMiner

View File

@@ -14,6 +14,8 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import logging
from pathlib import Path
from typing import List, Optional, Union from typing import List, Optional, Union
from pyasic.config import MinerConfig, MiningModeConfig from pyasic.config import MinerConfig, MiningModeConfig
@@ -124,6 +126,41 @@ class AntminerModern(BMMiner):
# break # break
# await asyncio.sleep(1) # await asyncio.sleep(1)
async def upgrade_firmware(self, file: Path, keep_settings: bool = True) -> str:
"""
Upgrade the firmware of the AntMiner device.
Args:
file (Path): Path to the firmware file.
keep_settings (bool): Whether to keep the current settings after the update.
Returns:
str: Result of the upgrade process.
"""
if not file:
raise ValueError("File location must be provided for firmware upgrade.")
try:
result = await self.web.update_firmware(
file=file, keep_settings=keep_settings
)
if result.get("success"):
logging.info(
"Firmware upgrade process completed successfully for AntMiner."
)
return "Firmware upgrade completed successfully."
else:
error_message = result.get("message", "Unknown error")
logging.error(f"Firmware upgrade failed. Response: {error_message}")
return f"Firmware upgrade failed. Response: {error_message}"
except Exception as e:
logging.error(
f"An error occurred during the firmware upgrade process: {e}",
exc_info=True,
)
raise
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
data = await self.web.blink(blink=True) data = await self.web.blink(blink=True)
if data: if data:
@@ -337,7 +374,7 @@ class AntminerModern(BMMiner):
if web_get_conf is not None: if web_get_conf is not None:
try: try:
if web_get_conf["bitmain-work-mode"].isdigit(): if str(web_get_conf["bitmain-work-mode"]).isdigit():
return ( return (
False if int(web_get_conf["bitmain-work-mode"]) == 1 else True False if int(web_get_conf["bitmain-work-mode"]) == 1 else True
) )

View File

@@ -118,7 +118,7 @@ class AvalonMiner(CGMiner):
stats_items = [] stats_items = []
stats_dict = {} stats_dict = {}
for item in _stats_items: for item in _stats_items:
if ":" in item: if ": " in item:
data = item.replace("]", "").split("[") data = item.replace("]", "").split("[")
data_list = [i.split(": ") for i in data[1].strip().split(", ")] data_list = [i.split(": ") for i in data[1].strip().split(", ")]
data_dict = {} data_dict = {}
@@ -147,10 +147,7 @@ class AvalonMiner(CGMiner):
if raw_data[0] == "": if raw_data[0] == "":
raw_data = raw_data[1:] raw_data = raw_data[1:]
if len(raw_data) == 2: stats_dict[raw_data[0]] = raw_data[1:]
stats_dict[raw_data[0]] = raw_data[1]
else:
stats_dict[raw_data[0]] = raw_data[1:]
stats_items.append(raw_data) stats_items.append(raw_data)
return stats_dict return stats_dict
@@ -220,7 +217,7 @@ class AvalonMiner(CGMiner):
try: try:
board_hr = parsed_stats["MGHS"][board] board_hr = parsed_stats["MGHS"][board]
hashboards[board].hashrate = AlgoHashRate.SHA256( hashboards[board].hashrate = AlgoHashRate.SHA256(
board_hr, HashUnit.SHA256.GH float(board_hr), HashUnit.SHA256.GH
).into(self.algo.unit.default) ).into(self.algo.unit.default)
except LookupError: except LookupError:
pass pass
@@ -256,7 +253,7 @@ class AvalonMiner(CGMiner):
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] unparsed_stats = rpc_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats) parsed_stats = self.parse_stats(unparsed_stats)
return AlgoHashRate.SHA256( return AlgoHashRate.SHA256(
parsed_stats["GHSmm"], HashUnit.SHA256.GH float(parsed_stats["GHSmm"][0]), HashUnit.SHA256.GH
).into(self.algo.unit.default) ).into(self.algo.unit.default)
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass
@@ -272,7 +269,7 @@ class AvalonMiner(CGMiner):
try: try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] unparsed_stats = rpc_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats) parsed_stats = self.parse_stats(unparsed_stats)
return float(parsed_stats["Temp"]) return float(parsed_stats["Temp"][0])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass
@@ -287,7 +284,7 @@ class AvalonMiner(CGMiner):
try: try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] unparsed_stats = rpc_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats) parsed_stats = self.parse_stats(unparsed_stats)
return int(parsed_stats["MPO"]) return int(parsed_stats["MPO"][0])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass
@@ -308,7 +305,7 @@ class AvalonMiner(CGMiner):
for fan in range(self.expected_fans): for fan in range(self.expected_fans):
try: try:
fans_data[fan].speed = int(parsed_stats[f"Fan{fan + 1}"]) fans_data[fan].speed = int(parsed_stats[f"Fan{fan + 1}"][0])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass
return fans_data return fans_data
@@ -326,7 +323,7 @@ class AvalonMiner(CGMiner):
try: try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] unparsed_stats = rpc_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats) parsed_stats = self.parse_stats(unparsed_stats)
led = int(parsed_stats["Led"]) led = int(parsed_stats["Led"][0])
return True if led == 1 else False return True if led == 1 else False
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass

View File

@@ -22,6 +22,7 @@ from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware from pyasic.miners.device.firmware import StockFirmware
from pyasic.rpc.bfgminer import BFGMinerRPCAPI from pyasic.rpc.bfgminer import BFGMinerRPCAPI
from pyasic.data.pools import PoolMetrics, PoolUrl
BFGMINER_DATA_LOC = DataLocations( BFGMINER_DATA_LOC = DataLocations(
**{ **{
@@ -49,6 +50,10 @@ BFGMINER_DATA_LOC = DataLocations(
"_get_fans", "_get_fans",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_stats", "stats")],
), ),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
),
} }
) )
@@ -207,6 +212,36 @@ class BFGMiner(StockFirmware):
return fans return fans
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
if rpc_pools is None:
try:
rpc_pools = await self.rpc.pools()
except APIError:
pass
pools_data = []
if rpc_pools is not None:
try:
pools = rpc_pools.get("POOLS", [])
for pool_info in pools:
url = pool_info.get("URL")
pool_url = PoolUrl.from_str(url) if url else None
pool_data = PoolMetrics(
accepted=pool_info.get("Accepted"),
rejected=pool_info.get("Rejected"),
get_failures=pool_info.get("Get Failures"),
remote_failures=pool_info.get("Remote Failures"),
active=pool_info.get("Stratum Active"),
alive=pool_info.get("Status") == "Alive",
url=pool_url,
user=pool_info.get("User"),
index=pool_info.get("POOL"),
)
pools_data.append(pool_data)
except LookupError:
pass
return pools_data
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_stats: dict = None self, rpc_stats: dict = None
) -> Optional[AlgoHashRate]: ) -> Optional[AlgoHashRate]:

View File

@@ -41,6 +41,10 @@ BITAXE_DATA_LOC = DataLocations(
"_get_api_ver", "_get_api_ver",
[WebAPICommand("web_system_info", "system/info")], [WebAPICommand("web_system_info", "system/info")],
), ),
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_system_info", "system/info")],
),
} }
) )
@@ -187,3 +191,16 @@ class BitAxe(BaseMiner):
return web_system_info["version"] return web_system_info["version"]
except KeyError: except KeyError:
pass pass
async def _get_mac(self, web_system_info: dict = None) -> Optional[str]:
if web_system_info is None:
try:
web_system_info = await self.web.system_info()
except APIError:
pass
if web_system_info is not None:
try:
return web_system_info["macAddr"].upper()
except KeyError:
pass

View File

@@ -21,6 +21,7 @@ from typing import List, Optional, Union
import aiofiles import aiofiles
import tomli_w import tomli_w
try: try:
import tomllib import tomllib
except ImportError: except ImportError:
@@ -726,9 +727,8 @@ BOSER_DATA_LOC = DataLocations(
[RPCAPICommand("rpc_summary", "summary")], [RPCAPICommand("rpc_summary", "summary")],
), ),
str(DataOptions.POOLS): DataFunction( str(DataOptions.POOLS): DataFunction(
"_get_pools", "_get_pools", [WebAPICommand("grpc_pool_groups", "get_pool_groups")]
[WebAPICommand("grpc_pool_groups", "get_pool_groups")] ),
)
} }
) )
@@ -798,7 +798,7 @@ class BOSer(BraiinsOSFirmware):
async def set_power_limit(self, wattage: int) -> bool: async def set_power_limit(self, wattage: int) -> bool:
try: try:
result = await self.web.set_power_target( result = await self.web.set_power_target(
wattage, save_action=SaveAction.SAVE_ACTION_SAVE_AND_FORCE_APPLY wattage, save_action=SaveAction.SAVE_AND_FORCE_APPLY
) )
except APIError: except APIError:
return False return False
@@ -926,8 +926,10 @@ class BOSer(BraiinsOSFirmware):
pass pass
if grpc_hashboards is not None: if grpc_hashboards is not None:
for board in grpc_hashboards["hashboards"]: grpc_boards = sorted(
idx = int(board["id"]) - 1 grpc_hashboards["hashboards"], key=lambda x: int(x["id"])
)
for idx, board in enumerate(grpc_boards):
if board.get("chipsCount") is not None: if board.get("chipsCount") is not None:
hashboards[idx].chips = board["chipsCount"] hashboards[idx].chips = board["chipsCount"]
if board.get("boardTemp") is not None: if board.get("boardTemp") is not None:
@@ -951,7 +953,7 @@ class BOSer(BraiinsOSFirmware):
async def _get_wattage(self, grpc_miner_stats: dict = None) -> Optional[int]: async def _get_wattage(self, grpc_miner_stats: dict = None) -> Optional[int]:
if grpc_miner_stats is None: if grpc_miner_stats is None:
try: try:
grpc_miner_stats = self.web.get_miner_stats() grpc_miner_stats = await self.web.get_miner_stats()
except APIError: except APIError:
pass pass
@@ -983,7 +985,7 @@ class BOSer(BraiinsOSFirmware):
async def _get_fans(self, grpc_cooling_state: dict = None) -> List[Fan]: async def _get_fans(self, grpc_cooling_state: dict = None) -> List[Fan]:
if grpc_cooling_state is None: if grpc_cooling_state is None:
try: try:
grpc_cooling_state = self.web.get_cooling_state() grpc_cooling_state = await self.web.get_cooling_state()
except APIError: except APIError:
pass pass
@@ -1086,12 +1088,12 @@ class BOSer(BraiinsOSFirmware):
url=pool_info["url"], url=pool_info["url"],
user=pool_info["user"], user=pool_info["user"],
index=idx, index=idx,
accepted=pool_info["stats"]["acceptedShares"], accepted=pool_info["stats"].get("acceptedShares", 0),
rejected=pool_info["stats"]["rejectedShares"], rejected=pool_info["stats"].get("rejectedShares", 0),
get_failures=pool_info["stats"]["stale_shares"], get_failures=0,
remote_failures=0, remote_failures=0,
active=pool_info["active"], active=pool_info.get("active", False),
alive=pool_info["alive"] alive=pool_info.get("alive"),
) )
pools_data.append(pool_data) pools_data.append(pool_data)

View File

@@ -27,6 +27,7 @@ from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware from pyasic.miners.device.firmware import StockFirmware
from pyasic.rpc.btminer import BTMinerRPCAPI from pyasic.rpc.btminer import BTMinerRPCAPI
from pyasic.data.pools import PoolMetrics, PoolUrl
BTMINER_DATA_LOC = DataLocations( BTMINER_DATA_LOC = DataLocations(
**{ **{
@@ -109,6 +110,10 @@ BTMINER_DATA_LOC = DataLocations(
"_get_uptime", "_get_uptime",
[RPCAPICommand("rpc_summary", "summary")], [RPCAPICommand("rpc_summary", "summary")],
), ),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
)
} }
) )
@@ -655,13 +660,42 @@ class BTMiner(StockFirmware):
except LookupError: except LookupError:
pass pass
async def upgrade_firmware(self, file: Path, token: str): async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
if rpc_pools is None:
try:
rpc_pools = await self.rpc.pools()
except APIError:
pass
pools_data = []
if rpc_pools is not None:
try:
pools = rpc_pools.get("POOLS", [])
for pool_info in pools:
url = pool_info.get("URL")
pool_url = PoolUrl.from_str(url) if url else None
pool_data = PoolMetrics(
accepted=pool_info.get("Accepted"),
rejected=pool_info.get("Rejected"),
get_failures=pool_info.get("Get Failures"),
remote_failures=pool_info.get("Remote Failures"),
active=pool_info.get("Stratum Active"),
alive=pool_info.get("Status") == "Alive",
url=pool_url,
user=pool_info.get("User"),
index=pool_info.get("POOL"),
)
pools_data.append(pool_data)
except LookupError:
pass
return pools_data
async def upgrade_firmware(self, file: Path):
""" """
Upgrade the firmware of the Whatsminer device. Upgrade the firmware of the Whatsminer device.
Args: Args:
file (Path): The local file path of the firmware to be uploaded. file (Path): The local file path of the firmware to be uploaded.
token (str): The authentication token for the firmware upgrade.
Returns: Returns:
str: Confirmation message after upgrading the firmware. str: Confirmation message after upgrading the firmware.

View File

@@ -62,6 +62,10 @@ GOLDSHELL_DATA_LOC = DataLocations(
"_get_fans", "_get_fans",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_stats", "stats")],
), ),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
),
} }
) )

View File

@@ -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

View File

@@ -14,6 +14,7 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional from typing import List, Optional
import logging
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
@@ -146,6 +147,22 @@ class LUXMiner(LuxOSFirmware):
async def get_config(self) -> MinerConfig: async def get_config(self) -> MinerConfig:
return self.config return self.config
async def upgrade_firmware(self) -> bool:
"""
Upgrade the firmware on a LuxOS miner by calling the 'updaterun' API command.
Returns:
bool: True if the firmware upgrade was successfully initiated, False otherwise.
"""
try:
await self.rpc.upgraderun()
logging.info(f"{self.ip}: Firmware upgrade initiated successfully.")
return True
except APIError as e:
logging.error(f"{self.ip}: Firmware upgrade failed: {e}")
return False
################################################## ##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### DATA GATHERING FUNCTIONS (get_{some_data}) ###
################################################## ##################################################

View File

@@ -9,6 +9,7 @@ from pyasic.miners.device.firmware import MaraFirmware
from pyasic.misc import merge_dicts from pyasic.misc import merge_dicts
from pyasic.rpc.marathon import MaraRPCAPI from pyasic.rpc.marathon import MaraRPCAPI
from pyasic.web.marathon import MaraWebAPI from pyasic.web.marathon import MaraWebAPI
from pyasic.data.pools import PoolMetrics, PoolUrl
MARA_DATA_LOC = DataLocations( MARA_DATA_LOC = DataLocations(
**{ **{
@@ -60,6 +61,10 @@ MARA_DATA_LOC = DataLocations(
"_get_uptime", "_get_uptime",
[WebAPICommand("web_brief", "brief")], [WebAPICommand("web_brief", "brief")],
), ),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[WebAPICommand("web_pools", "pools")],
),
} }
) )
@@ -305,3 +310,40 @@ class MaraMiner(MaraFirmware):
return web_miner_config["mode"]["concorde"]["power-target"] return web_miner_config["mode"]["concorde"]["power-target"]
except LookupError: except LookupError:
pass pass
async def _get_pools(self, web_pools: list = None) -> List[PoolMetrics]:
if web_pools is None:
try:
web_pools = await self.web.pools()
except APIError:
return []
active_pool_index = None
highest_priority = float('inf')
for pool_info in web_pools:
if pool_info.get("status") == "Alive" and pool_info.get("priority", float('inf')) < highest_priority:
highest_priority = pool_info.get["priority"]
active_pool_index = pool_info["index"]
pools_data = []
if web_pools is not None:
try:
for pool_info in web_pools:
url = pool_info.get("url")
pool_url = PoolUrl.from_str(url) if url else None
pool_data = PoolMetrics(
accepted=pool_info.get("accepted"),
rejected=pool_info.get("rejected"),
get_failures=pool_info.get("stale"),
remote_failures=pool_info.get("discarded"),
active=pool_info.get("index") == active_pool_index,
alive=pool_info.get("status") == "Alive",
url=pool_url,
user=pool_info.get("user"),
index=pool_info.get("index"),
)
pools_data.append(pool_data)
except LookupError:
pass
return pools_data

View File

@@ -48,3 +48,7 @@ class ePICMake(BaseMiner):
class BitAxeMake(BaseMiner): class BitAxeMake(BaseMiner):
make = MinerMake.BITAXE make = MinerMake.BITAXE
class IceRiverMake(BaseMiner):
make = MinerMake.BITAXE

View File

@@ -19,5 +19,6 @@ from .auradine import *
from .avalonminer import * from .avalonminer import *
from .epic import * from .epic import *
from .goldshell import * from .goldshell import *
from .iceriver import *
from .innosilicon import * from .innosilicon import *
from .whatsminer import * from .whatsminer import *

View File

@@ -0,0 +1,23 @@
# ------------------------------------------------------------------------------
# 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.device.models import MinerModel
from pyasic.miners.device.makes import AntMinerMake
class KA3(AntMinerMake):
raw_model = MinerModel.ANTMINER.KA3
expected_chips = 92

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.device.models import MinerModel
from pyasic.miners.device.makes import AntMinerMake
class KS3(AntMinerMake):
raw_model = MinerModel.ANTMINER.KS3
expected_chips = 92
expected_fans = 2

View File

@@ -15,4 +15,6 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .D3 import D3 from .D3 import D3
from .HS3 import HS3 from .HS3 import HS3
from .KA3 import KA3
from .KS3 import KS3
from .L3 import L3Plus from .L3 import L3Plus

View File

@@ -0,0 +1,23 @@
# ------------------------------------------------------------------------------
# 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.device.models import MinerModel
from pyasic.miners.device.makes import AntMinerMake
class KS5(AntMinerMake):
raw_model = MinerModel.ANTMINER.KS5
expected_chips = 92

View File

@@ -14,3 +14,4 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .DR5 import DR5 from .DR5 import DR5
from .KS5 import KS5

View File

@@ -20,3 +20,4 @@ from .A9X import *
from .A10X import * from .A10X import *
from .A11X import * from .A11X import *
from .A12X import * from .A12X import *
from .nano import *

View File

@@ -0,0 +1 @@
from .nano3 import AvalonNano3

View File

@@ -0,0 +1,10 @@
from pyasic.device import MinerModel
from pyasic.miners.device.makes import AvalonMinerMake
class AvalonNano3(AvalonMinerMake):
raw_model = MinerModel.AVALONMINER.AvalonNano3
expected_hashboards = 1
expected_chips = 10
expected_fans = 1

View File

@@ -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

View File

@@ -0,0 +1 @@
from .KS2 import KS2

View File

@@ -0,0 +1 @@
from .KSX import *

View File

@@ -0,0 +1,21 @@
# ------------------------------------------------------------------------------
# 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.device.models import MinerModel
from pyasic.miners.device.makes import InnosiliconMake
class A11MX(InnosiliconMake):
raw_model = MinerModel.INNOSILICON.A11MX

View File

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

View File

@@ -15,4 +15,5 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .A10X import * from .A10X import *
from .A11X import *
from .T3X import * from .T3X import *

View File

@@ -69,6 +69,8 @@ class M50VH60(WhatsMinerMake):
class M50VH70(WhatsMinerMake): class M50VH70(WhatsMinerMake):
raw_model = MinerModel.WHATSMINER.M50VH70 raw_model = MinerModel.WHATSMINER.M50VH70
expected_chips = 105
class M50VH80(WhatsMinerMake): class M50VH80(WhatsMinerMake):
raw_model = MinerModel.WHATSMINER.M50VH80 raw_model = MinerModel.WHATSMINER.M50VH80
@@ -83,6 +85,10 @@ class M50VJ10(WhatsMinerMake):
class M50VJ20(WhatsMinerMake): class M50VJ20(WhatsMinerMake):
raw_model = MinerModel.WHATSMINER.M50VJ20 raw_model = MinerModel.WHATSMINER.M50VJ20
expected_chips = 111
class M50VJ30(WhatsMinerMake): class M50VJ30(WhatsMinerMake):
raw_model = MinerModel.WHATSMINER.M50VJ30 raw_model = MinerModel.WHATSMINER.M50VJ30
expected_chips = 117

View File

@@ -51,3 +51,5 @@ class M50SVH40(WhatsMinerMake):
class M50SVH50(WhatsMinerMake): class M50SVH50(WhatsMinerMake):
raw_model = MinerModel.WHATSMINER.M50SVH50 raw_model = MinerModel.WHATSMINER.M50SVH50
expected_chips = 135

View File

@@ -24,6 +24,8 @@ class M60VK10(WhatsMinerMake):
class M60VK20(WhatsMinerMake): class M60VK20(WhatsMinerMake):
raw_model = MinerModel.WHATSMINER.M60VK20 raw_model = MinerModel.WHATSMINER.M60VK20
expected_chips = 172
class M60VK30(WhatsMinerMake): class M60VK30(WhatsMinerMake):
raw_model = MinerModel.WHATSMINER.M60VK30 raw_model = MinerModel.WHATSMINER.M60VK30

View File

@@ -20,6 +20,8 @@ import enum
import ipaddress import ipaddress
import json import json
import re import re
import warnings
from logging import warning
from typing import Any, AsyncGenerator, Callable from typing import Any, AsyncGenerator, Callable
import anyio import anyio
@@ -38,6 +40,7 @@ from pyasic.miners.bitaxe import *
from pyasic.miners.blockminer import * from pyasic.miners.blockminer import *
from pyasic.miners.device.makes import * from pyasic.miners.device.makes import *
from pyasic.miners.goldshell import * from pyasic.miners.goldshell import *
from pyasic.miners.iceriver import *
from pyasic.miners.innosilicon import * from pyasic.miners.innosilicon import *
from pyasic.miners.whatsminer import * from pyasic.miners.whatsminer import *
@@ -56,6 +59,7 @@ class MinerTypes(enum.Enum):
AURADINE = 10 AURADINE = 10
MARATHON = 11 MARATHON = 11
BITAXE = 12 BITAXE = 12
ICERIVER = 13
MINER_CLASSES = { MINER_CLASSES = {
@@ -64,7 +68,10 @@ MINER_CLASSES = {
"ANTMINER D3": CGMinerD3, "ANTMINER D3": CGMinerD3,
"ANTMINER HS3": BMMinerHS3, "ANTMINER HS3": BMMinerHS3,
"ANTMINER L3+": BMMinerL3Plus, "ANTMINER L3+": BMMinerL3Plus,
"ANTMINER KA3": BMMinerKA3,
"ANTMINER KS3": BMMinerKS3,
"ANTMINER DR5": CGMinerDR5, "ANTMINER DR5": CGMinerDR5,
"ANTMINER KS5": BMMinerKS5,
"ANTMINER L7": BMMinerL7, "ANTMINER L7": BMMinerL7,
"ANTMINER E9 PRO": BMMinerE9Pro, "ANTMINER E9 PRO": BMMinerE9Pro,
"ANTMINER S9": BMMinerS9, "ANTMINER S9": BMMinerS9,
@@ -330,11 +337,13 @@ MINER_CLASSES = {
"AVALONMINER 1066": CGMinerAvalon1066, "AVALONMINER 1066": CGMinerAvalon1066,
"AVALONMINER 1166PRO": CGMinerAvalon1166Pro, "AVALONMINER 1166PRO": CGMinerAvalon1166Pro,
"AVALONMINER 1246": CGMinerAvalon1246, "AVALONMINER 1246": CGMinerAvalon1246,
"AVALONMINER NANO3": CGMinerAvalonNano3,
}, },
MinerTypes.INNOSILICON: { MinerTypes.INNOSILICON: {
None: type("InnosiliconUnknown", (Innosilicon, InnosiliconMake), {}), None: type("InnosiliconUnknown", (Innosilicon, InnosiliconMake), {}),
"T3H+": InnosiliconT3HPlus, "T3H+": InnosiliconT3HPlus,
"A10X": InnosiliconA10X, "A10X": InnosiliconA10X,
"A11MX": InnosiliconA11MX,
}, },
MinerTypes.GOLDSHELL: { MinerTypes.GOLDSHELL: {
None: type("GoldshellUnknown", (GoldshellMiner, GoldshellMake), {}), None: type("GoldshellUnknown", (GoldshellMiner, GoldshellMake), {}),
@@ -373,6 +382,7 @@ MINER_CLASSES = {
"ANTMINER S19 PRO+ HYD.": BOSMinerS19ProPlusHydro, "ANTMINER S19 PRO+ HYD.": BOSMinerS19ProPlusHydro,
"ANTMINER T19": BOSMinerT19, "ANTMINER T19": BOSMinerT19,
"ANTMINER S21": BOSMinerS21, "ANTMINER S21": BOSMinerS21,
"ANTMINER T21": BOSMinerT21,
}, },
MinerTypes.VNISH: { MinerTypes.VNISH: {
None: VNish, None: VNish,
@@ -451,6 +461,10 @@ MINER_CLASSES = {
"BM1366": BitAxeUltra, "BM1366": BitAxeUltra,
"BM1397": BitAxeMax, "BM1397": BitAxeMax,
}, },
MinerTypes.ICERIVER: {
None: type("IceRiverUnknown", (IceRiver, IceRiverMake), {}),
"KS2": IceRiverKS2,
},
} }
@@ -623,6 +637,8 @@ class MinerFactory:
return MinerTypes.INNOSILICON return MinerTypes.INNOSILICON
if "Miner UI" in web_text: if "Miner UI" in web_text:
return MinerTypes.AURADINE return MinerTypes.AURADINE
if "<TITLE>用户界面</TITLE>" in web_text:
return MinerTypes.ICERIVER
async def _get_miner_socket(self, ip: str) -> MinerTypes | None: async def _get_miner_socket(self, ip: str) -> MinerTypes | None:
commands = ["version", "devdetails"] commands = ["version", "devdetails"]
@@ -689,8 +705,6 @@ class MinerFactory:
return MinerTypes.BRAIINS_OS return MinerTypes.BRAIINS_OS
if "BTMINER" in upper_data or "BITMICRO" in upper_data: if "BTMINER" in upper_data or "BITMICRO" in upper_data:
return MinerTypes.WHATSMINER return MinerTypes.WHATSMINER
if "VNISH" in upper_data or "DEVICE PATH" in upper_data:
return MinerTypes.VNISH
if "HIVEON" in upper_data: if "HIVEON" in upper_data:
return MinerTypes.HIVEON return MinerTypes.HIVEON
if "LUXMINER" in upper_data: if "LUXMINER" in upper_data:
@@ -705,10 +719,14 @@ class MinerFactory:
or "BFGMINER" in upper_data or "BFGMINER" in upper_data
): ):
return MinerTypes.GOLDSHELL return MinerTypes.GOLDSHELL
if "INNOMINER" in upper_data:
return MinerTypes.INNOSILICON
if "AVALON" in upper_data: if "AVALON" in upper_data:
return MinerTypes.AVALONMINER return MinerTypes.AVALONMINER
if "GCMINER" in upper_data or "FLUXOS" in upper_data: if "GCMINER" in upper_data or "FLUXOS" in upper_data:
return MinerTypes.AURADINE return MinerTypes.AURADINE
if "VNISH" in upper_data:
return MinerTypes.VNISH
async def send_web_command( async def send_web_command(
self, self,
@@ -798,7 +816,9 @@ class MinerFactory:
str_data = str_data.replace("info", "1nfo") str_data = str_data.replace("info", "1nfo")
str_data = str_data.replace("inf", "0") str_data = str_data.replace("inf", "0")
str_data = str_data.replace("1nfo", "info") str_data = str_data.replace("1nfo", "info")
str_data = str_data.replace("nano", "n4no")
str_data = str_data.replace("nan", "0") str_data = str_data.replace("nan", "0")
str_data = str_data.replace("n4no", "nano")
# fix whatever this garbage from avalonminers is `,"id":1}` # fix whatever this garbage from avalonminers is `,"id":1}`
if str_data.startswith(","): if str_data.startswith(","):
str_data = f"{{{str_data[1:]}" str_data = f"{{{str_data[1:]}"
@@ -823,6 +843,10 @@ class MinerFactory:
return MINER_CLASSES[miner_type][str(miner_model).upper()](ip) return MINER_CLASSES[miner_type][str(miner_model).upper()](ip)
except LookupError: except LookupError:
if miner_type in MINER_CLASSES: if miner_type in MINER_CLASSES:
warnings.warn(
f"Partially supported miner found: {miner_model}, please open an issue with miner data "
f"and this model on GitHub (https://github.com/UpstreamData/pyasic/issues)."
)
return MINER_CLASSES[miner_type][None](ip) return MINER_CLASSES[miner_type][None](ip)
return UnknownMiner(str(ip)) return UnknownMiner(str(ip))
@@ -898,10 +922,12 @@ class MinerFactory:
async def get_miner_model_avalonminer(self, ip: str) -> str | None: async def get_miner_model_avalonminer(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "version") sock_json_data = await self.send_api_command(ip, "version")
try: try:
miner_model = sock_json_data["VERSION"][0]["PROD"] miner_model = sock_json_data["VERSION"][0]["PROD"].upper()
if "-" in miner_model: if "-" in miner_model:
miner_model = miner_model.split("-")[0] miner_model = miner_model.split("-")[0]
if miner_model in ["AVALONNANO", "AVALON0O"]:
nano_subtype = sock_json_data["VERSION"][0]["MODEL"].upper()
miner_model = f"AVALONMINER {nano_subtype}"
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass

View File

@@ -0,0 +1 @@
from .iceminer import *

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends.iceriver import IceRiver
from pyasic.miners.device.models import KS2
class IceRiverKS2(IceRiver, KS2):
pass

View File

@@ -0,0 +1 @@
from .KS2 import IceRiverKS2

View File

@@ -0,0 +1 @@
from .KSX import *

View 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.innosilicon import Innosilicon
from pyasic.miners.device.models import A11MX
class InnosiliconA11MX(Innosilicon, A11MX):
pass

View File

@@ -0,0 +1 @@
from .A11M import InnosiliconA11MX

View File

@@ -15,4 +15,5 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .A10X import * from .A10X import *
from .A11X import *
from .T3X import * from .T3X import *

View File

@@ -268,10 +268,9 @@ If you are sure you want to use this command please use API.send_command("{comma
# fix an error with a btminer return having a missing comma. (2023-01-06 version) # fix an error with a btminer return having a missing comma. (2023-01-06 version)
str_data = str_data.replace('""temp0', '","temp0') str_data = str_data.replace('""temp0', '","temp0')
# fix an error with Avalonminers returning inf and nan # fix an error with Avalonminers returning inf and nan
str_data = str_data.replace("info", "1nfo") str_data = str_data.replace('"inf"', "0")
str_data = str_data.replace("inf", "0") str_data = str_data.replace('"nan"', "0")
str_data = str_data.replace("1nfo", "info")
str_data = str_data.replace("nan", "0")
# fix whatever this garbage from avalonminers is `,"id":1}` # fix whatever this garbage from avalonminers is `,"id":1}`
if str_data.startswith(","): if str_data.startswith(","):
str_data = f"{{{str_data[1:]}" str_data = f"{{{str_data[1:]}"

View File

@@ -749,3 +749,12 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
</details> </details>
""" """
return await self.send_command("wakeup", parameters=session_id) return await self.send_command("wakeup", parameters=session_id)
async def upgraderun(self):
"""
Send the 'updaterun' command to the miner.
Returns:
The response from the miner after sending the 'updaterun' command.
"""
return await self.send_command("updaterun")

View File

@@ -37,6 +37,7 @@ _settings = { # defaults
"default_auradine_web_password": "admin", "default_auradine_web_password": "admin",
"default_epic_web_password": "letmein", "default_epic_web_password": "letmein",
"default_hive_web_password": "admin", "default_hive_web_password": "admin",
"default_iceriver_web_password": "12345678",
"default_antminer_ssh_password": "miner", "default_antminer_ssh_password": "miner",
"default_bosminer_ssh_password": "root", "default_bosminer_ssh_password": "root",
} }

View File

@@ -19,5 +19,6 @@ from .base import BaseWebAPI
from .braiins_os import BOSerWebAPI, BOSMinerWebAPI from .braiins_os import BOSerWebAPI, BOSMinerWebAPI
from .epic import ePICWebAPI from .epic import ePICWebAPI
from .goldshell import GoldshellWebAPI from .goldshell import GoldshellWebAPI
from .iceriver import IceRiverWebAPI
from .innosilicon import InnosiliconWebAPI from .innosilicon import InnosiliconWebAPI
from .vnish import VNishWebAPI from .vnish import VNishWebAPI

View File

@@ -18,8 +18,9 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
from typing import Any from typing import Any
import aiofiles
import httpx import httpx
from pathlib import Path
from pyasic import settings from pyasic import settings
from pyasic.web.base import BaseWebAPI from pyasic.web.base import BaseWebAPI
@@ -59,9 +60,8 @@ class AntminerModernWebAPI(BaseWebAPI):
url = f"http://{self.ip}:{self.port}/cgi-bin/{command}.cgi" url = f"http://{self.ip}:{self.port}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.username, self.pwd) auth = httpx.DigestAuth(self.username, self.pwd)
try: try:
async with httpx.AsyncClient( async with httpx.AsyncClient(transport=settings.transport()) as client:
transport=settings.transport(),
) as client:
if parameters: if parameters:
data = await client.post( data = await client.post(
url, url,
@@ -71,14 +71,15 @@ class AntminerModernWebAPI(BaseWebAPI):
) )
else: else:
data = await client.get(url, auth=auth) data = await client.get(url, auth=auth)
except httpx.HTTPError: except httpx.HTTPError as e:
pass return {"success": False, "message": f"HTTP error occurred: {str(e)}"}
else: else:
if data.status_code == 200: if data.status_code == 200:
try: try:
return data.json() return data.json()
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
pass return {"success": False, "message": "Failed to decode JSON"}
return {"success": False, "message": "Unknown error occurred"}
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
@@ -403,3 +404,20 @@ class AntminerOldWebAPI(BaseWebAPI):
dict: Information about the mining pools configured in the miner. dict: Information about the mining pools configured in the miner.
""" """
return await self.send_command("miner_pools") return await self.send_command("miner_pools")
async def update_firmware(self, file: Path, keep_settings: bool = True) -> dict:
"""Perform a system update by uploading a firmware file and sending a command to initiate the update."""
async with aiofiles.open(file, "rb") as firmware:
file_content = await firmware.read()
parameters = {
"file": (file.name, file_content, "application/octet-stream"),
"filename": file.name,
"keep_settings": keep_settings
}
return await self.send_command(
command="upgrade",
**parameters
)

View File

@@ -0,0 +1,93 @@
from datetime import datetime, timedelta
from typing import Any, Dict
from betterproto import DATETIME_ZERO, TYPE_MAP, TYPE_MESSAGE, Casing, Message
# https://github.com/danielgtaylor/python-betterproto/pull/609
def to_pydict(
self, casing: Casing = Casing.CAMEL, include_default_values: bool = False
) -> Dict[str, Any]:
"""
Returns a python dict representation of this object.
Parameters
-----------
casing: :class:`Casing`
The casing to use for key values. Default is :attr:`Casing.CAMEL` for
compatibility purposes.
include_default_values: :class:`bool`
If ``True`` will include the default values of fields. Default is ``False``.
E.g. an ``int32`` field will be included with a value of ``0`` if this is
set to ``True``, otherwise this would be ignored.
Returns
--------
Dict[:class:`str`, Any]
The python dict representation of this object.
"""
output: Dict[str, Any] = {}
defaults = self._betterproto.default_gen
for field_name, meta in self._betterproto.meta_by_field_name.items():
field_is_repeated = defaults[field_name] is list
try:
value = getattr(self, field_name)
except AttributeError:
value = self._get_field_default(field_name)
cased_name = casing(field_name).rstrip("_") # type: ignore
if meta.proto_type == TYPE_MESSAGE:
if isinstance(value, datetime):
if (
value != DATETIME_ZERO
or include_default_values
or self._include_default_value_for_oneof(
field_name=field_name, meta=meta
)
):
output[cased_name] = value
elif isinstance(value, timedelta):
if (
value != timedelta(0)
or include_default_values
or self._include_default_value_for_oneof(
field_name=field_name, meta=meta
)
):
output[cased_name] = value
elif meta.wraps:
if value is not None or include_default_values:
output[cased_name] = value
elif field_is_repeated:
# Convert each item.
value = [i.to_pydict(casing, include_default_values) for i in value]
if value or include_default_values:
output[cased_name] = value
elif value is None:
if include_default_values:
output[cased_name] = None
elif (
value._serialized_on_wire
or include_default_values
or self._include_default_value_for_oneof(
field_name=field_name, meta=meta
)
):
output[cased_name] = value.to_pydict(casing, include_default_values)
elif meta.proto_type == TYPE_MAP:
for k in value:
if hasattr(value[k], "to_pydict"):
value[k] = value[k].to_pydict(casing, include_default_values)
if value or include_default_values:
output[cased_name] = value
elif (
value != self._get_field_default(field_name)
or include_default_values
or self._include_default_value_for_oneof(field_name=field_name, meta=meta)
):
output[cased_name] = value
return output
def patch():
Message.to_pydict = to_pydict

View File

@@ -26,6 +26,9 @@ from grpclib.client import Channel
from pyasic import settings from pyasic import settings
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.web.base import BaseWebAPI from pyasic.web.base import BaseWebAPI
from pyasic.web.braiins_os.better_monkey import patch
patch()
from .proto.braiins.bos import * from .proto.braiins.bos import *
from .proto.braiins.bos.v1 import * from .proto.braiins.bos.v1 import *
@@ -206,7 +209,7 @@ class BOSerWebAPI(BaseWebAPI):
async def set_immersion_mode( async def set_immersion_mode(
self, self,
enable: bool, enable: bool,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_immersion_mode", "set_immersion_mode",
@@ -227,7 +230,7 @@ class BOSerWebAPI(BaseWebAPI):
) )
async def set_default_power_target( async def set_default_power_target(
self, save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_default_power_target", "set_default_power_target",
@@ -238,7 +241,7 @@ class BOSerWebAPI(BaseWebAPI):
async def set_power_target( async def set_power_target(
self, self,
power_target: int, power_target: int,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_power_target", "set_power_target",
@@ -251,7 +254,7 @@ class BOSerWebAPI(BaseWebAPI):
async def increment_power_target( async def increment_power_target(
self, self,
power_target_increment: int, power_target_increment: int,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"increment_power_target", "increment_power_target",
@@ -265,7 +268,7 @@ class BOSerWebAPI(BaseWebAPI):
async def decrement_power_target( async def decrement_power_target(
self, self,
power_target_decrement: int, power_target_decrement: int,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"decrement_power_target", "decrement_power_target",
@@ -277,7 +280,7 @@ class BOSerWebAPI(BaseWebAPI):
) )
async def set_default_hashrate_target( async def set_default_hashrate_target(
self, save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_default_hashrate_target", "set_default_hashrate_target",
@@ -288,7 +291,7 @@ class BOSerWebAPI(BaseWebAPI):
async def set_hashrate_target( async def set_hashrate_target(
self, self,
hashrate_target: float, hashrate_target: float,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_hashrate_target", "set_hashrate_target",
@@ -302,7 +305,7 @@ class BOSerWebAPI(BaseWebAPI):
async def increment_hashrate_target( async def increment_hashrate_target(
self, self,
hashrate_target_increment: int, hashrate_target_increment: int,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"increment_hashrate_target", "increment_hashrate_target",
@@ -318,7 +321,7 @@ class BOSerWebAPI(BaseWebAPI):
async def decrement_hashrate_target( async def decrement_hashrate_target(
self, self,
hashrate_target_decrement: int, hashrate_target_decrement: int,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"decrement_hashrate_target", "decrement_hashrate_target",
@@ -359,7 +362,7 @@ class BOSerWebAPI(BaseWebAPI):
self, self,
wattage_target: int = None, wattage_target: int = None,
hashrate_target: int = None, hashrate_target: int = None,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
if wattage_target is not None and hashrate_target is not None: if wattage_target is not None and hashrate_target is not None:
logging.error( logging.error(
@@ -459,7 +462,7 @@ class BOSerWebAPI(BaseWebAPI):
async def enable_hashboards( async def enable_hashboards(
self, self,
hashboard_ids: List[str], hashboard_ids: List[str],
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"enable_hashboards", "enable_hashboards",
@@ -472,7 +475,7 @@ class BOSerWebAPI(BaseWebAPI):
async def disable_hashboards( async def disable_hashboards(
self, self,
hashboard_ids: List[str], hashboard_ids: List[str],
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"disable_hashboards", "disable_hashboards",
@@ -485,7 +488,7 @@ class BOSerWebAPI(BaseWebAPI):
async def set_pool_groups( async def set_pool_groups(
self, self,
pool_groups: List[PoolGroupConfiguration], pool_groups: List[PoolGroupConfiguration],
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_pool_groups", "set_pool_groups",

View File

@@ -4,12 +4,17 @@
# This file has been @generated # This file has been @generated
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, Optional from typing import (
TYPE_CHECKING,
Dict,
Optional,
)
import betterproto import betterproto
import grpclib import grpclib
from betterproto.grpc.grpclib_server import ServiceBase from betterproto.grpc.grpclib_server import ServiceBase
if TYPE_CHECKING: if TYPE_CHECKING:
import grpclib.server import grpclib.server
from betterproto.grpc.grpclib_client import MetadataLike from betterproto.grpc.grpclib_client import MetadataLike
@@ -18,7 +23,7 @@ if TYPE_CHECKING:
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class ApiVersion(betterproto.Message): class ApiVersion(betterproto.Message):
"""LATEST_API_VERSION=1.2.0""" """LATEST_API_VERSION=1.3.0"""
major: int = betterproto.uint64_field(1) major: int = betterproto.uint64_field(1)
minor: int = betterproto.uint64_field(2) minor: int = betterproto.uint64_field(2)

View File

@@ -5,12 +5,19 @@
import warnings import warnings
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, AsyncIterator, Dict, List, Optional from typing import (
TYPE_CHECKING,
AsyncIterator,
Dict,
List,
Optional,
)
import betterproto import betterproto
import grpclib import grpclib
from betterproto.grpc.grpclib_server import ServiceBase from betterproto.grpc.grpclib_server import ServiceBase
if TYPE_CHECKING: if TYPE_CHECKING:
import grpclib.server import grpclib.server
from betterproto.grpc.grpclib_client import MetadataLike from betterproto.grpc.grpclib_client import MetadataLike
@@ -20,125 +27,129 @@ if TYPE_CHECKING:
class SaveAction(betterproto.Enum): class SaveAction(betterproto.Enum):
"""Save action for different operations""" """Save action for different operations"""
SAVE_ACTION_UNSPECIFIED = 0 UNSPECIFIED = 0
SAVE_ACTION_SAVE = 1 SAVE = 1
SAVE_ACTION_SAVE_AND_APPLY = 2 SAVE_AND_APPLY = 2
SAVE_ACTION_SAVE_AND_FORCE_APPLY = 3 SAVE_AND_FORCE_APPLY = 3
class CoolingMode(betterproto.Enum): class CoolingMode(betterproto.Enum):
COOLING_MODE_UNSPECIFIED = 0 UNSPECIFIED = 0
COOLING_MODE_AUTO = 1 AUTO = 1
COOLING_MODE_MANUAL = 2 MANUAL = 2
COOLING_MODE_DISABLED = 3 DISABLED = 3
class SensorLocation(betterproto.Enum): class SensorLocation(betterproto.Enum):
SENSOR_LOCATION_UNSPECIFIED = 0 UNSPECIFIED = 0
SENSOR_LOCATION_CHIP = 1 CHIP = 1
SENSOR_LOCATION_PCB = 2 PCB = 2
class TunerMode(betterproto.Enum): class TunerMode(betterproto.Enum):
TUNER_MODE_UNSPECIFIED = 0 UNSPECIFIED = 0
TUNER_MODE_POWER_TARGET = 1 POWER_TARGET = 1
TUNER_MODE_HASHRATE_TARGET = 2 HASHRATE_TARGET = 2
class TunerState(betterproto.Enum): class TunerState(betterproto.Enum):
TUNER_STATE_UNSPECIFIED = 0 UNSPECIFIED = 0
TUNER_STATE_DISABLED = 1 DISABLED = 1
TUNER_STATE_STABLE = 2 STABLE = 2
TUNER_STATE_TUNING = 3 TUNING = 3
TUNER_STATE_ERROR = 4 ERROR = 4
class LicenseType(betterproto.Enum): class LicenseType(betterproto.Enum):
LICENSE_TYPE_UNSPECIFIED = 0 UNSPECIFIED = 0
LICENSE_TYPE_STANDARD = 1 STANDARD = 1
LICENSE_TYPE_CUSTOM = 2 CUSTOM = 2
class Platform(betterproto.Enum): class Platform(betterproto.Enum):
"""Supported platforms""" """Supported platforms"""
PLATFORM_UNSPECIFIED = 0 UNSPECIFIED = 0
PLATFORM_AM1_S9 = 1 AM1_S9 = 1
PLATFORM_AM2_S17 = 2 AM2_S17 = 2
PLATFORM_AM3_BBB = 3 AM3_BBB = 3
PLATFORM_AM3_AML = 4 AM3_AML = 4
PLATFORM_STM32MP157C_II1_AM2 = 5 STM32MP157C_II1_AM2 = 5
PLATFORM_CVITEK_BM1_AM2 = 6 CVITEK_BM1_AM2 = 6
PLATFORM_ZYNQ_BM3_AM2 = 7 ZYNQ_BM3_AM2 = 7
STM32MP157C_II2_BMM1 = 8
class BosMode(betterproto.Enum): class BosMode(betterproto.Enum):
"""BOS modes enumeration""" """BOS modes enumeration"""
BOS_MODE_UNSPECIFIED = 0 UNSPECIFIED = 0
BOS_MODE_UPGRADE = 1 UPGRADE = 1
BOS_MODE_RECOVERY = 2 RECOVERY = 2
BOS_MODE_SD = 3 SD = 3
BOS_MODE_NAND = 4 NAND = 4
BOS_MODE_EMMC = 5 EMMC = 5
class MinerBrand(betterproto.Enum): class MinerBrand(betterproto.Enum):
MINER_BRAND_UNSPECIFIED = 0 UNSPECIFIED = 0
MINER_BRAND_ANTMINER = 1 ANTMINER = 1
MINER_BRAND_WHATSMINER = 2 WHATSMINER = 2
class MinerModel(betterproto.Enum): class MinerModel(betterproto.Enum):
"""Deprecated: This enumeration is not longer maintained""" """Deprecated: This enumeration is not longer maintained"""
MINER_MODEL_UNSPECIFIED = 0 UNSPECIFIED = 0
MINER_MODEL_ANTMINER_S9 = 1 ANTMINER_S9 = 1
MINER_MODEL_ANTMINER_X17 = 2 ANTMINER_X17 = 2
MINER_MODEL_ANTMINER_S17 = 3 ANTMINER_S17 = 3
MINER_MODEL_ANTMINER_S17_PLUS = 4 ANTMINER_S17_PLUS = 4
MINER_MODEL_ANTMINER_S17_PRO = 5 ANTMINER_S17_PRO = 5
MINER_MODEL_ANTMINER_S17E = 6 ANTMINER_S17E = 6
MINER_MODEL_ANTMINER_T17 = 7 ANTMINER_T17 = 7
MINER_MODEL_ANTMINER_T17E = 8 ANTMINER_T17E = 8
MINER_MODEL_ANTMINER_T17_PLUS = 9 ANTMINER_T17_PLUS = 9
MINER_MODEL_ANTMINER_X19 = 10 ANTMINER_X19 = 10
MINER_MODEL_ANTMINER_S19 = 11 ANTMINER_S19 = 11
MINER_MODEL_ANTMINER_S19_PRO = 12 ANTMINER_S19_PRO = 12
MINER_MODEL_ANTMINER_S19_PLUS = 13 ANTMINER_S19_PLUS = 13
MINER_MODEL_ANTMINER_S19J = 14 ANTMINER_S19J = 14
MINER_MODEL_ANTMINER_S19J_PRO = 15 ANTMINER_S19J_PRO = 15
MINER_MODEL_ANTMINER_S19A = 16 ANTMINER_S19A = 16
MINER_MODEL_ANTMINER_S19A_PRO = 17 ANTMINER_S19A_PRO = 17
MINER_MODEL_ANTMINER_S19XP = 18 ANTMINER_S19XP = 18
MINER_MODEL_ANTMINER_T19 = 19 ANTMINER_T19 = 19
MINER_MODEL_ANTMINER_S19J_PRO_PLUS = 20 ANTMINER_S19J_PRO_PLUS = 20
class MinerStatus(betterproto.Enum): class MinerStatus(betterproto.Enum):
MINER_STATUS_UNSPECIFIED = 0 UNSPECIFIED = 0
MINER_STATUS_NOT_STARTED = 1 NOT_STARTED = 1
MINER_STATUS_NORMAL = 2 NORMAL = 2
MINER_STATUS_PAUSED = 3 PAUSED = 3
MINER_STATUS_SUSPENDED = 4 SUSPENDED = 4
MINER_STATUS_RESTRICTED = 5 RESTRICTED = 5
class SupportArchiveFormat(betterproto.Enum): class SupportArchiveFormat(betterproto.Enum):
"""Enumeration for support archive format""" """Enumeration for support archive format"""
SUPPORT_ARCHIVE_FORMAT_UNSPECIFIED = 0 UNSPECIFIED = 0
SUPPORT_ARCHIVE_FORMAT_ZIP = 1 ZIP = 1
"""Compressed zip format""" """Compressed zip format"""
SUPPORT_ARCHIVE_FORMAT_BOS = 2 BOS = 2
"""BOS custom format""" """BOS custom format"""
ZIP_ENCRYPTED = 3
"""Compressed encrypted zip format"""
class NetworkProtocol(betterproto.Enum): class NetworkProtocol(betterproto.Enum):
NETWORK_PROTOCOL_UNSPECIFIED = 0 UNSPECIFIED = 0
NETWORK_PROTOCOL_DHCP = 1 DHCP = 1
NETWORK_PROTOCOL_STATIC = 2 STATIC = 2
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
@@ -268,8 +279,8 @@ class LoginResponse(betterproto.Message):
timeout_s: int = betterproto.uint32_field(2) timeout_s: int = betterproto.uint32_field(2)
""" """
Authentication token validity/timeout in seconds. Token validity refreshed Authentication token validity/timeout in seconds.
to this value with each request. Token validity refreshed to this value with each request.
""" """
@@ -277,9 +288,7 @@ class LoginResponse(betterproto.Message):
class SetPasswordRequest(betterproto.Message): class SetPasswordRequest(betterproto.Message):
"""Request for set password action.""" """Request for set password action."""
password: Optional[str] = betterproto.string_field( password: Optional[str] = betterproto.string_field(1, optional=True)
1, optional=True, group="_password"
)
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
@@ -335,8 +344,8 @@ class BasesPoints(betterproto.Message):
bsp: int = betterproto.uint32_field(1) bsp: int = betterproto.uint32_field(1)
""" """
A basis point is one hundredth of 1 percentage point. For example: 1bps = A basis point is one hundredth of 1 percentage point.
0.01%, 250bps = 2.5% For example: 1bps = 0.01%, 250bps = 2.5%
""" """
@@ -409,9 +418,9 @@ class VoltageConstraints(betterproto.Message):
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class CoolingAutoMode(betterproto.Message): class CoolingAutoMode(betterproto.Message):
""" """
The temperature control modes. Miner software tries to regulate the fan The temperature control modes.
speed so that miner temperature is approximately at the target temperature. Miner software tries to regulate the fan speed so that miner temperature is approximately at the target temperature.
The allowed temperature range is 0-200 degree Celsius. The allowed temperature range is 0-200 degree Celsius.
""" """
target_temperature: "Temperature" = betterproto.message_field(1) target_temperature: "Temperature" = betterproto.message_field(1)
@@ -422,8 +431,7 @@ class CoolingAutoMode(betterproto.Message):
dangerous_temperature: "Temperature" = betterproto.message_field(3) dangerous_temperature: "Temperature" = betterproto.message_field(3)
""" """
Temperature threshold at which BOSMiner shuts down in order to prevent Temperature threshold at which BOSMiner shuts down in order to prevent overheating and damaging the miner.
overheating and damaging the miner.
""" """
@@ -433,12 +441,11 @@ class CoolingManualMode(betterproto.Message):
Fans are kept at a fixed, user-defined speed, no matter the temperature. Fans are kept at a fixed, user-defined speed, no matter the temperature.
""" """
fan_speed_ratio: Optional[float] = betterproto.double_field( fan_speed_ratio: Optional[float] = betterproto.double_field(1, optional=True)
1, optional=True, group="_fan_speed_ratio"
)
""" """
User defined fan speed expressed as a ratio between 0.0 and 1.0 where 0.0 User defined fan speed expressed as a ratio between 0.0 and 1.0
means completely turned off and 1.0 means running at full speed possible where 0.0 means completely turned off and
1.0 means running at full speed possible
""" """
hot_temperature: "Temperature" = betterproto.message_field(2) hot_temperature: "Temperature" = betterproto.message_field(2)
@@ -446,8 +453,7 @@ class CoolingManualMode(betterproto.Message):
dangerous_temperature: "Temperature" = betterproto.message_field(3) dangerous_temperature: "Temperature" = betterproto.message_field(3)
""" """
Temperature threshold at which BOSMiner shuts down in order to prevent Temperature threshold at which BOSMiner shuts down in order to prevent overheating and damaging the miner.
overheating and damaging the miner.
""" """
@@ -455,20 +461,17 @@ class CoolingManualMode(betterproto.Message):
class CoolingDisabledMode(betterproto.Message): class CoolingDisabledMode(betterproto.Message):
"""Disable temperature control. May be dangerous.""" """Disable temperature control. May be dangerous."""
fan_speed_ratio: Optional[float] = betterproto.double_field( fan_speed_ratio: Optional[float] = betterproto.double_field(1, optional=True)
1, optional=True, group="_fan_speed_ratio"
)
""" """
User defined fan speed expressed as a ratio between 0.0 and 1.0 where 0.0 User defined fan speed expressed as a ratio between 0.0 and 1.0
means completely turned off and 1.0 means running at full speed possible where 0.0 means completely turned off and
1.0 means running at full speed possible
""" """
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class CoolingConfiguration(betterproto.Message): class CoolingConfiguration(betterproto.Message):
minimum_required_fans: Optional[int] = betterproto.uint32_field( minimum_required_fans: Optional[int] = betterproto.uint32_field(1, optional=True)
1, optional=True, group="_minimum_required_fans"
)
auto: "CoolingAutoMode" = betterproto.message_field(2, group="mode") auto: "CoolingAutoMode" = betterproto.message_field(2, group="mode")
manual: "CoolingManualMode" = betterproto.message_field(3, group="mode") manual: "CoolingManualMode" = betterproto.message_field(3, group="mode")
disabled: "CoolingDisabledMode" = betterproto.message_field(4, group="mode") disabled: "CoolingDisabledMode" = betterproto.message_field(4, group="mode")
@@ -488,23 +491,19 @@ class CoolingConstraints(betterproto.Message):
class FanState(betterproto.Message): class FanState(betterproto.Message):
"""Structure which contain info about one specific miner fan.""" """Structure which contain info about one specific miner fan."""
position: Optional[int] = betterproto.uint32_field( position: Optional[int] = betterproto.uint32_field(1, optional=True)
1, optional=True, group="_position"
)
"""Fan positions/ID""" """Fan positions/ID"""
rpm: int = betterproto.uint32_field(2) rpm: int = betterproto.uint32_field(2)
"""Actual fan RPM (Revolutions/Rotation Per Minute)""" """Actual fan RPM (Revolutions/Rotation Per Minute)"""
target_speed_ratio: Optional[float] = betterproto.double_field( target_speed_ratio: Optional[float] = betterproto.double_field(3, optional=True)
3, optional=True, group="_target_speed_ratio"
)
"""Actual fan speed ratio(PWM) in range 0.0 - 1.0""" """Actual fan speed ratio(PWM) in range 0.0 - 1.0"""
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class TemperatureSensor(betterproto.Message): class TemperatureSensor(betterproto.Message):
id: Optional[int] = betterproto.uint32_field(1, optional=True, group="_id") id: Optional[int] = betterproto.uint32_field(1, optional=True)
"""Sensor id""" """Sensor id"""
location: "SensorLocation" = betterproto.enum_field(2) location: "SensorLocation" = betterproto.enum_field(2)
@@ -523,7 +522,10 @@ class GetCoolingStateRequest(betterproto.Message):
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class GetCoolingStateResponse(betterproto.Message): class GetCoolingStateResponse(betterproto.Message):
"""Response to get current fan states and temperature measurements""" """
Response to get current fan states and
temperature measurements
"""
fans: List["FanState"] = betterproto.message_field(1) fans: List["FanState"] = betterproto.message_field(1)
"""All Fans state""" """All Fans state"""
@@ -551,12 +553,10 @@ class SetImmersionModeResponse(betterproto.Message):
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class TunerConfiguration(betterproto.Message): class TunerConfiguration(betterproto.Message):
enabled: Optional[bool] = betterproto.bool_field(1, optional=True, group="_enabled") enabled: Optional[bool] = betterproto.bool_field(1, optional=True)
"""Flag if tuner is enabled""" """Flag if tuner is enabled"""
tuner_mode: Optional["TunerMode"] = betterproto.enum_field( tuner_mode: Optional["TunerMode"] = betterproto.enum_field(2, optional=True)
2, optional=True, group="_tuner_mode"
)
"""Tuner mode""" """Tuner mode"""
power_target: "Power" = betterproto.message_field(3) power_target: "Power" = betterproto.message_field(3)
@@ -583,7 +583,7 @@ class TunerConstraints(betterproto.Message):
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class DpsConfiguration(betterproto.Message): class DpsConfiguration(betterproto.Message):
enabled: Optional[bool] = betterproto.bool_field(1, optional=True, group="_enabled") enabled: Optional[bool] = betterproto.bool_field(1, optional=True)
"""Flag if Dynamic Performance Scaling is enabled""" """Flag if Dynamic Performance Scaling is enabled"""
power_step: "Power" = betterproto.message_field(2) power_step: "Power" = betterproto.message_field(2)
@@ -598,9 +598,7 @@ class DpsConfiguration(betterproto.Message):
min_hashrate_target: "TeraHashrate" = betterproto.message_field(5) min_hashrate_target: "TeraHashrate" = betterproto.message_field(5)
"""Dynamic Performance Scaling minimal hashrate target""" """Dynamic Performance Scaling minimal hashrate target"""
shutdown_enabled: Optional[bool] = betterproto.bool_field( shutdown_enabled: Optional[bool] = betterproto.bool_field(6, optional=True)
6, optional=True, group="_shutdown_enabled"
)
"""Flag if shutdown for Dynamic Performance Scaling is enabled""" """Flag if shutdown for Dynamic Performance Scaling is enabled"""
shutdown_duration: "Hours" = betterproto.message_field(7) shutdown_duration: "Hours" = betterproto.message_field(7)
@@ -879,17 +877,13 @@ class SetDpsRequest(betterproto.Message):
save_action: "SaveAction" = betterproto.enum_field(1) save_action: "SaveAction" = betterproto.enum_field(1)
"""Save action""" """Save action"""
enable: Optional[bool] = betterproto.bool_field(2, optional=True, group="_enable") enable: Optional[bool] = betterproto.bool_field(2, optional=True)
"""Flag if Dynamic Performance Scaling should be enabled""" """Flag if Dynamic Performance Scaling should be enabled"""
enable_shutdown: Optional[bool] = betterproto.bool_field( enable_shutdown: Optional[bool] = betterproto.bool_field(3, optional=True)
3, optional=True, group="_enable_shutdown"
)
"""Flag if shutdown for Dynamic Performance Scaling should be enabled""" """Flag if shutdown for Dynamic Performance Scaling should be enabled"""
shutdown_duration: Optional["Hours"] = betterproto.message_field( shutdown_duration: Optional["Hours"] = betterproto.message_field(4, optional=True)
4, optional=True, group="_shutdown_duration"
)
"""Dynamic Performance Scaling shutdown duration""" """Dynamic Performance Scaling shutdown duration"""
target: "DpsTarget" = betterproto.message_field(5) target: "DpsTarget" = betterproto.message_field(5)
@@ -898,17 +892,13 @@ class SetDpsRequest(betterproto.Message):
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class SetDpsResponse(betterproto.Message): class SetDpsResponse(betterproto.Message):
enabled: Optional[bool] = betterproto.bool_field(1, optional=True, group="_enabled") enabled: Optional[bool] = betterproto.bool_field(1, optional=True)
"""Flag if Dynamic Performance Scaling is enabled""" """Flag if Dynamic Performance Scaling is enabled"""
shutdown_enabled: Optional[bool] = betterproto.bool_field( shutdown_enabled: Optional[bool] = betterproto.bool_field(2, optional=True)
2, optional=True, group="_shutdown_enabled"
)
"""Flag if shutdown for Dynamic Performance Scaling should be enabled""" """Flag if shutdown for Dynamic Performance Scaling should be enabled"""
shutdown_duration: Optional["Hours"] = betterproto.message_field( shutdown_duration: Optional["Hours"] = betterproto.message_field(3, optional=True)
3, optional=True, group="_shutdown_duration"
)
"""Dynamic Performance Scaling shutdown duration""" """Dynamic Performance Scaling shutdown duration"""
power_target: "DpsPowerTarget" = betterproto.message_field(4) power_target: "DpsPowerTarget" = betterproto.message_field(4)
@@ -935,7 +925,7 @@ class HashboardConfig(betterproto.Message):
id: str = betterproto.string_field(1) id: str = betterproto.string_field(1)
"""Hashboard id""" """Hashboard id"""
enabled: Optional[bool] = betterproto.bool_field(2, optional=True, group="_enabled") enabled: Optional[bool] = betterproto.bool_field(2, optional=True)
"""Flag if HB si enabled""" """Flag if HB si enabled"""
frequency: "Frequency" = betterproto.message_field(3) frequency: "Frequency" = betterproto.message_field(3)
@@ -1019,9 +1009,9 @@ class Quota(betterproto.Message):
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class FixedShareRatio(betterproto.Message): class FixedShareRatio(betterproto.Message):
""" """
Structure for fixed share ratio load balance strategy Fixed share ratio is Structure for fixed share ratio load balance strategy
value between 0.0 to 1.0 where 1.0 represents that all work is generated Fixed share ratio is value between 0.0 to 1.0 where 1.0 represents that all work is
from the group generated from the group
""" """
value: float = betterproto.double_field(1) value: float = betterproto.double_field(1)
@@ -1058,12 +1048,10 @@ class PoolConfiguration(betterproto.Message):
user: str = betterproto.string_field(3) user: str = betterproto.string_field(3)
"""Pool connection user""" """Pool connection user"""
password: Optional[str] = betterproto.string_field( password: Optional[str] = betterproto.string_field(4, optional=True)
4, optional=True, group="_password"
)
"""Pool connection password if set""" """Pool connection password if set"""
enabled: Optional[bool] = betterproto.bool_field(5, optional=True, group="_enabled") enabled: Optional[bool] = betterproto.bool_field(5, optional=True)
"""Flag if pool connection is enabled""" """Flag if pool connection is enabled"""
@@ -1130,9 +1118,7 @@ class PoolStats(betterproto.Message):
generated_work: int = betterproto.uint64_field(6) generated_work: int = betterproto.uint64_field(6)
"""Generated work""" """Generated work"""
last_share_time: Optional[datetime] = betterproto.message_field( last_share_time: Optional[datetime] = betterproto.message_field(7, optional=True)
7, optional=True, group="_last_share_time"
)
"""Last share time""" """Last share time"""
@@ -1154,9 +1140,9 @@ class GetPoolGroupsResponse(betterproto.Message):
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class CreatePoolGroupRequest(betterproto.Message): class CreatePoolGroupRequest(betterproto.Message):
""" """
Request for pool group create action group.uid must not be specified (it Request for pool group create action
will be generated) group.pools[].uid must not be specified (it will be group.uid must not be specified (it will be generated)
generated) group.pools[].uid must not be specified (it will be generated)
""" """
save_action: "SaveAction" = betterproto.enum_field(1) save_action: "SaveAction" = betterproto.enum_field(1)
@@ -1177,9 +1163,9 @@ class CreatePoolGroupResponse(betterproto.Message):
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class UpdatePoolGroupRequest(betterproto.Message): class UpdatePoolGroupRequest(betterproto.Message):
""" """
Request for pool group update action group.uid must be specified and Request for pool group update action
represents unique id of group which will be updated group.pools[].uid must group.uid must be specified and represents unique id of group which will be updated
not be specified (it will be generated) group.pools[].uid must not be specified (it will be generated)
""" """
save_action: "SaveAction" = betterproto.enum_field(1) save_action: "SaveAction" = betterproto.enum_field(1)
@@ -1221,16 +1207,15 @@ class SetPoolGroupsRequest(betterproto.Message):
save_action: "SaveAction" = betterproto.enum_field(1) save_action: "SaveAction" = betterproto.enum_field(1)
""" """
Save action SAVE just update config but changes will not be applied Save action
SAVE_AND_APPLY and SAVE_AND_FORCE_APPLY are equal for this method. Pools SAVE just update config but changes will not be applied
config will be updated and changes will be applied that will trigger SAVE_AND_APPLY and SAVE_AND_FORCE_APPLY are equal for this method. Pools config will be updated and changes will be applied that will trigger restart.
restart.
""" """
pool_groups: List["PoolGroupConfiguration"] = betterproto.message_field(2) pool_groups: List["PoolGroupConfiguration"] = betterproto.message_field(2)
""" """
Pool groups configuration `uid` must not be specified (it will be Pool groups configuration
generated) `uid` must not be specified (it will be generated)
""" """
@@ -1292,9 +1277,8 @@ class NoneLicense(betterproto.Message):
time_to_restricted: int = betterproto.uint32_field(1) time_to_restricted: int = betterproto.uint32_field(1)
""" """
BOS Initialization timeout - number of seconds elapsed since bosminer start BOS Initialization timeout - number of seconds elapsed since bosminer start
i.e., number of seconds BOS will start mining in restricted mode burning 5% i.e., number of seconds BOS will start mining in restricted mode burning 5% of hashrate
of hashrate For more, see Section 3.10 of For more, see Section 3.10 of https://braiins.com/os/plus/license
https://braiins.com/os/plus/license
""" """
@@ -1313,9 +1297,8 @@ class ValidLicense(betterproto.Message):
time_to_restricted: int = betterproto.uint32_field(3) time_to_restricted: int = betterproto.uint32_field(3)
""" """
Current license expiration - number of seconds since the moment the license Current license expiration - number of seconds since the moment the license was received
was received i.e., number of seconds BOS will start mining in restricted i.e., number of seconds BOS will start mining in restricted mode burning 15% of hashrate
mode burning 15% of hashrate
""" """
dev_fee: "BasesPoints" = betterproto.message_field(4) dev_fee: "BasesPoints" = betterproto.message_field(4)
@@ -1388,7 +1371,8 @@ class MinerIdentity(betterproto.Message):
brand: "MinerBrand" = betterproto.enum_field(1) brand: "MinerBrand" = betterproto.enum_field(1)
model: "MinerModel" = betterproto.enum_field(2) model: "MinerModel" = betterproto.enum_field(2)
""" """
Deprecated: Use miner_model instead. This field is no longer supported. Deprecated: Use miner_model instead.
This field is no longer supported.
""" """
name: str = betterproto.string_field(3) name: str = betterproto.string_field(3)
@@ -1564,7 +1548,7 @@ class Hashboard(betterproto.Message):
stats: "WorkSolverStats" = betterproto.message_field(8) stats: "WorkSolverStats" = betterproto.message_field(8)
"""Hashboard stats""" """Hashboard stats"""
model: Optional[str] = betterproto.string_field(9, optional=True, group="_model") model: Optional[str] = betterproto.string_field(9, optional=True)
"""Hashboard model""" """Hashboard model"""
@@ -1644,9 +1628,7 @@ class GetNetworkConfigurationResponse(betterproto.Message):
class SetNetworkConfigurationRequest(betterproto.Message): class SetNetworkConfigurationRequest(betterproto.Message):
dhcp: "Dhcp" = betterproto.message_field(1, group="protocol") dhcp: "Dhcp" = betterproto.message_field(1, group="protocol")
static: "Static" = betterproto.message_field(2, group="protocol") static: "Static" = betterproto.message_field(2, group="protocol")
hostname: Optional[str] = betterproto.string_field( hostname: Optional[str] = betterproto.string_field(3, optional=True)
3, optional=True, group="_hostname"
)
"""Hostname. Existing value will be preserved if this field is not set.""" """Hostname. Existing value will be preserved if this field is not set."""
@@ -1691,27 +1673,21 @@ class GetNetworkInfoRequest(betterproto.Message):
@dataclass(eq=False, repr=False) @dataclass(eq=False, repr=False)
class GetNetworkInfoResponse(betterproto.Message): class GetNetworkInfoResponse(betterproto.Message):
""" """
Response message for GetCurrentNetworkConfiguration Represents the current Response message for GetCurrentNetworkConfiguration
network configuration for the default network interface. Only IPv4 is Represents the current network configuration for the default network interface.
supported. Only IPv4 is supported.
""" """
name: str = betterproto.string_field(1) name: str = betterproto.string_field(1)
"""Name of the network interface""" """Name of the network interface"""
mac_address: Optional[str] = betterproto.string_field( mac_address: Optional[str] = betterproto.string_field(2, optional=True)
2, optional=True, group="_mac_address"
)
"""MAC address of the network interface""" """MAC address of the network interface"""
hostname: Optional[str] = betterproto.string_field( hostname: Optional[str] = betterproto.string_field(3, optional=True)
3, optional=True, group="_hostname"
)
"""Miner hostname""" """Miner hostname"""
protocol: Optional["NetworkProtocol"] = betterproto.enum_field( protocol: Optional["NetworkProtocol"] = betterproto.enum_field(4, optional=True)
4, optional=True, group="_protocol"
)
"""Network protocol""" """Network protocol"""
dns_servers: List[str] = betterproto.string_field(5) dns_servers: List[str] = betterproto.string_field(5)
@@ -1720,9 +1696,7 @@ class GetNetworkInfoResponse(betterproto.Message):
networks: List["IpNetwork"] = betterproto.message_field(6) networks: List["IpNetwork"] = betterproto.message_field(6)
"""List of assigned IP addresses""" """List of assigned IP addresses"""
default_gateway: Optional[str] = betterproto.string_field( default_gateway: Optional[str] = betterproto.string_field(7, optional=True)
7, optional=True, group="_default_gateway"
)
"""Default gateway/route for the interface""" """Default gateway/route for the interface"""
@@ -2332,7 +2306,7 @@ class MinerServiceStub(betterproto.ServiceStub):
timeout: Optional[float] = None, timeout: Optional[float] = None,
deadline: Optional["Deadline"] = None, deadline: Optional["Deadline"] = None,
metadata: Optional["MetadataLike"] = None metadata: Optional["MetadataLike"] = None
) -> AsyncIterator["GetMinerStatusResponse"]: ) -> AsyncIterator[GetMinerStatusResponse]:
async for response in self._unary_stream( async for response in self._unary_stream(
"/braiins.bos.v1.MinerService/GetMinerStatus", "/braiins.bos.v1.MinerService/GetMinerStatus",
get_miner_status_request, get_miner_status_request,
@@ -2418,7 +2392,7 @@ class MinerServiceStub(betterproto.ServiceStub):
timeout: Optional[float] = None, timeout: Optional[float] = None,
deadline: Optional["Deadline"] = None, deadline: Optional["Deadline"] = None,
metadata: Optional["MetadataLike"] = None metadata: Optional["MetadataLike"] = None
) -> AsyncIterator["GetSupportArchiveResponse"]: ) -> AsyncIterator[GetSupportArchiveResponse]:
async for response in self._unary_stream( async for response in self._unary_stream(
"/braiins.bos.v1.MinerService/GetSupportArchive", "/braiins.bos.v1.MinerService/GetSupportArchive",
get_support_archive_request, get_support_archive_request,
@@ -3195,7 +3169,7 @@ class MinerServiceBase(ServiceBase):
async def get_miner_status( async def get_miner_status(
self, get_miner_status_request: "GetMinerStatusRequest" self, get_miner_status_request: "GetMinerStatusRequest"
) -> AsyncIterator["GetMinerStatusResponse"]: ) -> AsyncIterator[GetMinerStatusResponse]:
raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)
yield GetMinerStatusResponse() yield GetMinerStatusResponse()
@@ -3221,7 +3195,7 @@ class MinerServiceBase(ServiceBase):
async def get_support_archive( async def get_support_archive(
self, get_support_archive_request: "GetSupportArchiveRequest" self, get_support_archive_request: "GetSupportArchiveRequest"
) -> AsyncIterator["GetSupportArchiveResponse"]: ) -> AsyncIterator[GetSupportArchiveResponse]:
raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)
yield GetSupportArchiveResponse() yield GetSupportArchiveResponse()

77
pyasic/web/iceriver.py Normal file
View File

@@ -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")

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pyasic" name = "pyasic"
version = "0.59.5" version = "0.61.3"
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"
@@ -8,21 +8,21 @@ documentation = "https://pyasic.readthedocs.io/en/latest/"
readme = "README.md" readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.9"
httpx = ">=0.26.0" httpx = ">=0.26.0"
asyncssh = ">=2.14.2" asyncssh = ">=2.17.0"
passlib = ">=1.7.4" passlib = ">=1.7.4"
pyaml = ">=23.12.0" pyaml = ">=23.12.0"
tomli = { version = ">=2.0.1", python = "<3.11" } tomli = { version = ">=2.0.1", python = "<3.11" }
tomli-w = "1.0.0" tomli-w = "^1.0.0"
betterproto = ">=2.0.0b6" betterproto = "2.0.0b7"
aiofiles = ">=23.2.1" aiofiles = ">=23.2.1"
[tool.poetry.group.dev] [tool.poetry.group.dev]
optional = true optional = true
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pre-commit = "^3.5.0" pre-commit = "^4.0.1"
isort = "^5.12.0" isort = "^5.12.0"
[tool.poetry.group.docs] [tool.poetry.group.docs]