Compare commits

...

7 Commits

Author SHA1 Message Date
UpstreamData
fbd73881d4 version: bump version number. 2023-11-28 16:42:15 -07:00
JP Compagnone
68c4dadb63 hotfix: fix epic api error handling (#74)
* hotfix: fix epic api error handling

* much cleaner way to handle the retry
2023-11-28 16:39:57 -07:00
UpstreamData
87016670d4 version: bump version number. 2023-11-28 10:50:56 -07:00
JP Compagnone
8701bbe4e2 Feature - Add initial ePIC UMC support (#71)
* prelim support of ePIC UMC

* slowly adding things

* add most api calls

* add some guards

* fix post commands

* remove print

* catch when API returns error

* missing guard

* remove semicolon

* recommended changes

* add docs and changes

* respect ignore_errors
2023-11-28 10:49:49 -07:00
UpstreamData
7d1f125b0b docs: update settings docs to list. 2023-11-27 11:07:43 -07:00
UpstreamData
e433902bd5 docs: update settings docs. 2023-11-27 10:57:13 -07:00
UpstreamData
a653772968 docs: update network docs. 2023-11-27 10:53:40 -07:00
16 changed files with 599 additions and 7 deletions

View File

@@ -31,6 +31,8 @@ def backend_str(backend: MinerTypes) -> str:
return "Stock Firmware Avalonminers"
case MinerTypes.VNISH:
return "Vnish Firmware Miners"
case MinerTypes.EPIC:
return "ePIC Firmware Miners"
case MinerTypes.BRAIINS_OS:
return "BOS+ Firmware Miners"
case MinerTypes.HIVEON:

View File

@@ -310,5 +310,6 @@ Here are of all the settings, and their default values:
"default_antminer_password": "root",
"default_bosminer_password": "root",
"default_vnish_password": "admin",
"default_epic_password": "letmein",
"default_goldshell_password": "123456789",
```

View File

@@ -197,3 +197,38 @@
show_root_heading: false
heading_level: 4
## S19 (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19j
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 XP (ePIC)
::: pyasic.miners.antminer.epic.X19.S19.ePICS19XP
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,8 @@
# pyasic
## ePIC Backend
::: pyasic.miners.backends.epic.ePIC
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -445,6 +445,21 @@ details {
</ul>
</details>
<details>
<summary>ePIC Firmware Miners:</summary>
<ul>
<details>
<summary>X19 Series:</summary>
<ul>
<li><a href="../antminer/X19#s19-epic">S19 (ePIC)</a></li>
<li><a href="../antminer/X19#s19-pro-epic">S19 Pro (ePIC)</a></li>
<li><a href="../antminer/X19#s19j-epic">S19j (ePIC)</a></li>
<li><a href="../antminer/X19#s19j-pro-epic">S19j Pro (ePIC)</a></li>
<li><a href="../antminer/X19#s19-xp-epic">S19 XP (ePIC)</a></li>
</ul>
</details>
</ul>
</details>
<details>
<summary>HiveOS Firmware Miners:</summary>
<ul>
<details>

View File

@@ -1,11 +1,23 @@
# pyasic
## settings
::: pyasic.settings
handler: python
options:
show_root_heading: false
heading_level: 4
All settings here are global settings for all of pyasic. Set these settings with `update(key, value)`.
Settings options:
- `network_ping_retries`
- `network_ping_timeout`
- `network_scan_threads`
- `factory_get_retries`
- `factory_get_timeout`
- `get_data_retries`
- `api_function_timeout`
- `default_whatsminer_password`
- `default_innosilicon_password`
- `default_antminer_password`
- `default_bosminer_password`
- `default_vnish_password`
- `default_goldshell_password`
- `socket_linger_time`
### get

View File

@@ -7,7 +7,6 @@ nav:
- Miner Factory: "miners/miner_factory.md"
- Network:
- Miner Network: "network/miner_network.md"
- Miner Network Range: "network/miner_network_range.md"
- Dataclasses:
- Miner Data: "data/miner_data.md"
- Error Codes: "data/error_codes.md"
@@ -30,6 +29,7 @@ nav:
- CGMiner: "miners/backends/cgminer.md"
- LUXMiner: "miners/backends/luxminer.md"
- VNish: "miners/backends/vnish.md"
- ePIC: "miners/backends/epic.md"
- Hiveon: "miners/backends/hiveon.md"
- Classes:
- Antminer X3: "miners/antminer/X3.md"

View File

@@ -20,3 +20,4 @@ from .cgminer import *
from .hiveon import *
from .luxos import *
from .vnish import *
from .epic import *

View File

@@ -0,0 +1,40 @@
# ------------------------------------------------------------------------------
# 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 ePIC
from pyasic.miners.types import (
S19,
S19Pro,
S19j,
S19jPro,
S19XP,
)
class ePICS19(ePIC, S19):
pass
class ePICS19Pro(ePIC, S19Pro):
pass
class ePICS19j(ePIC, S19j):
pass
class ePICS19jPro(ePIC, S19jPro):
pass
class ePICS19XP(ePIC, S19XP):
pass

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 .S19 import (
ePICS19,
ePICS19Pro,
ePICS19j,
ePICS19jPro,
ePICS19XP,
)

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .X19 import *

View File

@@ -24,4 +24,5 @@ from .cgminer_avalon import CGMinerAvalon
from .hiveon import Hiveon
from .luxminer import LUXMiner
from .vnish import VNish
from .epic import ePIC
from .whatsminer import M2X, M3X, M5X

View File

@@ -0,0 +1,302 @@
# ------------------------------------------------------------------------------
# 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 typing import Optional
from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.miners.backends.bmminer import BMMiner
from pyasic.web.epic import ePICWebAPI
from pyasic.data import Fan, HashBoard
from typing import List, Optional, Tuple, Union
from pyasic.data.error_codes import MinerErrorData, X19Error
EPIC_DATA_LOC = {
"mac": {"cmd": "get_mac", "kwargs": {"web_summary": {"web": "network"}}},
"model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}},
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"web_summary": {"web": "summary"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {"web_summary": {"web": "summary"}}},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"web_summary": {"web": "summary"}}},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"kwargs": {"web_summary": {"web": "summary"}},
},
"hashboards": {"cmd": "get_hashboards", "kwargs": {"web_summary": {"web": "summary"}, "web_hashrate": {"web": "hashrate"}}},
"env_temp": {"cmd": "get_env_temp", "kwargs": {}},
"wattage": {"cmd": "get_wattage", "kwargs": {"web_summary": {"web": "summary"}}},
"fans": {"cmd": "get_fans", "kwargs": {"web_summary": {"web": "summary"}}},
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {"web_summary": {"web": "summary"}}},
"pools": {"cmd": "get_pools", "kwargs": {"web_summary": {"web": "summary"}}},
"is_mining": {"cmd": "is_mining", "kwargs": {}},
"uptime": {"cmd": "get_uptime", "kwargs": {"web_summary": {"web": "summary"}}},
"errors": {"cmd": "get_errors", "kwargs": {"web_summary": {"web": "summary"}}},
}
class ePIC(BMMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver)
# interfaces
self.web = ePICWebAPI(ip)
# static data
self.api_type = "ePIC"
# data gathering locations
self.data_locations = EPIC_DATA_LOC
async def get_model(self) -> Optional[str]:
if self.model is not None:
return self.model + " (ePIC)"
return "? (ePIC)"
async def restart_backend(self) -> bool:
data = await self.web.restart_epic()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def stop_mining(self) -> bool:
data = await self.web.stop_mining()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def resume_mining(self) -> bool:
data = await self.web.resume_mining()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def reboot(self) -> bool:
data = await self.web.reboot()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def get_mac(self, web_summary: dict = None) -> str:
if not web_summary:
web_summary = await self.web.network()
if web_summary:
try:
for network in web_summary:
mac = web_summary[network]["mac_address"]
return mac
except KeyError:
pass
async def get_hostname(self, web_summary: dict = None) -> str:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
hostname = web_summary["Hostname"]
return hostname
except KeyError:
pass
async def get_wattage(self, web_summary: dict = None) -> Optional[int]:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
wattage = web_summary["Power Supply Stats"]["Input Power"]
wattage = round(wattage)
return wattage
except KeyError:
pass
async def get_hashrate(self, web_summary: dict = None) -> Optional[float]:
# get hr from API
if not web_summary:
try:
web_summary = await self.web.summary()
except APIError:
pass
if web_summary:
try:
hashrate = 0
if web_summary["HBs"] != None:
for hb in web_summary["HBs"]:
hashrate += hb["Hashrate"][0]
return round(
float(float(hashrate/ 1000000)), 2)
except (LookupError, ValueError, TypeError) as e:
logger.error(e)
pass
async def get_nominal_hashrate(self, web_summary: dict = None) -> Optional[float]:
# get hr from API
if not web_summary:
try:
web_summary = await self.web.summary()
except APIError:
pass
if web_summary:
try:
hashrate = 0
if web_summary["HBs"] != None:
for hb in web_summary["HBs"]:
if hb["Hashrate"][1] == 0:
ideal = 1.0
else:
ideal = hb["Hashrate"][1]/100
hashrate += hb["Hashrate"][0]/ideal
return round(
float(float(hashrate/ 1000000)), 2)
except (IndexError, KeyError, ValueError, TypeError) as e:
logger.error(e)
pass
async def get_fw_ver(self, web_summary: dict = None) -> Optional[str]:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
fw_ver = web_summary["Software"]
fw_ver = fw_ver.split(" ")[1].replace("v", "")
return fw_ver
except KeyError:
pass
async def get_fans(self, web_summary: dict = None) -> List[Fan]:
if not web_summary:
try:
web_summary = await self.web.summary()
except APIError:
pass
fans = []
if web_summary:
for fan in web_summary["Fans Rpm"]:
try:
fans.append(Fan(web_summary["Fans Rpm"][fan]))
except (LookupError, ValueError, TypeError):
fans.append(Fan())
return fans
async def get_hashboards(self, web_summary: dict = None, web_hashrate: dict= None) -> List[HashBoard]:
if not web_summary:
try:
web_summary = await self.web.summary()
except APIError:
pass
if not web_hashrate:
try:
web_hashrate = await self.web.hashrate()
except APIError:
pass
hb_list = [HashBoard(slot=i, expected_chips=self.nominal_chips) for i in range(self.ideal_hashboards)]
if web_summary["HBs"] != None:
for hb in web_summary["HBs"]:
for hr in web_hashrate:
if hr["Index"] == hb["Index"]:
num_of_chips = len(hr["Data"])
hashrate = hb["Hashrate"][0]
#Update the Hashboard object
hb_list[hr["Index"]].expected_chips = num_of_chips
hb_list[hr["Index"]].missing = False
hb_list[hr["Index"]].hashrate = round(hashrate/1000000,2)
hb_list[hr["Index"]].chips = num_of_chips
hb_list[hr["Index"]].temp = hb["Temperature"]
return hb_list
async def is_mining(self, *args, **kwargs) -> Optional[bool]:
return None
async def get_pools(self, web_summary: dict = None) -> List[dict]:
groups = []
if not web_summary:
try:
web_summary = await self.api.summary()
except APIError:
pass
if web_summary:
try:
pools = {}
for i, pool in enumerate(web_summary["StratumConfigs"]):
pools[f"pool_{i + 1}_url"] = (
pool["pool"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["login"]
pools["quota"] = pool["Quota"] if pool.get("Quota") else "0"
groups.append(pools)
except KeyError:
pass
return groups
async def get_uptime(self, web_summary: dict = None) -> Optional[int]:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
uptime = web_summary["Session"]["Uptime"]
return uptime
except KeyError:
pass
return None
async def get_fault_light(self, web_summary: dict = None) -> bool:
if not web_summary:
web_summary = await self.web.summary()
if web_summary:
try:
light = web_summary["Misc"]["Locate Miner State"]
return light
except KeyError:
pass
return False
async def get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
if not web_summary:
web_summary = await self.web.summary()
errors = []
if web_summary:
try:
error = web_summary["Status"]["Last Error"]
if error != None:
errors.append(X19Error(str(error)))
return errors
except KeyError:
pass
return errors

View File

@@ -37,6 +37,7 @@ from pyasic.miners.backends import (
Hiveon,
LUXMiner,
VNish,
ePIC,
)
from pyasic.miners.base import AnyMiner
from pyasic.miners.goldshell import *
@@ -55,6 +56,7 @@ class MinerTypes(enum.Enum):
VNISH = 6
HIVEON = 7
LUX_OS = 8
EPIC = 9
MINER_CLASSES = {
@@ -346,6 +348,14 @@ MINER_CLASSES = {
"ANTMINER S19A PRO": VNishS19aPro,
"ANTMINER T19": VNishT19,
},
MinerTypes.EPIC: {
None: ePIC,
"ANTMINER S19": ePICS19,
"ANTMINER S19 PRO": ePICS19Pro,
"ANTMINER S19J": ePICS19j,
"ANTMINER S19J PRO": ePICS19jPro,
"ANTMINER S19 XP": ePICS19XP,
},
MinerTypes.HIVEON: {
None: Hiveon,
"ANTMINER T9": HiveonT9,
@@ -437,6 +447,7 @@ class MinerFactory:
MinerTypes.GOLDSHELL: self.get_miner_model_goldshell,
MinerTypes.BRAIINS_OS: self.get_miner_model_braiins_os,
MinerTypes.VNISH: self.get_miner_model_vnish,
MinerTypes.EPIC: self.get_miner_model_epic,
MinerTypes.HIVEON: self.get_miner_model_hiveon,
MinerTypes.LUX_OS: self.get_miner_model_luxos,
}
@@ -525,6 +536,8 @@ class MinerFactory:
return MinerTypes.GOLDSHELL
if "AnthillOS" in web_text:
return MinerTypes.VNISH
if "Miner Web Dashboard" in web_text:
return MinerTypes.EPIC
if "Avalon" in web_text:
return MinerTypes.AVALONMINER
if "DragonMint" in web_text:
@@ -877,6 +890,14 @@ class MinerFactory:
return miner_model
except (TypeError, LookupError):
pass
async def get_miner_model_epic(self, ip: str) -> Optional[str]:
sock_json_data = await self.send_web_command(ip, ":4028/capabilities")
try:
miner_model = sock_json_data["Model"]
return miner_model
except (TypeError, LookupError):
pass
async def get_miner_model_hiveon(self, ip: str) -> Optional[str]:
sock_json_data = await self.send_api_command(ip, "version")

114
pyasic/web/epic.py Normal file
View File

@@ -0,0 +1,114 @@
# ------------------------------------------------------------------------------
# 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. -
# ------------------------------------------------------------------------------
import json
import warnings
from typing import Union
import httpx
from pyasic import settings
from pyasic.web import BaseWebAPI
from pyasic.errors import APIError, APIWarning
class ePICWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.username = "root"
self.pwd = settings.get("default_epic_password", "letmein")
self.token = None
async def send_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
allow_warning: bool = True,
post: bool = False,
**parameters: Union[str, int, bool],
) -> dict:
if post or parameters != {}:
post = True
async with httpx.AsyncClient(transport=settings.transport()) as client:
for i in range(settings.get("get_data_retries", 1) + 1):
try:
if post:
epic_param = {"param": parameters.get("parameters"),
"password": self.pwd}
response = await client.post(
f"http://{self.ip}:4028/{command}",
timeout=5,
json=epic_param,
)
else:
response = await client.get(
f"http://{self.ip}:4028/{command}",
timeout=5,
)
if not response.status_code == 200:
continue
json_data = response.json()
if json_data:
# The API can return a fail status if the miner cannot return the requested data. Catch this and pass
if "result" in json_data and json_data["result"] is False and not post:
if not i > settings.get("get_data_retries", 1):
continue
if not ignore_errors:
raise APIError(json_data["error"])
return json_data
return {"success": True}
except (httpx.HTTPError, json.JSONDecodeError, AttributeError):
pass
async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict:
data = {k: None for k in commands}
data["multicommand"] = True
for command in commands:
data[command] = await self.send_command(command)
return data
async def restart_epic(self) -> dict:
return await self.send_command("softreboot", post=True, parameters=None)
async def reboot(self) -> dict:
return await self.send_command("reboot", post=True, parameters=None)
async def pause_mining(self) -> dict:
return await self.send_command("miner", post=True, parameters="Stop")
async def resume_mining(self) -> dict:
return await self.send_command("miner", post=True, parameters="Autostart")
async def stop_mining(self) -> dict:
return await self.send_command("miner", post=True, parameters="Stop")
async def start_mining(self) -> dict:
return await self.send_command("miner", post=True, parameters="Autostart")
async def summary(self):
return await self.send_command("summary")
async def hashrate(self):
return await self.send_command("hashrate")
async def network(self):
return await self.send_command("network")
async def capabilities(self):
return await self.send_command("capabilities")

View File

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