Compare commits

...

72 Commits

Author SHA1 Message Date
Upstream Data
689b34611e version: bump version number. 2024-05-14 08:48:38 -06:00
Brett Rowan
d55c3f45ef Merge pull request #143 from jpcomps/master
feat: add additional ePIC UMC features, add Output Voltage to MinerData
2024-05-14 08:47:00 -06:00
John-Paul Compagnone
5ac533616f missed chiptune 2024-05-14 10:46:26 -04:00
John-Paul Compagnone
96ea5f5d16 remove passing self 2024-05-14 10:41:18 -04:00
John-Paul Compagnone
87526f5efc change target as well to get 2024-05-13 23:54:41 -04:00
John-Paul Compagnone
d31bafbc0e less lines 2024-05-13 23:51:23 -04:00
John-Paul Compagnone
66bae47bb9 seperate these out 2024-05-13 21:26:15 -04:00
John-Paul Compagnone
7a09b66d4e simplify 2024-05-13 21:22:31 -04:00
John-Paul Compagnone
de5932184f use get and cheap way to do full backwards compat 2024-05-13 21:17:48 -04:00
John-Paul Compagnone
2cba62e050 push self into secondary level 2024-05-13 21:03:26 -04:00
John-Paul Compagnone
c7520d98e0 better descriptions 2024-05-13 17:00:02 -04:00
John-Paul Compagnone
92e9f7bc08 fix typo 2024-05-13 16:57:19 -04:00
John-Paul Compagnone
a65c4ba215 feat: add voltage param to minerdata 2024-05-13 15:44:12 -04:00
John-Paul Compagnone
4cd0c3357b remove prints 2024-05-12 16:05:08 -04:00
John-Paul Compagnone
d5cabf8af5 fix: fix send_config for epic, add throttle step and min throttle for Vopt 2024-05-12 14:08:12 -04:00
JP Compagnone
3120de757d Merge branch 'UpstreamData:master' into master 2024-05-12 13:20:50 -04:00
Upstream Data
0b69fe591e version: bump version number. 2024-05-10 13:34:00 -06:00
Upstream Data
032288d062 bug: fix k pro identification on latest versions. 2024-05-10 13:33:42 -06:00
Brett Rowan
5f67b987a0 Merge pull request #139 from UpstreamData/snyk-fix-77dddc60686385a842810025bed37669
[Snyk] Security upgrade jinja2 from 3.1.2 to 3.1.4
2024-05-09 12:26:13 -06:00
snyk-bot
5842b3c4aa fix: docs/requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-JINJA2-6809379
2024-05-08 20:27:28 +00:00
Upstream Data
e2e1d2f2fd version: bump version number. 2024-05-06 14:43:19 -06:00
Upstream Data
dd205c0f06 feature: use semaphore for scanning. 2024-05-06 14:43:03 -06:00
Upstream Data
79e247c0cf version: bump version number. 2024-05-03 11:19:50 -06:00
Upstream Data
836d045b65 bug: fix bosminer sometimes not being able to set config due to temp settings not being set. 2024-05-03 11:19:33 -06:00
Upstream Data
2d029b65e6 version: bump version number. 2024-05-03 08:31:39 -06:00
Upstream Data
993b7efeef bug: fix bosminer.toml writing to use only ssh, no sftp. 2024-05-03 08:30:51 -06:00
Upstream Data
b7a81097a4 version: bump version number. 2024-04-30 15:50:37 -06:00
Upstream Data
651bef8203 bug: round mara fw wattage. 2024-04-30 14:43:35 -06:00
Upstream Data
a9e5c99ab2 bug: fix mara fw expected hashrate scaling. 2024-04-30 14:42:43 -06:00
Upstream Data
30216fdd5b bug: fix mara fw hashrate scaling. 2024-04-30 14:32:43 -06:00
Upstream Data
341cc13d83 bug: fix no firmware type for BOSer. 2024-04-30 13:12:01 -06:00
Upstream Data
05a4ae6f04 bug: reformat, and fix miner listener. 2024-04-30 11:50:21 -06:00
Brett Rowan
5971d9fd83 Merge pull request #125 from UpstreamData/dev_marafw
feature: Add maraFW support.
2024-04-30 10:01:35 -06:00
Brett Rowan
3968f2275c Merge branch 'master' into dev_marafw 2024-04-30 10:01:28 -06:00
Upstream Data
84344ca883 feature: make mara discover more consistent. 2024-04-30 09:11:23 -06:00
1e9abhi1e10
59b80254eb remove trailing whitespace 2024-04-30 09:11:23 -06:00
1e9abhi1e10
06fdb19e0b Add tess in config_tests 2024-04-30 09:11:23 -06:00
Upstream Data
75222e8cd2 docs: update docs generation to fix marathon miners. 2024-04-30 09:11:23 -06:00
Upstream Data
5a067d60e7 bug: remove some excepts that were catching propagated cancellations. 2024-04-30 09:11:21 -06:00
Upstream Data
2fbc4fcb4a version: bump version number. 2024-04-30 09:11:18 -06:00
Upstream Data
13fe60504a bug: fix bad naming of S19J Pro Plus NOPIC. 2024-04-30 09:11:18 -06:00
Upstream Data
cdc52c3605 docs: update docs. 2024-04-30 09:11:17 -06:00
Upstream Data
a9db097355 version: bump version number. 2024-04-30 09:11:17 -06:00
Upstream Data
ec5557cf88 feature: Add support for S19jPro+ No PIC. 2024-04-30 09:11:17 -06:00
Upstream Data
a8ea84d2f3 version: bump version number. 2024-04-30 09:11:17 -06:00
Upstream Data
58d369eedf bug: fix disabling fans when using immersion mode. 2024-04-30 09:11:17 -06:00
Abhishek Patidar
f8f9dd7070 Add tests in rpc_tests (#128) 2024-04-30 09:11:17 -06:00
Upstream Data
01b72e1ee6 version: bump version number. 2024-04-30 09:11:16 -06:00
Upstream Data
e367e630b8 bug: fix bosminer fan control for 2.0 versions. 2024-04-30 09:11:16 -06:00
upstreamdata
385cca6fc0 docs: update index and readme. 2024-04-30 09:11:16 -06:00
upstreamdata
75a3a466a3 docs: update documentation to include new web docstrings. 2024-04-30 09:11:16 -06:00
Harsh Singh Rawat
4bf08dbfe6 docs: add docstring for antminer and auradine in web (#127)
* docs: add docstring for antminer and auradine in web

Signed-off-by: PAINxNAGATO <harshrawat349@gmail.com>

* Remove Trailing whitespace from the docstring

Signed-off-by: PAINxNAGATO <harshrawat349@gmail.com>

* Remove Trailing whitespace from the docstring

Signed-off-by: PAINxNAGATO <harshrawat349@gmail.com>

* Remove Trailing whitespace from the docstring of the fucntion multicommand

Signed-off-by: PAINxNAGATO <harshrawat349@gmail.com>

---------

Signed-off-by: PAINxNAGATO <harshrawat349@gmail.com>
2024-04-30 09:11:16 -06:00
Abhishek Patidar
833d061315 docs: Add documentation for config (#121)
* docs: Add documentation for `config`

* Fix styling

* Add documentation for classmethods

* fixes some docstring
2024-04-30 09:11:16 -06:00
Abhishek Patidar
a85558278d docs: Add documentation for ssh (#126) 2024-04-30 09:11:15 -06:00
Upstream Data
bf5087b06d Merge remote-tracking branch 'origin/master' 2024-04-29 09:14:59 -06:00
Upstream Data
ec58d13bae docs: update docs generation to fix marathon miners. 2024-04-29 09:14:47 -06:00
Brett Rowan
75993564ab Merge pull request #131 from 1e9abhi1e10/config_tests 2024-04-27 12:16:30 -06:00
1e9abhi1e10
f3ea169dec remove trailing whitespace 2024-04-27 15:47:08 +05:30
1e9abhi1e10
39b3fe5c25 Add tess in config_tests 2024-04-27 15:40:03 +05:30
John-Paul Compagnone
3b2b586420 add new critical temp api, add board serial no 2024-04-26 19:27:19 -04:00
Upstream Data
5c79c6cb0c bug: remove some excepts that were catching propagated cancellations. 2024-04-26 13:00:16 -06:00
Upstream Data
bab4261bed version: bump version number. 2024-04-24 10:39:34 -06:00
Upstream Data
e1d5c89388 bug: fix bad naming of S19J Pro Plus NOPIC. 2024-04-24 10:37:07 -06:00
Upstream Data
5f6c8cca18 docs: update docs. 2024-04-24 10:04:46 -06:00
Upstream Data
3d94e30f22 feature: Add wattage settings, reboot, restart backend, stop mining, and start mining. 2024-04-12 15:20:09 -06:00
Upstream Data
d5a7ff3a46 feature: add wattage limit data and set wattage to mara. 2024-04-12 15:15:23 -06:00
Upstream Data
bbfa97632d feature: add send_config for mara miners. 2024-04-12 14:56:18 -06:00
Upstream Data
ecf0ce22d6 feature: add mara config parsing. 2024-04-12 14:28:22 -06:00
Upstream Data
d56da007a5 feature: make mara discover more consistent. 2024-04-12 13:40:10 -06:00
Upstream Data
2c86b2da7e feature: Add mara fw light functions. 2024-04-12 13:16:55 -06:00
Upstream Data
c73b1ceb07 feature: update mara fw data locations. 2024-04-12 13:09:23 -06:00
Upstream Data
a320c8967d feature: remove some mara support in order to get rid of Antminer function calls. 2024-04-12 10:19:24 -06:00
28 changed files with 939 additions and 294 deletions

View File

@@ -47,8 +47,8 @@ def backend_str(backend: MinerTypes) -> str:
return "Stock Firmware Goldshells"
case MinerTypes.LUX_OS:
return "LuxOS Firmware Miners"
case MinerTypes.EPIC:
return "ePIC Firmware Miners"
case MinerTypes.MARATHON:
return "Mara Firmware Miners"
def create_url_str(mtype: str):

View File

@@ -176,8 +176,8 @@
show_root_heading: false
heading_level: 4
## S19j Pro
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19jPro
## S19j Pro No PIC
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19jProNoPIC
handler: python
options:
show_root_heading: false
@@ -190,6 +190,20 @@
show_root_heading: false
heading_level: 4
## S19j Pro+
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19jProPlus
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro+ No PIC
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19jProPlusNoPIC
handler: python
options:
show_root_heading: false
heading_level: 4
## S19k Pro No PIC
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19kProNoPIC
handler: python
@@ -365,3 +379,52 @@
show_root_heading: false
heading_level: 4
## S19 (MaraFW)
::: pyasic.miners.antminer.marathon.X19.S19.MaraS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro (MaraFW)
::: pyasic.miners.antminer.marathon.X19.S19.MaraS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j (MaraFW)
::: pyasic.miners.antminer.marathon.X19.S19.MaraS19j
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j No PIC (MaraFW)
::: pyasic.miners.antminer.marathon.X19.S19.MaraS19jNoPIC
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro (MaraFW)
::: pyasic.miners.antminer.marathon.X19.S19.MaraS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 XP (MaraFW)
::: pyasic.miners.antminer.marathon.X19.S19.MaraS19XP
handler: python
options:
show_root_heading: false
heading_level: 4
## S19K Pro (MaraFW)
::: pyasic.miners.antminer.marathon.X19.S19.MaraS19KPro
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -8,6 +8,13 @@
show_root_heading: false
heading_level: 4
## T21
::: pyasic.miners.antminer.bmminer.X21.T21.BMMinerT21
handler: python
options:
show_root_heading: false
heading_level: 4
## S21
::: pyasic.miners.antminer.bosminer.X21.S21.BOSMinerS21
handler: python
@@ -22,6 +29,13 @@
show_root_heading: false
heading_level: 4
## T21 (ePIC)
::: pyasic.miners.antminer.epic.X21.T21.ePICT21
handler: python
options:
show_root_heading: false
heading_level: 4
## S21 (LuxOS)
::: pyasic.miners.antminer.luxos.X21.S21.LUXMinerS21
handler: python
@@ -29,3 +43,17 @@
show_root_heading: false
heading_level: 4
## S21 (MaraFW)
::: pyasic.miners.antminer.marathon.X21.S21.MaraS21
handler: python
options:
show_root_heading: false
heading_level: 4
## T21 (MaraFW)
::: pyasic.miners.antminer.marathon.X21.T21.MaraT21
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -89,6 +89,7 @@ details {
<summary>X21 Series:</summary>
<ul>
<li><a href="../antminer/X21#s21">S21</a></li>
<li><a href="../antminer/X21#t21">T21</a></li>
</ul>
</details>
</ul>
@@ -453,8 +454,10 @@ details {
<li><a href="../antminer/X19#s19j">S19j</a></li>
<li><a href="../antminer/X19#s19j-no-pic">S19j No PIC</a></li>
<li><a href="../antminer/X19#s19j-pro">S19j Pro</a></li>
<li><a href="../antminer/X19#s19j-pro">S19j Pro</a></li>
<li><a href="../antminer/X19#s19j-pro-no-pic">S19j Pro No PIC</a></li>
<li><a href="../antminer/X19#s19j-pro_1">S19j Pro+</a></li>
<li><a href="../antminer/X19#s19j-pro_1">S19j Pro+</a></li>
<li><a href="../antminer/X19#s19j-pro_1-no-pic">S19j Pro+ No PIC</a></li>
<li><a href="../antminer/X19#s19k-pro-no-pic">S19k Pro No PIC</a></li>
<li><a href="../antminer/X19#s19-xp">S19 XP</a></li>
<li><a href="../antminer/X19#t19">T19</a></li>
@@ -525,12 +528,14 @@ details {
<summary>X21 Series:</summary>
<ul>
<li><a href="../antminer/X21#s21-epic">S21 (ePIC)</a></li>
<li><a href="../antminer/X21#t21-epic">T21 (ePIC)</a></li>
</ul>
</details>
<details>
<summary>blockminer Series:</summary>
<ul>
<li><a href="../blockminer/blockminer#blockminer-520i-epic">BlockMiner 520i (ePIC)</a></li>
<li><a href="../blockminer/blockminer#blockminer-720i-epic">BlockMiner 720i (ePIC)</a></li>
</ul>
</details>
</ul>
@@ -602,3 +607,27 @@ details {
</details>
</ul>
</details>
<details>
<summary>Mara Firmware Miners:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-marafw">S19 (MaraFW)</a></li>
<li><a href="../antminer/X19#s19-pro-marafw">S19 Pro (MaraFW)</a></li>
<li><a href="../antminer/X19#s19j-marafw">S19j (MaraFW)</a></li>
<li><a href="../antminer/X19#s19j-no-pic-marafw">S19j No PIC (MaraFW)</a></li>
<li><a href="../antminer/X19#s19j-pro-marafw">S19j Pro (MaraFW)</a></li>
<li><a href="../antminer/X19#s19-xp-marafw">S19 XP (MaraFW)</a></li>
<li><a href="../antminer/X19#s19k-pro-marafw">S19K Pro (MaraFW)</a></li>
</ul>
</details>
<details>
<summary>X21 Series:</summary>
<ul>
<li><a href="../antminer/X21#s21-marafw">S21 (MaraFW)</a></li>
<li><a href="../antminer/X21#t21-marafw">T21 (MaraFW)</a></li>
</ul>
</details>
</ul>
</details>

View File

@@ -1,3 +1,3 @@
jinja2<3.1.3
jinja2<3.1.4
mkdocs
mkdocstrings[python]

View File

@@ -27,6 +27,7 @@ from pyasic.misc import merge_dicts
class MinerConfig:
"""Represents the configuration for a miner including pool configuration,
fan mode, temperature settings, mining mode, and power scaling."""
pools: PoolConfig = field(default_factory=PoolConfig.default)
fan_mode: FanModeConfig = field(default_factory=FanModeConfig.default)
temperature: TemperatureConfig = field(default_factory=TemperatureConfig.default)
@@ -138,6 +139,15 @@ class MinerConfig:
**self.power_scaling.as_auradine(),
}
def as_mara(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_mara(),
**self.temperature.as_mara(),
**self.mining_mode.as_mara(),
**self.pools.as_mara(user_suffix=user_suffix),
**self.power_scaling.as_mara(),
}
@classmethod
def from_dict(cls, dict_conf: dict) -> "MinerConfig":
"""Constructs a MinerConfig object from a dictionary."""
@@ -228,3 +238,11 @@ class MinerConfig:
fan_mode=FanModeConfig.from_auradine(web_conf["fan"]),
mining_mode=MiningModeConfig.from_auradine(web_conf["mode"]),
)
@classmethod
def from_mara(cls, web_miner_config: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_mara(web_miner_config),
fan_mode=FanModeConfig.from_mara(web_miner_config),
mining_mode=MiningModeConfig.from_mara(web_miner_config),
)

View File

@@ -57,6 +57,9 @@ class MinerConfigOption(Enum):
def as_auradine(self) -> dict:
return self.value.as_auradine()
def as_mara(self) -> dict:
return self.value.as_mara()
def __call__(self, *args, **kwargs):
return self.value(*args, **kwargs)
@@ -106,3 +109,6 @@ class MinerConfigValue:
def as_auradine(self) -> dict:
return {}
def as_mara(self) -> dict:
return {}

View File

@@ -71,6 +71,15 @@ class FanModeNormal(MinerConfigValue):
}
}
def as_mara(self) -> dict:
return {
"general-config": {"environment-profile": "AirCooling"},
"advance-config": {
"override-fan-control": False,
"fan-fixed-percent": 0,
},
}
@dataclass
class FanModeManual(MinerConfigValue):
@@ -120,6 +129,15 @@ class FanModeManual(MinerConfigValue):
def as_epic(self) -> dict:
return {"fans": {"Manual": {"speed": self.speed}}}
def as_mara(self) -> dict:
return {
"general-config": {"environment-profile": "AirCooling"},
"advance-config": {
"override-fan-control": True,
"fan-fixed-percent": self.speed,
},
}
@dataclass
class FanModeImmersion(MinerConfigValue):
@@ -140,6 +158,9 @@ class FanModeImmersion(MinerConfigValue):
def as_auradine(self) -> dict:
return {"fan": {"percentage": 0}}
def as_mara(self) -> dict:
return {"general-config": {"environment-profile": "OilImmersionCooling"}}
class FanModeConfig(MinerConfigOption):
normal = FanModeNormal
@@ -255,4 +276,18 @@ class FanModeConfig(MinerConfigOption):
fan_1_target = fan_data["Target"]
return cls.manual(speed=round((fan_1_target / fan_1_max) * 100))
except LookupError:
pass
return cls.default()
@classmethod
def from_mara(cls, web_config: dict):
try:
mode = web_config["general-config"]["environment-profile"]
if mode == "AirCooling":
if web_config["advance-config"]["override-fan-control"]:
return cls.manual(web_config["advance-config"]["fan-fixed-percent"])
return cls.normal()
return cls.immersion()
except LookupError:
pass
return cls.default()

View File

@@ -56,6 +56,13 @@ class MiningModeNormal(MinerConfigValue):
def as_goldshell(self) -> dict:
return {"settings": {"level": 0}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Stock",
}
}
@dataclass
class MiningModeSleep(MinerConfigValue):
@@ -82,6 +89,13 @@ class MiningModeSleep(MinerConfigValue):
def as_goldshell(self) -> dict:
return {"settings": {"level": 3}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Sleep",
}
}
@dataclass
class MiningModeLPM(MinerConfigValue):
@@ -142,6 +156,7 @@ class VOptAlgo(MinerConfigValue):
return "VoltageOptimizer"
@dataclass
class ChipTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="chip_tune")
@@ -219,11 +234,24 @@ class MiningModePowerTune(MinerConfigValue):
def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "power", "power": self.power}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Auto",
"concorde": {
"mode-select": "PowerTarget",
"power-target": self.power,
},
}
}
@dataclass
class MiningModeHashrateTune(MinerConfigValue):
mode: str = field(init=False, default="hashrate_tuning")
hashrate: int = None
throttle_limit: int = None
throttle_step: int = None
algo: TunerAlgo = field(default_factory=TunerAlgo.default)
@classmethod
@@ -231,6 +259,10 @@ class MiningModeHashrateTune(MinerConfigValue):
cls_conf = {}
if dict_conf.get("hashrate"):
cls_conf["hashrate"] = dict_conf["hashrate"]
if dict_conf.get("throttle_limit"):
cls_conf["throttle_limit"] = dict_conf["throttle_limit"]
if dict_conf.get("throttle_step"):
cls_conf["throttle_step"] = dict_conf["throttle_step"]
if dict_conf.get("algo"):
cls_conf["algo"] = TunerAlgo.from_dict(dict_conf["algo"])
@@ -267,7 +299,28 @@ class MiningModeHashrateTune(MinerConfigValue):
return {"mode": {"mode": "custom", "tune": "ths", "ths": self.hashrate}}
def as_epic(self) -> dict:
return {"ptune": {"algo": self.algo.as_epic(), "target": self.hashrate}}
mode = {
"ptune": {
"algo": self.algo.as_epic(),
"target": self.hashrate,
}
}
if self.throttle_limit is not None:
mode["ptune"]["min_throttle"] = self.throttle_limit
if self.throttle_step is not None:
mode["ptune"]["throttle_step"] = self.throttle_step
return mode
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Auto",
"concorde": {
"mode-select": "Hashrate",
"hash-target": self.hashrate,
},
}
}
@dataclass
@@ -320,6 +373,17 @@ class MiningModeManual(MinerConfigValue):
}
return cls(global_freq=freq, global_volt=voltage, boards=boards)
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Fixed",
"fixed": {
"frequency": str(self.global_freq),
"voltage": self.global_volt,
},
}
}
class MiningModeConfig(MinerConfigOption):
normal = MiningModeNormal
@@ -369,13 +433,19 @@ class MiningModeConfig(MinerConfigOption):
algo_info = web_conf["PerpetualTune"]["Algorithm"]
if algo_info.get("VoltageOptimizer") is not None:
return cls.hashrate_tuning(
hashrate=algo_info["VoltageOptimizer"]["Target"],
algo=TunerAlgo.voltage_optimizer,
hashrate=algo_info["VoltageOptimizer"].get("Target"),
throttle_limit=algo_info["VoltageOptimizer"].get(
"Min Throttle Target"
),
throttle_step=algo_info["VoltageOptimizer"].get(
"Throttle Step"
),
algo=TunerAlgo.voltage_optimizer(),
)
else:
return cls.hashrate_tuning(
hashrate=algo_info["ChipTune"]["Target"],
algo=TunerAlgo.chip_tune,
algo=TunerAlgo.chip_tune(),
)
else:
return cls.normal()
@@ -468,3 +538,28 @@ class MiningModeConfig(MinerConfigOption):
return cls.power_tuning(mode_data["Power"])
except LookupError:
return cls.default()
@classmethod
def from_mara(cls, web_config: dict):
try:
mode = web_config["mode"]["work-mode-selector"]
if mode == "Fixed":
fixed_conf = web_config["mode"]["fixed"]
return cls.manual(
global_freq=int(fixed_conf["frequency"]),
global_volt=fixed_conf["voltage"],
)
elif mode == "Stock":
return cls.normal()
elif mode == "Sleep":
return cls.sleep()
elif mode == "Auto":
auto_conf = web_config["mode"]["concorde"]
auto_mode = auto_conf["mode-select"]
if auto_mode == "Hashrate":
return cls.hashrate_tuning(hashrate=auto_conf["hash-target"])
elif auto_mode == "PowerTarget":
return cls.power_tuning(power=auto_conf["power-target"])
except LookupError:
pass
return cls.default()

