diff --git a/docs/API/api.md b/docs/API/api.md
index 382a75ec..96a297a4 100644
--- a/docs/API/api.md
+++ b/docs/API/api.md
@@ -2,11 +2,12 @@
## Miner APIs
Each miner has a unique API that is used to communicate with it.
Each of these API types has commands that differ between them, and some commands have data that others do not.
-Each miner that is a subclass of `BaseMiner` should have an API linked to it as `Miner.api`.
+Each miner that is a subclass of [`BaseMiner`][pyasic.miners.BaseMiner] should have an API linked to it as `Miner.api`.
All API implementations inherit from [`BaseMinerAPI`][pyasic.API.BaseMinerAPI], which implements the basic communications protocols.
-BaseMinerAPI should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
+[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
+[`BaseMinerAPI`][pyasic.API.BaseMinerAPI] cannot be instantiated directly, it will raise a `TypeError`.
Use these instead -
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]
diff --git a/docs/data/error_codes.md b/docs/data/error_codes.md
new file mode 100644
index 00000000..d30f0aa5
--- /dev/null
+++ b/docs/data/error_codes.md
@@ -0,0 +1,25 @@
+# pyasic
+
+
+## Whatsminer Error Codes
+::: pyasic.data.error_codes.WhatsminerError
+ handler: python
+ options:
+ show_root_heading: false
+ heading_level: 4
+
+
+## Braiins OS Error Codes
+::: pyasic.data.error_codes.BraiinsOSError
+ handler: python
+ options:
+ show_root_heading: false
+ heading_level: 4
+
+
+## X19 Error Codes
+::: pyasic.data.error_codes.X19Error
+ handler: python
+ options:
+ show_root_heading: false
+ heading_level: 4
diff --git a/docs/miners/base_miner.md b/docs/miners/base_miner.md
new file mode 100644
index 00000000..e2754b87
--- /dev/null
+++ b/docs/miners/base_miner.md
@@ -0,0 +1,10 @@
+# pyasic
+## Base Miner
+[`BaseMiner`][pyasic.miners.BaseMiner] is the basis for all miner classes, they all subclass (usually indirectly) from this class.
+
+You may not instantiate this class on its own, only subclass from it. Trying to instantiate an instance of this class will raise `TypeError`.
+
+::: pyasic.miners.BaseMiner
+ handler: python
+ options:
+ heading_level: 4
diff --git a/docs/miners/miner_factory.md b/docs/miners/miner_factory.md
index 975c84eb..c9c5fb28 100644
--- a/docs/miners/miner_factory.md
+++ b/docs/miners/miner_factory.md
@@ -6,3 +6,15 @@
options:
show_root_heading: false
heading_level: 4
+
+
+## AnyMiner
+::: pyasic.miners.miner_factory.AnyMiner
+ handler: python
+ options:
+ show_root_heading: false
+ heading_level: 4
+
+[`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] is a placeholder type variable used for typing returns of functions.
+A function returning [`AnyMiner`][pyasic.miners.miner_factory.AnyMiner] will always return a subclass of [`BaseMiner`][pyasic.miners.BaseMiner],
+and is used to specify a function returning some arbitrary type of miner class instance.
diff --git a/docs/miners/supported_types.md b/docs/miners/supported_types.md
index 50c077a0..7416b6f5 100644
--- a/docs/miners/supported_types.md
+++ b/docs/miners/supported_types.md
@@ -3,8 +3,6 @@
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
-## Miner List
-
##### pyasic currently supports the following miners and subtypes:
* Braiins OS+ Devices:
* All devices supported by BraiinsOS+ are supported here.
diff --git a/docs/network/miner_network_range.md b/docs/network/miner_network_range.md
index 11ca6b06..18a7a623 100644
--- a/docs/network/miner_network_range.md
+++ b/docs/network/miner_network_range.md
@@ -1,7 +1,7 @@
# pyasic
## Miner Network Range
-`MinerNetworkRange` is a class used by [`MinerNetwork`][pyasic.network.MinerNetwork] to handle any constructor stings.
+[`MinerNetworkRange`][pyasic.network.net_range.MinerNetworkRange] is a class used by [`MinerNetwork`][pyasic.network.MinerNetwork] to handle any constructor stings.
The goal is to emulate what is produced by `ipaddress.ip_network` by allowing [`MinerNetwork`][pyasic.network.MinerNetwork] to get a list of hosts.
This allows this class to be the [`MinerNetwork.network`][pyasic.network.MinerNetwork] and hence be used for scanning.
diff --git a/mkdocs.yml b/mkdocs.yml
index 74371b3c..13604d14 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -28,17 +28,21 @@ nav:
- Miner Network Range: "network/miner_network_range.md"
- Data:
- Miner Data: "data/miner_data.md"
+ - Error Codes: "data/error_codes.md"
- Config:
- Miner Config: "config/miner_config.md"
- Advanced:
- Miner APIs:
- - Base: "API/api.md"
+ - Intro: "API/api.md"
- BMMiner: "API/bmminer.md"
- BOSMiner: "API/bosminer.md"
- BTMiner: "API/btminer.md"
- CGMiner: "API/cgminer.md"
- Unknown: "API/unknown.md"
+ - Base Miner: "miners/base_miner.md"
+
+
plugins:
- mkdocstrings
- search
diff --git a/pyasic/API/__init__.py b/pyasic/API/__init__.py
index 8d600a4c..c85af84b 100644
--- a/pyasic/API/__init__.py
+++ b/pyasic/API/__init__.py
@@ -55,6 +55,11 @@ class BaseMinerAPI:
# ip address of the miner
self.ip = ipaddress.ip_address(ip)
+ def __new__(cls, *args, **kwargs):
+ if cls is BaseMinerAPI:
+ raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
+ return object.__new__(cls)
+
def get_commands(self) -> list:
"""Get a list of command accessible to a specific type of API on the miner.
diff --git a/pyasic/API/btminer.py b/pyasic/API/btminer.py
index 3d3aa42a..e82b33b3 100644
--- a/pyasic/API/btminer.py
+++ b/pyasic/API/btminer.py
@@ -19,6 +19,7 @@ import hashlib
import binascii
import base64
import logging
+from typing import Union
from passlib.handlers.md5_crypt import md5_crypt
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@@ -187,8 +188,8 @@ class BTMinerAPI(BaseMinerAPI):
async def send_command(
self,
- command: str or bytes,
- parameters: str or int or bool = None,
+ command: Union[str, bytes],
+ parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
**kwargs,
) -> dict:
diff --git a/pyasic/config/__init__.py b/pyasic/config/__init__.py
index e5d48357..4c73ef21 100644
--- a/pyasic/config/__init__.py
+++ b/pyasic/config/__init__.py
@@ -287,6 +287,15 @@ class MinerConfig:
self.pool_groups = pool_groups
return self
+ def from_api(self, pools: list):
+ _pools = []
+ for pool in pools:
+ url = pool.get("URL")
+ user = pool.get("User")
+ _pools.append({"url": url, "user": user, "pass": "123"})
+ self.pool_groups = [_PoolGroup().from_dict({"pools": _pools})]
+ return self
+
def from_dict(self, data: dict):
"""Convert an output dict of this class back into usable data and save it to this class.
diff --git a/pyasic/data/__init__.py b/pyasic/data/__init__.py
index 7eef11ef..436b4c0a 100644
--- a/pyasic/data/__init__.py
+++ b/pyasic/data/__init__.py
@@ -12,10 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from typing import Union
+from typing import Union, List
from dataclasses import dataclass, field, asdict
from datetime import datetime
+from .error_codes import X19Error, WhatsminerError, BraiinsOSError
+
@dataclass
class MinerData:
@@ -95,7 +97,9 @@ class MinerData:
pool_1_user: str = "Unknown"
pool_2_url: str = ""
pool_2_user: str = ""
- errors: list = field(default_factory=list)
+ errors: List[Union[WhatsminerError, BraiinsOSError, X19Error]] = field(
+ default_factory=list
+ )
fault_light: Union[bool, None] = None
def __post_init__(self):
diff --git a/pyasic/data/error_codes/X19.py b/pyasic/data/error_codes/X19.py
index 7ec473da..9707cbfa 100644
--- a/pyasic/data/error_codes/X19.py
+++ b/pyasic/data/error_codes/X19.py
@@ -17,7 +17,11 @@ from dataclasses import dataclass, asdict
@dataclass
class X19Error:
- """A Dataclass to handle error codes of X19 miners."""
+ """A Dataclass to handle error codes of X19 miners.
+
+ Attributes:
+ error_message: The error message as a string.
+ """
error_message: str
diff --git a/pyasic/data/error_codes/bos.py b/pyasic/data/error_codes/bos.py
index a9b50991..cdb259a2 100644
--- a/pyasic/data/error_codes/bos.py
+++ b/pyasic/data/error_codes/bos.py
@@ -17,7 +17,11 @@ from dataclasses import dataclass, asdict
@dataclass
class BraiinsOSError:
- """A Dataclass to handle error codes of BraiinsOS+ miners."""
+ """A Dataclass to handle error codes of BraiinsOS+ miners.
+
+ Attributes:
+ error_message: The error message as a string.
+ """
error_message: str
diff --git a/pyasic/data/error_codes/whatsminer.py b/pyasic/data/error_codes/whatsminer.py
index ebe419fd..b7593819 100644
--- a/pyasic/data/error_codes/whatsminer.py
+++ b/pyasic/data/error_codes/whatsminer.py
@@ -17,7 +17,12 @@ from dataclasses import dataclass, field, asdict
@dataclass
class WhatsminerError:
- """A Dataclass to handle error codes of Whatsminers."""
+ """A Dataclass to handle error codes of Whatsminers.
+
+ Attributes:
+ error_code: The error code as an int.
+ error_message: The error message as a string. Automatically found from the error code.
+ """
error_code: int
error_message: str = field(init=False)
diff --git a/pyasic/miners/__init__.py b/pyasic/miners/__init__.py
index ccb758e5..e905677f 100644
--- a/pyasic/miners/__init__.py
+++ b/pyasic/miners/__init__.py
@@ -15,11 +15,13 @@
import asyncssh
import logging
import ipaddress
+from abc import ABC, abstractmethod
from pyasic.data import MinerData
+from pyasic.config import MinerConfig
-class BaseMiner:
+class BaseMiner(ABC):
def __init__(self, *args) -> None:
self.ip = None
self.uname = "root"
@@ -34,6 +36,11 @@ class BaseMiner:
self.fan_count = 2
self.config = None
+ def __new__(cls, *args, **kwargs):
+ if cls is BaseMiner:
+ raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
+ return object.__new__(cls)
+
def __repr__(self):
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
@@ -75,45 +82,56 @@ class BaseMiner:
except Exception as e:
raise e
+ @abstractmethod
async def fault_light_on(self) -> bool:
- return False
+ pass
+ @abstractmethod
async def fault_light_off(self) -> bool:
- return False
+ pass
- async def send_file(self, src, dest):
- async with (await self._get_ssh_connection()) as conn:
- await asyncssh.scp(src, (conn, dest))
+ # async def send_file(self, src, dest):
+ # async with (await self._get_ssh_connection()) as conn:
+ # await asyncssh.scp(src, (conn, dest))
- async def check_light(self):
- return self.light
+ @abstractmethod
+ async def check_light(self) -> bool:
+ pass
+ # @abstractmethod
async def get_board_info(self):
return None
- async def get_config(self):
- return None
+ @abstractmethod
+ async def get_config(self) -> MinerConfig:
+ pass
- async def get_hostname(self):
- return None
+ @abstractmethod
+ async def get_hostname(self) -> str:
+ pass
- async def get_model(self):
- return None
+ @abstractmethod
+ async def get_model(self) -> str:
+ pass
- async def reboot(self):
- return False
+ @abstractmethod
+ async def reboot(self) -> bool:
+ pass
- async def restart_backend(self):
- return False
+ @abstractmethod
+ async def restart_backend(self) -> bool:
+ pass
async def send_config(self, *args, **kwargs):
return None
- async def get_mac(self):
- return None
+ @abstractmethod
+ async def get_mac(self) -> str:
+ pass
- async def get_errors(self):
- return None
+ @abstractmethod
+ async def get_errors(self) -> list:
+ pass
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))
diff --git a/pyasic/miners/_backends/bmminer.py b/pyasic/miners/_backends/bmminer.py
index 6334734c..d571bb8c 100644
--- a/pyasic/miners/_backends/bmminer.py
+++ b/pyasic/miners/_backends/bmminer.py
@@ -154,6 +154,26 @@ class BMMiner(BaseMiner):
return True
return False
+ async def check_light(self) -> bool:
+ if not self.light:
+ self.light = False
+ return self.light
+
+ async def fault_light_off(self) -> bool:
+ return False
+
+ async def fault_light_on(self) -> bool:
+ return False
+
+ async def get_errors(self) -> list:
+ return []
+
+ async def get_mac(self) -> str:
+ return "00:00:00:00:00:00"
+
+ async def restart_backend(self) -> bool:
+ return False
+
async def get_data(self) -> MinerData:
"""Get data from the miner.
diff --git a/pyasic/miners/_backends/bosminer.py b/pyasic/miners/_backends/bosminer.py
index 938ea8c8..c80147a6 100644
--- a/pyasic/miners/_backends/bosminer.py
+++ b/pyasic/miners/_backends/bosminer.py
@@ -240,6 +240,49 @@ class BOSMiner(BaseMiner):
logging.debug(f"{self}: Restarting BOSMiner")
await conn.run("/etc/init.d/bosminer start")
+ async def check_light(self) -> bool:
+ if not self.light:
+ self.light = False
+ return self.light
+
+ async def get_errors(self) -> list:
+ tunerstatus = None
+ errors = []
+
+ try:
+ tunerstatus = await self.api.tunerstatus()
+ except Exception as e:
+ logging.warning(e)
+
+ if tunerstatus:
+ tuner = tunerstatus[0].get("TUNERSTATUS")
+ if tuner:
+ if len(tuner) > 0:
+ chain_status = tuner[0].get("TunerChainStatus")
+ if chain_status and len(chain_status) > 0:
+ board_map = {
+ 0: "Left board",
+ 1: "Center board",
+ 2: "Right board",
+ }
+ offset = (
+ 6
+ if chain_status[0]["HashchainIndex"] in [6, 7, 8]
+ else chain_status[0]["HashchainIndex"]
+ )
+ for board in chain_status:
+ _id = board["HashchainIndex"] - offset
+ if board["Status"] not in [
+ "Stable",
+ "Testing performance profile",
+ ]:
+ _error = board["Status"]
+ _error = _error[0].lower() + _error[1:]
+ errors.append(
+ BraiinsOSError(f"{board_map[_id]} {_error}")
+ )
+ return errors
+
async def get_data(self) -> MinerData:
"""Get data from the miner.
diff --git a/pyasic/miners/_backends/bosminer_old.py b/pyasic/miners/_backends/bosminer_old.py
index b5558045..c335adec 100644
--- a/pyasic/miners/_backends/bosminer_old.py
+++ b/pyasic/miners/_backends/bosminer_old.py
@@ -15,6 +15,7 @@
import logging
import ipaddress
+from typing import Union
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.miners import BaseMiner
@@ -29,7 +30,7 @@ class BOSMinerOld(BaseMiner):
self.uname = "root"
self.pwd = "admin"
- async def send_ssh_command(self, cmd: str) -> str or None:
+ async def send_ssh_command(self, cmd: str) -> Union[str, None]:
"""Send a command to the miner over ssh.
:return: Result of the command or None.
@@ -61,3 +62,33 @@ class BOSMinerOld(BaseMiner):
async def update_to_plus(self):
result = await self.send_ssh_command("opkg update && opkg install bos_plus")
return result
+
+ async def check_light(self) -> bool:
+ return False
+
+ async def fault_light_on(self) -> bool:
+ return False
+
+ async def fault_light_off(self) -> bool:
+ return False
+
+ async def get_config(self) -> None:
+ return None
+
+ async def get_errors(self) -> list:
+ return []
+
+ async def get_hostname(self) -> str:
+ return "?"
+
+ async def get_mac(self) -> str:
+ return "00:00:00:00:00:00"
+
+ async def get_model(self) -> str:
+ return "S9"
+
+ async def reboot(self) -> bool:
+ return False
+
+ async def restart_backend(self) -> bool:
+ return False
diff --git a/pyasic/miners/_backends/btminer.py b/pyasic/miners/_backends/btminer.py
index b1b2c993..3cd1f5cb 100644
--- a/pyasic/miners/_backends/btminer.py
+++ b/pyasic/miners/_backends/btminer.py
@@ -23,6 +23,7 @@ from pyasic.API import APIError
from pyasic.data import MinerData
from pyasic.data.error_codes import WhatsminerError
+from pyasic.config import MinerConfig
from pyasic.settings import PyasicSettings
@@ -99,6 +100,40 @@ class BTMiner(BaseMiner):
return str(mac).upper()
+ async def check_light(self) -> bool:
+ if not self.light:
+ self.light = False
+ return self.light
+
+ async def fault_light_off(self) -> bool:
+ return False
+
+ async def fault_light_on(self) -> bool:
+ return False
+
+ async def get_errors(self) -> list:
+ return []
+
+ async def reboot(self) -> bool:
+ return False
+
+ async def restart_backend(self) -> bool:
+ return False
+
+ async def get_config(self) -> MinerConfig:
+ pools = None
+ cfg = MinerConfig()
+
+ try:
+ pools = await self.api.pools()
+ except APIError as e:
+ logging.warning(e)
+
+ if pools:
+ if "POOLS" in pools.keys():
+ cfg = cfg.from_api(pools["POOLS"])
+ return cfg
+
async def get_data(self) -> MinerData:
"""Get data from the miner.
diff --git a/pyasic/miners/_backends/cgminer.py b/pyasic/miners/_backends/cgminer.py
index 6c4c851a..f807e4b3 100644
--- a/pyasic/miners/_backends/cgminer.py
+++ b/pyasic/miners/_backends/cgminer.py
@@ -151,6 +151,23 @@ class CGMiner(BaseMiner):
self.config = result.stdout
return self.config
+ async def check_light(self) -> bool:
+ if not self.light:
+ self.light = False
+ return self.light
+
+ async def fault_light_off(self) -> bool:
+ return False
+
+ async def fault_light_on(self) -> bool:
+ return False
+
+ async def get_errors(self) -> list:
+ return []
+
+ async def get_mac(self) -> str:
+ return "00:00:00:00:00:00"
+
async def get_data(self) -> MinerData:
"""Get data from the miner.
diff --git a/pyasic/miners/miner_factory.py b/pyasic/miners/miner_factory.py
index db972646..c58f8e9b 100644
--- a/pyasic/miners/miner_factory.py
+++ b/pyasic/miners/miner_factory.py
@@ -235,6 +235,7 @@ MINER_CLASSES = {
"Default": CGMinerAvalon1066,
"CGMiner": CGMinerAvalon1066,
},
+ "Unknown": {"Default": UnknownMiner},
}
diff --git a/pyasic/miners/unknown.py b/pyasic/miners/unknown.py
index b6fa5463..a5de1737 100644
--- a/pyasic/miners/unknown.py
+++ b/pyasic/miners/unknown.py
@@ -31,3 +31,29 @@ class UnknownMiner(BaseMiner):
async def get_hostname(self):
return "Unknown"
+
+ async def check_light(self) -> bool:
+ if not self.light:
+ self.light = False
+ return self.light
+
+ async def fault_light_off(self) -> bool:
+ return False
+
+ async def fault_light_on(self) -> bool:
+ return False
+
+ async def get_config(self) -> None:
+ return None
+
+ async def get_errors(self) -> list:
+ return []
+
+ async def get_mac(self) -> str:
+ return "00:00:00:00:00:00"
+
+ async def reboot(self) -> bool:
+ return False
+
+ async def restart_backend(self) -> bool:
+ return False
diff --git a/pyasic/tests/__init__.py b/pyasic/tests/__init__.py
index 72078b8e..c33fd02c 100644
--- a/pyasic/tests/__init__.py
+++ b/pyasic/tests/__init__.py
@@ -13,6 +13,8 @@
# limitations under the License.
import unittest
+from pyasic.tests.miners_tests import MinersTest
+from pyasic.tests.network_tests import NetworkTest
if __name__ == "__main__":
unittest.main()
diff --git a/pyasic/tests/miners_tests/__init__.py b/pyasic/tests/miners_tests/__init__.py
new file mode 100644
index 00000000..b1887843
--- /dev/null
+++ b/pyasic/tests/miners_tests/__init__.py
@@ -0,0 +1,54 @@
+# 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 unittest
+
+from pyasic.miners.miner_factory import MINER_CLASSES
+
+import inspect
+import sys
+
+
+class MinersTest(unittest.TestCase):
+ def test_miner_model_creation(self):
+ for miner_model in MINER_CLASSES.keys():
+ for miner_api in MINER_CLASSES[miner_model].keys():
+ with self.subTest(miner_model=miner_model, miner_api=miner_api):
+ miner = MINER_CLASSES[miner_model][miner_api]("0.0.0.0")
+ self.assertTrue(
+ isinstance(miner, MINER_CLASSES[miner_model][miner_api])
+ )
+
+ def test_miner_backend_backup_creation(self):
+ backends = inspect.getmembers(
+ sys.modules["pyasic.miners._backends"], inspect.isclass
+ )
+ for backend in backends:
+ miner_class = backend[1]
+ with self.subTest(miner_class=miner_class):
+ miner = miner_class("0.0.0.0")
+ self.assertTrue(isinstance(miner, miner_class))
+
+ def test_miner_type_creation_failure(self):
+ backends = inspect.getmembers(
+ sys.modules["pyasic.miners._types"], inspect.isclass
+ )
+ for backend in backends:
+ miner_class = backend[1]
+ with self.subTest(miner_class=miner_class):
+ with self.assertRaises(TypeError):
+ miner_class("0.0.0.0")
+
+
+if __name__ == "__main__":
+ unittest.main()