View File

@@ -118,6 +118,15 @@ class Pool(MinerConfigValue):
}
return {"pool": self.url, "login": self.user, "password": self.password}
def as_mara(self, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
"url": self.url,
"user": f"{self.user}{user_suffix}",
"pass": self.password,
}
return {"url": self.url, "user": self.user, "pass": self.password}
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "Pool":
return cls(
@@ -177,6 +186,14 @@ class Pool(MinerConfigValue):
password=grpc_pool["password"],
)
@classmethod
def from_mara(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"],
user=web_pool["user"],
password=web_pool["pass"],
)
@dataclass
class PoolGroup(MinerConfigValue):
@@ -264,9 +281,12 @@ class PoolGroup(MinerConfigValue):
def as_auradine(self, user_suffix: str = None) -> list:
return [p.as_auradine(user_suffix=user_suffix) for p in self.pools]
def as_epic(self, user_suffix: str = None) -> dict:
def as_epic(self, user_suffix: str = None) -> list:
return [p.as_epic(user_suffix=user_suffix) for p in self.pools]
def as_mara(self, user_suffix: str = None) -> list:
return [p.as_mara(user_suffix=user_suffix) for p in self.pools]
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "PoolGroup":
cls_conf = {}
@@ -336,6 +356,10 @@ class PoolGroup(MinerConfigValue):
except LookupError:
return cls()
@classmethod
def from_mara(cls, web_config_pools: dict) -> "PoolGroup":
return cls(pools=[Pool.from_mara(pool_conf) for pool_conf in web_config_pools])
@dataclass
class PoolConfig(MinerConfigValue):
@@ -427,6 +451,11 @@ class PoolConfig(MinerConfigValue):
}
}
def as_mara(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_mara(user_suffix=user_suffix)}
return {"pools": []}
@classmethod
def from_api(cls, api_pools: dict) -> "PoolConfig":
try:
@@ -481,3 +510,7 @@ class PoolConfig(MinerConfigValue):
)
except LookupError:
return cls()
@classmethod
def from_mara(cls, web_config: dict) -> "PoolConfig":
return cls(groups=[PoolGroup.from_mara(web_config["pools"])])

View File

@@ -38,6 +38,8 @@ class TemperatureConfig(MinerConfigValue):
temp_cfg["hot_temp"] = self.hot
if self.danger is not None:
temp_cfg["dangerous_temp"] = self.danger
if len(temp_cfg) == 0:
return {}
return {"temp_control": temp_cfg}
def as_epic(self) -> dict:
@@ -47,7 +49,9 @@ class TemperatureConfig(MinerConfigValue):
else:
temps_config["fans"]["Auto"]["Target Temperature"] = 60
if self.danger is not None:
temps_config["temps"]["shutdown"] = self.danger
temps_config["temps"]["critical"] = self.danger
if self.hot is not None:
temps_config["temps"]["shutdown"] = self.hot
return temps_config
@classmethod
@@ -67,20 +71,25 @@ class TemperatureConfig(MinerConfigValue):
hot=temp_control.get("hot_temp"),
danger=temp_control.get("dangerous_temp"),
)
return cls()
@classmethod
def from_epic(cls, web_conf: dict) -> "TemperatureConfig":
try:
dangerous_temp = web_conf["Misc"]["Shutdown Temp"]
dangerous_temp = web_conf["Misc"]["Critical Temp"]
except KeyError:
dangerous_temp = None
try:
hot_temp = web_conf["Misc"]["Shutdown Temp"]
except KeyError:
hot_temp = None
# Need to do this in two blocks to avoid KeyError if one is missing
try:
target_temp = web_conf["Fans"]["Fan Mode"]["Auto"]["Target Temperature"]
except KeyError:
target_temp = None
return cls(target=target_temp, danger=dangerous_temp)
return cls(target=target_temp, hot=hot_temp, danger=dangerous_temp)
@classmethod
def from_vnish(cls, web_settings: dict) -> "TemperatureConfig":

View File

@@ -50,6 +50,7 @@ class MinerData:
temperature_avg: The average temperature across the boards. Calculated automatically.
env_temp: The environment temps as a float.
wattage: Current power draw of the miner as an int.
voltage: Current output voltage of the PSU as an float.
wattage_limit: Power limit of the miner as an int.
fans: A list of fans on the miner with their speeds.
fan_psu: The speed of the PSU on the fan if the miner collects it.
@@ -84,6 +85,7 @@ class MinerData:
env_temp: float = None
wattage: int = None
wattage_limit: int = field(init=False)
voltage: float = None
_wattage_limit: int = field(repr=False, default=None)
fans: List[Fan] = field(default_factory=list)
fan_psu: int = None

View File

@@ -176,11 +176,14 @@ class BOSMiner(BaseMiner):
self.config = cfg
except toml.TomlDecodeError as e:
raise APIError("Failed to decode toml when getting config.") from e
except TypeError as e:
raise APIError("Failed to decode toml when getting config.") from e
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
self.config = config
print(config)
parsed_cfg = config.as_bosminer(user_suffix=user_suffix)
toml_conf = toml.dumps(
@@ -201,9 +204,7 @@ class BOSMiner(BaseMiner):
async with conn:
await conn.run("/etc/init.d/bosminer stop")
async with conn.start_sftp_client() as sftp:
async with sftp.open("/etc/bosminer.toml", "w+") as file:
await file.write(toml_conf)
await conn.run("echo '" + toml_conf + "' > /etc/bosminer.toml")
await conn.run("/etc/init.d/bosminer start")
async def set_power_limit(self, wattage: int) -> bool:
@@ -642,6 +643,8 @@ class BOSer(BaseMiner):
_web_cls = BOSerWebAPI
web: BOSerWebAPI
firmware = "BOS+"
data_locations = BOSER_DATA_LOC
supports_autotuning = True

View File

@@ -58,6 +58,10 @@ EPIC_DATA_LOC = DataLocations(
"_get_wattage",
[WebAPICommand("web_summary", "summary")],
),
str(DataOptions.VOLTAGE): DataFunction(
"_get_voltage",
[WebAPICommand("web_summary", "summary")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[WebAPICommand("web_summary", "summary")],
@@ -119,6 +123,7 @@ class ePIC(BaseMiner):
# Temps
if not conf.get("temps", {}) == {}:
await self.web.set_shutdown_temp(conf["temps"]["shutdown"])
await self.web.set_critical_temp(conf["temps"]["critical"])
# Fans
# set with sub-keys instead of conf["fans"] because sometimes both can be set
if not conf["fans"].get("Manual", {}) == {}:
@@ -129,7 +134,7 @@ class ePIC(BaseMiner):
# Mining Mode -- Need to handle that you may not be able to change while miner is tuning
if conf["ptune"].get("enabled", True):
await self.web.set_ptune_enable(True)
await self.web.set_ptune_algo(**conf["ptune"])
await self.web.set_ptune_algo(conf["ptune"])
## Pools
await self.web.set_pools(conf["pools"])
@@ -216,6 +221,20 @@ class ePIC(BaseMiner):
except KeyError:
pass
async def _get_voltage(self, web_summary: dict = None) -> Optional[float]:
if web_summary is None:
try:
web_summary = await self.web.summary()
except APIError:
pass
if web_summary is not None:
try:
voltage = web_summary["Power Supply Stats"]["Output Voltage"]
return voltage
except KeyError:
pass
async def _get_hashrate(self, web_summary: dict = None) -> Optional[float]:
if web_summary is None:
try:
@@ -306,9 +325,20 @@ class ePIC(BaseMiner):
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if web_summary is not None and web_capabilities is not None:
if web_summary.get("HBs") is not None:
for hb in web_summary["HBs"]:
num_of_chips = web_capabilities["Performance Estimator"]["Chip Count"]
if web_capabilities.get("Performance Estimator") is not None:
num_of_chips = web_capabilities["Performance Estimator"][
"Chip Count"
]
else:
num_of_chips = self.expected_chips
if web_capabilities.get("Board Serial Numbers") is not None:
if hb["Index"] < len(web_capabilities["Board Serial Numbers"]):
hb_list[hb["Index"]].serial_number = web_capabilities[
"Board Serial Numbers"
][hb["Index"]]
hashrate = hb["Hashrate"][0]
# Update the Hashboard object
hb_list[hb["Index"]].missing = False

View File

@@ -1,71 +1,69 @@
from typing import Optional
from typing import List, Optional
from pyasic import MinerConfig
from pyasic.config import MiningModeConfig
from pyasic.data import Fan, HashBoard
from pyasic.errors import APIError
from pyasic.miners.backends import AntminerModern
from pyasic.miners.data import (
DataFunction,
DataLocations,
DataOptions,
RPCAPICommand,
WebAPICommand,
)
from pyasic.miners.base import BaseMiner
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
from pyasic.misc import merge_dicts
from pyasic.web.marathon import MaraWebAPI
MARA_DATA_LOC = DataLocations(
**{
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_get_system_info", "get_system_info")],
),
str(DataOptions.API_VERSION): DataFunction(
"_get_api_ver",
[RPCAPICommand("rpc_version", "version")],
[WebAPICommand("web_overview", "overview")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[RPCAPICommand("rpc_version", "version")],
[WebAPICommand("web_overview", "overview")],
),
str(DataOptions.HOSTNAME): DataFunction(
"_get_hostname",
[WebAPICommand("web_get_system_info", "get_system_info")],
[WebAPICommand("web_network_config", "network_config")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[RPCAPICommand("rpc_summary", "summary")],
[WebAPICommand("web_brief", "brief")],
),
str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate",
[RPCAPICommand("rpc_stats", "stats")],
[WebAPICommand("web_brief", "brief")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.ERRORS): DataFunction(
"_get_errors",
[WebAPICommand("web_summary", "summary")],
),
str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_fault_light",
[WebAPICommand("web_get_blink_status", "get_blink_status")],
),
str(DataOptions.IS_MINING): DataFunction(
"_is_mining",
[WebAPICommand("web_get_conf", "get_miner_conf")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[RPCAPICommand("rpc_stats", "stats")],
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[WebAPICommand("web_hashboards", "hashboards")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[WebAPICommand("web_brief", "brief")],
),
str(DataOptions.WATTAGE_LIMIT): DataFunction(
"_get_wattage_limit",
[WebAPICommand("web_miner_config", "miner_config")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[WebAPICommand("web_fans", "fans")],
),
str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_fault_light",
[WebAPICommand("web_locate_miner", "locate_miner")],
),
str(DataOptions.IS_MINING): DataFunction(
"_is_mining",
[WebAPICommand("web_brief", "brief")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[WebAPICommand("web_brief", "brief")],
),
}
)
class MaraMiner(AntminerModern):
class MaraMiner(BaseMiner):
_web_cls = MaraWebAPI
web: MaraWebAPI
@@ -73,6 +71,52 @@ class MaraMiner(AntminerModern):
firmware = "MaraFW"
async def fault_light_off(self) -> bool:
res = await self.web.set_locate_miner(blinking=False)
return res.get("blinking") is False
async def fault_light_on(self) -> bool:
res = await self.web.set_locate_miner(blinking=True)
return res.get("blinking") is True
async def get_config(self) -> MinerConfig:
data = await self.web.get_miner_config()
if data:
self.config = MinerConfig.from_mara(data)
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
data = await self.web.get_miner_config()
cfg_data = config.as_mara(user_suffix=user_suffix)
merged_cfg = merge_dicts(data, cfg_data)
await self.web.set_miner_config(**merged_cfg)
async def set_power_limit(self, wattage: int) -> bool:
cfg = await self.get_config()
cfg.mining_mode = MiningModeConfig.power_tuning(wattage)
await self.send_config(cfg)
return True
async def stop_mining(self) -> bool:
data = await self.web.get_miner_config()
data["mode"]["work-mode-selector"] = "Sleep"
await self.web.set_miner_config(**data)
return True
async def resume_mining(self) -> bool:
data = await self.web.get_miner_config()
data["mode"]["work-mode-selector"] = "Auto"
await self.web.set_miner_config(**data)
return True
async def reboot(self) -> bool:
await self.web.reboot()
return True
async def restart_backend(self) -> bool:
await self.web.reload()
return True
async def _get_wattage(self, web_brief: dict = None) -> Optional[int]:
if web_brief is None:
try:
@@ -82,6 +126,173 @@ class MaraMiner(AntminerModern):
if web_brief is not None:
try:
return web_brief["power_consumption_estimated"]
return round(web_brief["power_consumption_estimated"])
except LookupError:
pass
async def _is_mining(self, web_brief: dict = None) -> Optional[bool]:
if web_brief is None:
try:
web_brief = await self.web.brief()
except APIError:
pass
if web_brief is not None:
try:
return web_brief["status"] == "Mining"
except LookupError:
pass
async def _get_uptime(self, web_brief: dict = None) -> Optional[int]:
if web_brief is None:
try:
web_brief = await self.web.brief()
except APIError:
pass
if web_brief is not None:
try:
return web_brief["elapsed"]
except LookupError:
pass
async def _get_hashboards(self, web_hashboards: dict = None) -> List[HashBoard]:
hashboards = [
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if web_hashboards is None:
try:
web_hashboards = await self.web.hashboards()
except APIError:
pass
if web_hashboards is not None:
try:
for hb in web_hashboards["hashboards"]:
idx = hb["index"]
hashboards[idx].hashrate = round(hb["hashrate_average"] / 1000, 2)
hashboards[idx].temp = round(
sum(hb["temperature_pcb"]) / len(hb["temperature_pcb"]), 2
)
hashboards[idx].chip_temp = round(
sum(hb["temperature_chip"]) / len(hb["temperature_chip"]), 2
)
hashboards[idx].chips = hb["asic_num"]
hashboards[idx].serial_number = hb["serial_number"]
hashboards[idx].missing = False
except LookupError:
pass
return hashboards
async def _get_mac(self, web_overview: dict = None) -> Optional[str]:
if web_overview is None:
try:
web_overview = await self.web.overview()
except APIError:
pass
if web_overview is not None:
try:
return web_overview["mac"].upper()
except LookupError:
pass
async def _get_fw_ver(self, web_overview: dict = None) -> Optional[str]:
if web_overview is None:
try:
web_overview = await self.web.overview()
except APIError:
pass
if web_overview is not None:
try:
return web_overview["version_firmware"]
except LookupError:
pass
async def _get_hostname(self, web_network_config: dict = None) -> Optional[str]:
if web_network_config is None:
try:
web_network_config = await self.web.get_network_config()
except APIError:
pass
if web_network_config is not None:
try:
return web_network_config["hostname"]
except LookupError:
pass
async def _get_hashrate(self, web_brief: dict = None) -> Optional[float]:
if web_brief is None:
try:
web_brief = await self.web.brief()
except APIError:
pass
if web_brief is not None:
try:
return round(web_brief["hashrate_realtime"], 2)
except LookupError:
pass
async def _get_fans(self, web_fans: dict = None) -> List[Fan]:
if web_fans is None:
try:
web_fans = await self.web.fans()
except APIError:
pass
if web_fans is not None:
fans = []
for n in range(self.expected_fans):
try:
fans.append(Fan(web_fans["fans"][n]["current_speed"]))
except (IndexError, KeyError):
pass
return fans
return [Fan() for _ in range(self.expected_fans)]
async def _get_fault_light(self, web_locate_miner: dict = None) -> bool:
if web_locate_miner is None:
try:
web_locate_miner = await self.web.get_locate_miner()
except APIError:
pass
if web_locate_miner is not None:
try:
return web_locate_miner["blinking"]
except LookupError:
pass
return False
async def _get_expected_hashrate(self, web_brief: dict = None) -> Optional[float]:
if web_brief is None:
try:
web_brief = await self.web.brief()
except APIError:
pass
if web_brief is not None:
try:
return round(web_brief["hashrate_ideal"] / 1000, 2)
except LookupError:
pass
async def _get_wattage_limit(
self, web_miner_config: dict = None
) -> Optional[float]:
if web_miner_config is None:
try:
web_miner_config = await self.web.get_miner_config()
except APIError:
pass
if web_miner_config is not None:
try:
return web_miner_config["mode"]["concorde"]["power-target"]
except LookupError:
pass

View File

@@ -249,6 +249,14 @@ class MinerProtocol(Protocol):
"""
return await self._get_wattage()
async def get_voltage(self) -> Optional[float]:
"""Get output voltage of the PSU as a float.
Returns:
Output voltage of the PSU as an float.
"""
return await self._get_voltage()
async def get_wattage_limit(self) -> Optional[int]:
"""Get wattage limit from the miner as an int.
@@ -337,6 +345,9 @@ class MinerProtocol(Protocol):
async def _get_wattage(self) -> Optional[int]:
pass
async def _get_voltage(self) -> Optional[float]:
pass
async def _get_wattage_limit(self) -> Optional[int]:
pass

View File

@@ -37,6 +37,7 @@ class DataOptions(Enum):
IS_MINING = "is_mining"
UPTIME = "uptime"
CONFIG = "config"
VOLTAGE = "voltage"
def __str__(self):
return self.value

View File

@@ -364,6 +364,7 @@ MINER_CLASSES = {
"ANTMINER S19J PRO PLUS": BOSMinerS19jProPlus,
"ANTMINER S19J PRO PLUS NOPIC": BOSMinerS19jProPlusNoPIC,
"ANTMINER S19K PRO NOPIC": BOSMinerS19kProNoPIC,
"ANTMINER S19K PRO": BOSMinerS19kProNoPIC,
"ANTMINER S19 XP": BOSMinerS19XP,
"ANTMINER T19": BOSMinerT19,
"ANTMINER S21": BOSMinerS21,
@@ -541,15 +542,11 @@ class MinerFactory:
return await concurrent_get_first_result(tasks, lambda x: x is not None)
async def _get_miner_web(self, ip: str) -> MinerTypes | None:
tasks = []
try:
urls = [f"http://{ip}/", f"https://{ip}/"]
async with httpx.AsyncClient(
transport=settings.transport(verify=False)
) as session:
tasks = [
asyncio.create_task(self._web_ping(session, url)) for url in urls
]
tasks = [asyncio.create_task(self._web_ping(session, url)) for url in urls]
text, resp = await concurrent_get_first_result(
tasks,
@@ -557,14 +554,14 @@ class MinerFactory:
and self._parse_web_type(x[0], x[1]) is not None,
)
if text is not None:
return self._parse_web_type(text, resp)
except asyncio.CancelledError:
for t in tasks:
t.cancel()
try:
await t
except asyncio.CancelledError:
pass
mtype = self._parse_web_type(text, resp)
if mtype == MinerTypes.ANTMINER:
# could still be mara
auth = httpx.DigestAuth("root", "root")
res = await self.send_web_command(ip, "/kaonsu/v1/brief", auth=auth)
if res is not None:
mtype = MinerTypes.MARATHON
return mtype
@staticmethod
async def _web_ping(
@@ -612,12 +609,8 @@ class MinerFactory:
return MinerTypes.AURADINE
async def _get_miner_socket(self, ip: str) -> MinerTypes | None:
tasks = []
try:
commands = ["version", "devdetails"]
tasks = [
asyncio.create_task(self._socket_ping(ip, cmd)) for cmd in commands
]
tasks = [asyncio.create_task(self._socket_ping(ip, cmd)) for cmd in commands]
data = await concurrent_get_first_result(
tasks,
@@ -626,13 +619,6 @@ class MinerFactory:
if data is not None:
d = self._parse_socket_type(data)
return d
except asyncio.CancelledError:
for t in tasks:
t.cancel()
try:
await t
except asyncio.CancelledError:
pass
@staticmethod
async def _socket_ping(ip: str, cmd: str) -> str | None:

View File

@@ -21,22 +21,33 @@ class MinerListenerProtocol(asyncio.Protocol):
def __init__(self):
self.responses = {}
self.transport = None
self.new_miner = None
async def get_new_miner(self):
try:
while self.new_miner is None:
await asyncio.sleep(0)
return self.new_miner
finally:
self.new_miner = None
def connection_made(self, transport):
self.transport = transport
@staticmethod
def datagram_received(data, _addr):
def datagram_received(self, data, _addr):
if data == b"OK\x00\x00\x00\x00\x00\x00\x00\x00":
return
m = data.decode()
if "," in m:
ip, mac = m.split(",")
if "/" in ip:
ip = ip.replace("[", "").split("/")[0]
else:
d = m[:-1].split("MAC")
ip = d[0][3:]
mac = d[1][1:]
new_miner = {"IP": ip, "MAC": mac.upper()}
MinerListener().new_miner = new_miner
self.new_miner = {"IP": ip, "MAC": mac.upper()}
def connection_lost(self, _):
pass
@@ -45,32 +56,32 @@ class MinerListenerProtocol(asyncio.Protocol):
class MinerListener:
def __init__(self, bind_addr: str = "0.0.0.0"):
self.found_miners = []
self.new_miner = None
self.stop = False
self.stop = asyncio.Event()
self.bind_addr = bind_addr
async def listen(self):
self.stop = False
loop = asyncio.get_running_loop()
transport_14235, _ = await loop.create_datagram_endpoint(
transport_14235, protocol_14235 = await loop.create_datagram_endpoint(
MinerListenerProtocol, local_addr=(self.bind_addr, 14235)
)
transport_8888, _ = await loop.create_datagram_endpoint(
transport_8888, protocol_8888 = await loop.create_datagram_endpoint(
MinerListenerProtocol, local_addr=(self.bind_addr, 8888)
)
try:
while not self.stop.is_set():
tasks = [
asyncio.create_task(protocol_14235.get_new_miner()),
asyncio.create_task(protocol_8888.get_new_miner()),
]
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
for t in tasks:
if t.done():
yield t.result()
while True:
if self.new_miner:
yield self.new_miner
self.found_miners.append(self.new_miner)
self.new_miner = None
if self.stop:
finally:
transport_14235.close()
transport_8888.close()
break
await asyncio.sleep(0)
async def cancel(self):
self.stop = True

View File

@@ -32,6 +32,10 @@ class MinerNetwork:
def __init__(self, hosts: List[ipaddress.IPv4Address]):
self.hosts = hosts
semaphore_limit = settings.get("network_scan_semaphore", 255)
if semaphore_limit is None:
semaphore_limit = 255
self.semaphore = asyncio.Semaphore(semaphore_limit)
def __len__(self):
return len(self.hosts)
@@ -153,8 +157,16 @@ class MinerNetwork:
except TimeoutError:
yield None
async def ping_and_get_miner(
self, ip: ipaddress.ip_address
) -> Union[None, AnyMiner]:
if settings.get("network_scan_semaphore") is None:
return await self._ping_and_get_miner(ip)
async with self.semaphore:
return await self._ping_and_get_miner(ip)
@staticmethod
async def ping_and_get_miner(ip: ipaddress.ip_address) -> Union[None, AnyMiner]:
async def _ping_and_get_miner(ip: ipaddress.ip_address) -> Union[None, AnyMiner]:
try:
return await ping_and_get_miner(ip)
except ConnectionRefusedError:

View File

@@ -24,6 +24,7 @@ from httpx import AsyncHTTPTransport
_settings = { # defaults
"network_ping_retries": 1,
"network_ping_timeout": 3,
"network_scan_semaphore": None,
"factory_get_retries": 1,
"factory_get_timeout": 3,
"get_data_retries": 1,
@@ -46,6 +47,7 @@ _settings = { # defaults
ssl_cxt = httpx.create_ssl_context()
# this function configures socket options like SO_LINGER and returns an AsyncHTTPTransport instance to perform asynchronous HTTP requests
# using those options.
# SO_LINGER controls what happens when you close a socket with unsent data - it allows specifying linger time for the data to be sent.

View File

@@ -9,6 +9,7 @@ class AntminerModernSSH(BaseSSH):
Args:
ip (str): The IP address of the Antminer device.
"""
def __init__(self, ip: str):
super().__init__(ip)
self.pwd = settings.get("default_antminer_ssh_password", "root")

View File

@@ -97,6 +97,9 @@ class ePICWebAPI(BaseWebAPI):
async def set_shutdown_temp(self, params: int) -> dict:
return await self.send_command("shutdowntemp", param=params)
async def set_critical_temp(self, params: int) -> dict:
return await self.send_command("criticaltemp", param=params)
async def set_fan(self, params: dict) -> dict:
return await self.send_command("fanspeed", param=params)

View File

@@ -1,30 +1,56 @@
from __future__ import annotations
import asyncio
import json
from typing import Any
import httpx
from pyasic import settings
from pyasic.web.antminer import AntminerModernWebAPI
from pyasic.web.base import BaseWebAPI
class MaraWebAPI(AntminerModernWebAPI):
class MaraWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
self.am_commands = [
"get_miner_conf",
"set_miner_conf",
"blink",
"reboot",
"get_system_info",
"get_network_info",
"summary",
"get_blink_status",
"set_network_conf",
]
super().__init__(ip)
async def _send_mara_command(
async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict:
async with httpx.AsyncClient(transport=settings.transport()) as client:
tasks = [
asyncio.create_task(self._handle_multicommand(client, command))
for command in commands
]
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
data["multicommand"] = True
return data
async def _handle_multicommand(
self, client: httpx.AsyncClient, command: str
) -> dict:
auth = httpx.DigestAuth(self.username, self.pwd)
try:
url = f"http://{self.ip}:{self.port}/kaonsu/v1/{command}"
ret = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if ret.status_code == 200:
try:
json_data = ret.json()
return {command: json_data}
except json.decoder.JSONDecodeError:
pass
return {command: {}}
async def send_command(
self,
command: str | bytes,
ignore_errors: bool = False,
@@ -56,76 +82,66 @@ class MaraWebAPI(AntminerModernWebAPI):
except json.decoder.JSONDecodeError:
pass
async def _send_am_command(
self,
command: str | bytes,
ignore_errors: bool = False,
allow_warning: bool = True,
privileged: bool = False,
**parameters: Any,
):
url = f"http://{self.ip}:{self.port}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.username, self.pwd)
try:
async with httpx.AsyncClient(
transport=settings.transport(),
) as client:
if parameters:
data = await client.post(
url,
auth=auth,
timeout=settings.get("api_function_timeout", 3),
json=parameters,
)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
async def send_command(
self,
command: str | bytes,
ignore_errors: bool = False,
allow_warning: bool = True,
privileged: bool = False,
**parameters: Any,
) -> dict:
if command in self.am_commands:
return await self._send_am_command(
command,
ignore_errors=ignore_errors,
allow_warning=allow_warning,
privileged=privileged,
**parameters,
)
return await self._send_mara_command(
command,
ignore_errors=ignore_errors,
allow_warning=allow_warning,
privileged=privileged,
**parameters,
)
async def brief(self):
return await self.send_command("brief")
async def ping(self):
return await self.send_command("ping")
async def get_locate_miner(self):
return await self.send_command("locate_miner")
async def set_locate_miner(self, blinking: bool):
return await self.send_command("locate_miner", blinking=blinking)
async def reboot(self):
return await self.send_command("maintenance", type="reboot")
async def reset(self):
return await self.send_command("maintenance", type="reset")
async def reload(self):
return await self.send_command("maintenance", type="reload")
async def set_password(self, new_pwd: str):
return await self.send_command(
"maintenance",
type="passwd",
params={"curPwd": self.pwd, "confirmPwd": self.pwd, "newPwd": new_pwd},
)
async def get_network_config(self):
return await self.send_command("network_config")
async def set_network_config(self, **params):
return await self.send_command("network_config", **params)
async def get_miner_config(self):
return await self.send_command("miner_config")
async def set_miner_config(self, **params):
return await self.send_command("miner_config", **params)
async def fans(self):
return await self.send_command("fans")
async def log(self):
return await self.send_command("log")
async def overview(self):
return await self.send_command("overview")
async def connections(self):
return await self.send_command("connections")
async def controlboard_info(self):
return await self.send_command("controlboard_info")
async def event_chart(self):
return await self.send_command("event_chart")
async def hashboards(self):
return await self.send_command("hashboards")
async def mara_pools(self):
return await self._send_mara_command("pools")
async def pools(self):
return await self.send_command("pools")

View File

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

View File

@@ -33,3 +33,43 @@ class TestFanConfig(unittest.TestCase):
conf = fan_mode()
am_conf = conf.as_am_modern()
self.assertEqual(conf, FanModeConfig.from_am_modern(am_conf))
def test_epic_deserialize_and_serialize(self):
for fan_mode in FanModeConfig:
with self.subTest(
msg=f"Test serialization and deserialization of epic fan config",
fan_mode=fan_mode,
):
conf = fan_mode()
epic_conf = conf.as_epic()
self.assertEqual(conf, FanModeConfig.from_epic(epic_conf))
def test_vnish_deserialize_and_serialize(self):
for fan_mode in FanModeConfig:
with self.subTest(
msg=f"Test serialization and deserialization of vnish fan config",
fan_mode=fan_mode,
):
conf = fan_mode()
vnish_conf = conf.as_vnish()
self.assertEqual(conf, FanModeConfig.from_vnish(vnish_conf))
def test_auradine_deserialize_and_serialize(self):
for fan_mode in FanModeConfig:
with self.subTest(
msg=f"Test serialization and deserialization of auradine fan config",
fan_mode=fan_mode,
):
conf = fan_mode()
aur_conf = conf.as_auradine()
self.assertEqual(conf, FanModeConfig.from_auradine(aur_conf))
def test_boser_deserialize_and_serialize(self):
for fan_mode in FanModeConfig:
with self.subTest(
msg=f"Test serialization and deserialization of boser fan config",
fan_mode=fan_mode,
):
conf = fan_mode()
boser_conf = conf.as_boser()
self.assertEqual(conf, FanModeConfig.from_boser(boser_conf))