Refactor MinerFactory._get_miner_type(), move BaseMiner to its own file, and improve interface of miner.send_config() (#17)

This commit is contained in:
UpstreamData
2022-08-22 14:10:37 -06:00
committed by GitHub
parent 50ccfec1b3
commit 957c9a3678
54 changed files with 326 additions and 377 deletions

View File

@@ -94,7 +94,7 @@ if __name__ == "__main__":
import asyncio import asyncio
import sys import sys
from pyasic.miners.miner_factory import MinerFactory from pyasic.miners import get_miner
# Fix whatsminer bug # Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy # if the computer is windows, set the event loop policy to a WindowsSelector policy
@@ -106,7 +106,7 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
async def get_miner_data(miner_ip: str): async def get_miner_data(miner_ip: str):
# Use MinerFactory to get miner # Use MinerFactory to get miner
# MinerFactory is a singleton, so we can just get the instance in place # MinerFactory is a singleton, so we can just get the instance in place
miner = await MinerFactory().get_miner(miner_ip) miner = await get_miner(miner_ip)
# Get data from the miner # Get data from the miner
data = await miner.get_data() data = await miner.get_data()
@@ -125,7 +125,7 @@ If needed, this library exposes a wrapper for the miner API that can be used for
import asyncio import asyncio
import sys import sys
from pyasic.miners.miner_factory import MinerFactory from pyasic.miners import get_miner
# Fix whatsminer bug # Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy # if the computer is windows, set the event loop policy to a WindowsSelector policy
@@ -135,7 +135,7 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
async def get_api_commands(miner_ip: str): async def get_api_commands(miner_ip: str):
# Get the miner # Get the miner
miner = await MinerFactory().get_miner(miner_ip) miner = await get_miner(miner_ip)
# List all available commands # List all available commands
print(miner.api.get_commands()) print(miner.api.get_commands())
@@ -153,7 +153,7 @@ The miner API commands will raise an `APIError` if they fail with a bad status c
import asyncio import asyncio
import sys import sys
from pyasic.miners.miner_factory import MinerFactory from pyasic.miners import get_miner
# Fix whatsminer bug # Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy # if the computer is windows, set the event loop policy to a WindowsSelector policy
@@ -163,7 +163,7 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
async def get_api_commands(miner_ip: str): async def get_api_commands(miner_ip: str):
# Get the miner # Get the miner
miner = await MinerFactory().get_miner(miner_ip) miner = await get_miner(miner_ip)
# Run the devdetails command # Run the devdetails command
# This is equivalent to await miner.api.send_command("devdetails") # This is equivalent to await miner.api.send_command("devdetails")

View File

@@ -12,126 +12,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import asyncssh
import logging
import ipaddress import ipaddress
from abc import ABC, abstractmethod from typing import Union
from pyasic.data import MinerData from pyasic.miners.base import BaseMiner, AnyMiner
from pyasic.config import MinerConfig from pyasic.miners.miner_factory import MinerFactory
# abstracted version of get miner that is easier to access
class BaseMiner(ABC): async def get_miner(ip: Union[ipaddress.ip_address, str]) -> AnyMiner:
def __init__(self, *args) -> None: return await MinerFactory().get_miner(ip)
self.ip = None
self.uname = "root"
self.pwd = "admin"
self.api = None
self.api_type = None
self.model = None
self.light = None
self.hostname = None
self.nominal_chips = 1
self.version = None
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)}"
def __lt__(self, other):
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
def __gt__(self, other):
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username=self.uname,
password=self.pwd,
server_host_key_algs=["ssh-rsa"],
)
return conn
except asyncssh.misc.PermissionDenied:
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username="root",
password="admin",
server_host_key_algs=["ssh-rsa"],
)
return conn
except Exception as e:
raise e
except OSError as e:
logging.warning(f"Connection refused: {self}")
raise e
except Exception as e:
raise e
@abstractmethod
async def fault_light_on(self) -> bool:
pass
@abstractmethod
async def fault_light_off(self) -> bool:
pass
# async def send_file(self, src, dest):
# async with (await self._get_ssh_connection()) as conn:
# await asyncssh.scp(src, (conn, dest))
@abstractmethod
async def check_light(self) -> bool:
pass
# @abstractmethod
async def get_board_info(self):
return None
@abstractmethod
async def get_config(self) -> MinerConfig:
pass
@abstractmethod
async def get_hostname(self) -> str:
pass
@abstractmethod
async def get_model(self) -> str:
pass
@abstractmethod
async def reboot(self) -> bool:
pass
@abstractmethod
async def restart_backend(self) -> bool:
pass
async def send_config(self, *args, **kwargs):
return None
@abstractmethod
async def get_mac(self) -> str:
pass
@abstractmethod
async def get_errors(self) -> list:
pass
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))

View File

@@ -18,9 +18,10 @@ from typing import Union
from pyasic.API.bmminer import BMMinerAPI from pyasic.API.bmminer import BMMinerAPI
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.config import MinerConfig
from pyasic.settings import PyasicSettings from pyasic.settings import PyasicSettings
@@ -154,6 +155,9 @@ class BMMiner(BaseMiner):
return True return True
return False return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def check_light(self) -> bool: async def check_light(self) -> bool:
if not self.light: if not self.light:
self.light = False self.light = False

View File

@@ -20,7 +20,7 @@ from typing import Union
import toml import toml
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
from pyasic.API.bosminer import BOSMinerAPI from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API import APIError from pyasic.API import APIError
@@ -215,22 +215,12 @@ class BOSMiner(BaseMiner):
logging.warning(f"Failed to get model for miner: {self}") logging.warning(f"Failed to get model for miner: {self}")
return None return None
async def send_config(self, yaml_config, ip_user: bool = False) -> None: async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config.""" """Configures miner with yaml config."""
logging.debug(f"{self}: Sending config.") logging.debug(f"{self}: Sending config.")
if ip_user: toml_conf = config.as_bos(
suffix = str(self.ip).split(".")[-1] model=self.model.replace(" (BOS)", ""), user_suffix=user_suffix
toml_conf = ( )
MinerConfig()
.from_yaml(yaml_config)
.as_bos(model=self.model.replace(" (BOS)", ""), user_suffix=suffix)
)
else:
toml_conf = (
MinerConfig()
.from_yaml(yaml_config)
.as_bos(model=self.model.replace(" (BOS)", ""))
)
async with (await self._get_ssh_connection()) as conn: async with (await self._get_ssh_connection()) as conn:
await conn.run("/etc/init.d/bosminer stop") await conn.run("/etc/init.d/bosminer stop")
logging.debug(f"{self}: Opening SFTP connection.") logging.debug(f"{self}: Opening SFTP connection.")

View File

@@ -18,7 +18,8 @@ import ipaddress
from typing import Union from typing import Union
from pyasic.API.bosminer import BOSMinerAPI from pyasic.API.bosminer import BOSMinerAPI
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
from pyasic.config import MinerConfig
class BOSMinerOld(BaseMiner): class BOSMinerOld(BaseMiner):
@@ -92,3 +93,6 @@ class BOSMinerOld(BaseMiner):
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
return False return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None

View File

@@ -18,7 +18,7 @@ from typing import Union
from pyasic.API.btminer import BTMinerAPI from pyasic.API.btminer import BTMinerAPI
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
from pyasic.API import APIError from pyasic.API import APIError
from pyasic.data import MinerData from pyasic.data import MinerData
@@ -165,12 +165,8 @@ class BTMiner(BaseMiner):
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
return False return False
async def send_config(self, yaml_config, ip_user: bool = False): async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
if ip_user: conf = config.as_wm(user_suffix=user_suffix)
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_wm(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_wm()
await self.api.update_pools( await self.api.update_pools(
conf[0]["url"], conf[0]["url"],

View File

@@ -18,8 +18,9 @@ from typing import Union
from pyasic.API.cgminer import CGMinerAPI from pyasic.API.cgminer import CGMinerAPI
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
from pyasic.API import APIError from pyasic.API import APIError
from pyasic.config import MinerConfig
from pyasic.data import MinerData from pyasic.data import MinerData
@@ -165,6 +166,9 @@ class CGMiner(BaseMiner):
async def get_errors(self) -> list: async def get_errors(self) -> list:
return [] return []
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def get_mac(self) -> str: async def get_mac(self) -> str:
return "00:00:00:00:00:00" return "00:00:00:00:00:00"

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S17(BaseMiner): class S17(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S17Plus(BaseMiner): class S17Plus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S17Pro(BaseMiner): class S17Pro(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S17e(BaseMiner): class S17e(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class T17(BaseMiner): class T17(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class T17Plus(BaseMiner): class T17Plus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class T17e(BaseMiner): class T17e(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S19(BaseMiner): class S19(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S19Pro(BaseMiner): class S19Pro(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S19a(BaseMiner): class S19a(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S19j(BaseMiner): class S19j(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S19jPro(BaseMiner): class S19jPro(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class T19(BaseMiner): class T19(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S9(BaseMiner): class S9(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class S9i(BaseMiner): class S9i(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class T9(BaseMiner): class T9(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon1026(BaseMiner): class Avalon1026(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon1047(BaseMiner): class Avalon1047(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon1066(BaseMiner): class Avalon1066(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon721(BaseMiner): class Avalon721(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon741(BaseMiner): class Avalon741(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon761(BaseMiner): class Avalon761(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon821(BaseMiner): class Avalon821(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon841(BaseMiner): class Avalon841(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon851(BaseMiner): class Avalon851(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class Avalon921(BaseMiner): class Avalon921(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M20(BaseMiner): class M20(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M20S(BaseMiner): class M20S(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M20SPlus(BaseMiner): class M20SPlus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M21(BaseMiner): class M21(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M21S(BaseMiner): class M21S(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M21SPlus(BaseMiner): class M21SPlus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M30S(BaseMiner): class M30S(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M30SPlus(BaseMiner): class M30SPlus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M30SPlusPlus(BaseMiner): class M30SPlusPlus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M31S(BaseMiner): class M31S(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M31SPlus(BaseMiner): class M31SPlus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M32(BaseMiner): class M32(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
class M32S(BaseMiner): class M32S(BaseMiner):

View File

@@ -55,14 +55,10 @@ class BMMinerX19(BMMiner):
self.config = MinerConfig().from_raw(data) self.config = MinerConfig().from_raw(data)
return self.config return self.config
async def send_config(self, yaml_config, ip_user: bool = False) -> None: async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi" url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
auth = httpx.DigestAuth(self.uname, self.pwd) auth = httpx.DigestAuth(self.uname, self.pwd)
if ip_user: conf = config.as_x19(user_suffix=user_suffix)
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_x19()
try: try:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:

View File

@@ -51,15 +51,11 @@ class CGMinerA10X(CGMiner):
return True return True
return False return False
async def send_config(self, yaml_config, ip_user: bool = False) -> None: async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config.""" """Configures miner with yaml config."""
raise NotImplementedError raise NotImplementedError
logging.debug(f"{self}: Sending config.") logging.debug(f"{self}: Sending config.")
if ip_user: conf = config.as_avalon(user_suffix=user_suffix)
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
data = await self.api.ascset( data = await self.api.ascset(
0, "setpool", f"root,root,{conf}" 0, "setpool", f"root,root,{conf}"
) # this should work but doesn't ) # this should work but doesn't

View File

@@ -51,15 +51,11 @@ class CGMinerA7X(CGMiner):
return True return True
return False return False
async def send_config(self, yaml_config, ip_user: bool = False) -> None: async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config.""" """Configures miner with yaml config."""
raise NotImplementedError raise NotImplementedError
logging.debug(f"{self}: Sending config.") logging.debug(f"{self}: Sending config.")
if ip_user: conf = config.as_avalon(user_suffix=user_suffix)
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
data = await self.api.ascset( data = await self.api.ascset(
0, "setpool", f"root,root,{conf}" 0, "setpool", f"root,root,{conf}"
) # this should work but doesn't ) # this should work but doesn't

View File

@@ -51,15 +51,11 @@ class CGMinerA8X(CGMiner):
return True return True
return False return False
async def send_config(self, yaml_config, ip_user: bool = False) -> None: async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config.""" """Configures miner with yaml config."""
raise NotImplementedError raise NotImplementedError
logging.debug(f"{self}: Sending config.") logging.debug(f"{self}: Sending config.")
if ip_user: conf = config.as_avalon(user_suffix=user_suffix)
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
data = await self.api.ascset( data = await self.api.ascset(
0, "setpool", f"root,root,{conf}" 0, "setpool", f"root,root,{conf}"
) # this should work but doesn't ) # this should work but doesn't

View File

@@ -52,15 +52,11 @@ class CGMinerAvalon921(CGMiner, Avalon921):
return True return True
return False return False
async def send_config(self, yaml_config, ip_user: bool = False) -> None: async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config.""" """Configures miner with yaml config."""
raise NotImplementedError raise NotImplementedError
logging.debug(f"{self}: Sending config.") logging.debug(f"{self}: Sending config.")
if ip_user: conf = config.as_avalon(user_suffix=user_suffix)
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
data = await self.api.ascset( data = await self.api.ascset(
0, "setpool", f"root,root,{conf}" 0, "setpool", f"root,root,{conf}"
) # this should work but doesn't ) # this should work but doesn't

142
pyasic/miners/base.py Normal file
View File

@@ -0,0 +1,142 @@
# 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 asyncssh
import logging
import ipaddress
from abc import ABC, abstractmethod
from typing import TypeVar
from pyasic.data import MinerData
from pyasic.config import MinerConfig
class BaseMiner(ABC):
def __init__(self, *args) -> None:
self.ip = None
self.uname = "root"
self.pwd = "admin"
self.api = None
self.api_type = None
self.model = None
self.light = None
self.hostname = None
self.nominal_chips = 1
self.version = None
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)}"
def __lt__(self, other):
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
def __gt__(self, other):
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username=self.uname,
password=self.pwd,
server_host_key_algs=["ssh-rsa"],
)
return conn
except asyncssh.misc.PermissionDenied:
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username="root",
password="admin",
server_host_key_algs=["ssh-rsa"],
)
return conn
except Exception as e:
raise e
except OSError as e:
logging.warning(f"Connection refused: {self}")
raise e
except Exception as e:
raise e
@abstractmethod
async def fault_light_on(self) -> bool:
pass
@abstractmethod
async def fault_light_off(self) -> bool:
pass
# async def send_file(self, src, dest):
# async with (await self._get_ssh_connection()) as conn:
# await asyncssh.scp(src, (conn, dest))
@abstractmethod
async def check_light(self) -> bool:
pass
# @abstractmethod
async def get_board_info(self):
return None
@abstractmethod
async def get_config(self) -> MinerConfig:
pass
@abstractmethod
async def get_hostname(self) -> str:
pass
@abstractmethod
async def get_model(self) -> str:
pass
@abstractmethod
async def reboot(self) -> bool:
pass
@abstractmethod
async def restart_backend(self) -> bool:
pass
@abstractmethod
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
@abstractmethod
async def get_mac(self) -> str:
pass
@abstractmethod
async def get_errors(self) -> list:
pass
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)

View File

@@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import TypeVar, Tuple, List, Union from typing import Tuple, List, Union
from collections.abc import AsyncIterable from collections.abc import AsyncIterable
from pyasic.miners import BaseMiner from pyasic.miners.base import AnyMiner
import httpx import httpx
from pyasic.miners.antminer import * from pyasic.miners.antminer import *
@@ -42,8 +42,6 @@ from pyasic.settings import PyasicSettings
import asyncssh import asyncssh
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
MINER_CLASSES = { MINER_CLASSES = {
"ANTMINER S9": { "ANTMINER S9": {
"Default": BOSMinerS9, "Default": BOSMinerS9,
@@ -205,43 +203,43 @@ MINER_CLASSES = {
"BTMiner": BTMinerM32, "BTMiner": BTMinerM32,
"20": BTMinerM32V20, "20": BTMinerM32V20,
}, },
"AvalonMiner 721": { "AVALONMINER 721": {
"Default": CGMinerAvalon721, "Default": CGMinerAvalon721,
"CGMiner": CGMinerAvalon721, "CGMiner": CGMinerAvalon721,
}, },
"AvalonMiner 741": { "AVALONMINER 741": {
"Default": CGMinerAvalon741, "Default": CGMinerAvalon741,
"CGMiner": CGMinerAvalon741, "CGMiner": CGMinerAvalon741,
}, },
"AvalonMiner 761": { "AVALONMINER 761": {
"Default": CGMinerAvalon761, "Default": CGMinerAvalon761,
"CGMiner": CGMinerAvalon761, "CGMiner": CGMinerAvalon761,
}, },
"AvalonMiner 821": { "AVALONMINER 821": {
"Default": CGMinerAvalon821, "Default": CGMinerAvalon821,
"CGMiner": CGMinerAvalon821, "CGMiner": CGMinerAvalon821,
}, },
"AvalonMiner 841": { "AVALONMINER 841": {
"Default": CGMinerAvalon841, "Default": CGMinerAvalon841,
"CGMiner": CGMinerAvalon841, "CGMiner": CGMinerAvalon841,
}, },
"AvalonMiner 851": { "AVALONMINER 851": {
"Default": CGMinerAvalon851, "Default": CGMinerAvalon851,
"CGMiner": CGMinerAvalon851, "CGMiner": CGMinerAvalon851,
}, },
"AvalonMiner 921": { "AVALONMINER 921": {
"Default": CGMinerAvalon921, "Default": CGMinerAvalon921,
"CGMiner": CGMinerAvalon921, "CGMiner": CGMinerAvalon921,
}, },
"AvalonMiner 1026": { "AVALONMINER 1026": {
"Default": CGMinerAvalon1026, "Default": CGMinerAvalon1026,
"CGMiner": CGMinerAvalon1026, "CGMiner": CGMinerAvalon1026,
}, },
"AvalonMiner 1047": { "AVALONMINER 1047": {
"Default": CGMinerAvalon1047, "Default": CGMinerAvalon1047,
"CGMiner": CGMinerAvalon1047, "CGMiner": CGMinerAvalon1047,
}, },
"AvalonMiner 1066": { "AVALONMINER 1066": {
"Default": CGMinerAvalon1066, "Default": CGMinerAvalon1066,
"CGMiner": CGMinerAvalon1066, "CGMiner": CGMinerAvalon1066,
}, },
@@ -393,98 +391,122 @@ class MinerFactory(metaclass=Singleton):
model, api, ver = None, None, None model, api, ver = None, None, None
try: try:
devdetails, version = await self._get_devdetails_and_version(ip) devdetails, version = await self.__get_devdetails_and_version(ip)
except APIError as e: except APIError as e:
# catch APIError and let the factory know we cant get data # catch APIError and let the factory know we cant get data
logging.warning(f"{ip}: API Command Error: {e}") logging.warning(f"{ip}: API Command Error: {e}")
return None, None, None return None, None, None
except OSError or ConnectionRefusedError: except OSError or ConnectionRefusedError:
devdetails = None
version = None
# miner refused connection on API port, we wont be able to get data this way # miner refused connection on API port, we wont be able to get data this way
# try ssh # try ssh
try: try:
_model = await self._get_model_from_ssh(ip) _model = await self.__get_model_from_ssh(ip)
if _model: if _model:
model = _model model = _model
api = "BOSMiner+" api = "BOSMiner+"
return model, api, None
except asyncssh.misc.PermissionDenied: except asyncssh.misc.PermissionDenied:
try: try:
data = await self._get_system_info_from_web(ip) data = await self.__get_system_info_from_web(ip)
if "minertype" in data.keys(): if "minertype" in data.keys():
model = data["minertype"].upper() model = data["minertype"].upper()
if "bmminer" in "\t".join(data.keys()): if "bmminer" in "\t".join(data.keys()):
api = "BMMiner" api = "BMMiner"
except Exception as e: except Exception as e:
logging.debug(f"Unable to get miner - {e}") logging.debug(f"Unable to get miner - {e}")
return None, None, None return model, api, ver
# if we have devdetails, we can get model data from there # if we have devdetails, we can get model data from there
if devdetails: if devdetails:
_model = self._parse_model_from_devdetails(devdetails) for _devdetails_key in ["Model", "Driver"]:
if _model: try:
model = _model model = devdetails["DEVDETAILS"][0][_devdetails_key].upper()
if not model == "BITMICRO":
break
except KeyError:
continue
if not model:
# braiins OS bug check just in case
if "s9" in devdetails["STATUS"][0]["Description"]:
model = "ANTMINER S9"
if "s17" in version["STATUS"][0]["Description"]:
model = "ANTMINER S17"
# if we have version we can get API type from here # if we have version we can get API type from here
if version: if version:
_api, _model, _ver = self._parse_type_from_version(version) if "VERSION" in version:
if _api: api_types = ["BMMiner", "CGMiner", "BTMiner"]
api = _api # check basic API types, BOSMiner needs a special check
if _model: for api_type in api_types:
model = _model if any(api_type in string for string in version["VERSION"][0]):
if _ver: api = api_type
ver = _ver
# check if there are any BOSMiner strings in any of the dict keys
if any("BOSminer" in string for string in version["VERSION"][0]):
api = "BOSMiner"
if version["VERSION"][0].get("BOSminer"):
if "plus" in version["VERSION"][0]["BOSminer"]:
api = "BOSMiner+"
if "BOSminer+" in version["VERSION"][0]:
api = "BOSMiner+"
# check for avalonminers
for _version_key in ["PROD", "MODEL"]:
try:
_data = version["VERSION"][0][_version_key].split("-")
except KeyError:
continue
model = _data[0].upper()
if _version_key == "MODEL":
model = f"AVALONMINER {_data[0]}"
if len(_data) > 1:
ver = _data[1]
if version.get("Description") and (
"whatsminer" in version.get("Description")
):
api = "BTMiner"
# if we have no model from devdetails but have version, try to get it from there # if we have no model from devdetails but have version, try to get it from there
if version and not model: if version and not model:
# make sure version isn't blank try:
if ( model = version["VERSION"][0]["Type"].upper()
"VERSION" in version.keys() except KeyError:
and version.get("VERSION") pass
and not version.get("VERSION") == []
):
# try to get "Type" which is model
if version["VERSION"][0].get("Type"):
model = version["VERSION"][0]["Type"].upper()
# braiins OS bug check just in case
elif "am2-s17" in version["STATUS"][0]["Description"]:
model = "ANTMINER S17"
if not model: if not model:
_model = await self._get_model_from_stats(ip) stats = await self._send_api_command(str(ip), "stats")
if _model: if stats:
model = _model try:
_model = stats["STATS"][0]["Type"].upper()
except KeyError:
pass
else:
for split_point in [" BB", " XILINX", " (VNISH"]:
if split_point in _model:
_model = _model.split(split_point)[0]
if "PRO" in _model and " PRO" not in _model:
_model = _model.replace("PRO", " PRO")
model = _model
if model: if model:
_ver, model = self._get_ver_from_model(model) if " HIVEON" in model:
if _ver: # do hiveon check before whatsminer as HIVEON contains a V
ver = _ver model = model.split(" HIVEON")[0]
api = "Hiveon"
# whatsminer have a V in their version string (M20SV41), everything after it is ver
if "V" in model:
_ver = model.split("V")
if len(_ver) > 1:
ver = model.split("V")[1]
model = model.split("V")[0]
# don't need "Bitmain", just "ANTMINER XX" as model
if "BITMAIN " in model:
model = model.replace("BITMAIN ", "")
return model, api, ver return model, api, ver
@staticmethod async def __get_devdetails_and_version(
def _get_ver_from_model(model) -> Tuple[Union[str, None], Union[str, None]]:
ver, mode, = (
None,
None,
)
if " HIVEON" in model:
model = model.split(" HIVEON")[0]
api = "Hiveon"
# whatsminer have a V in their version string (M20SV41), remove everything after it
if "V" in model:
_ver = model.split("V")
if len(_ver) > 1:
ver = model.split("V")[1]
model = model.split("V")[0]
# don't need "Bitmain", just "ANTMINER XX" as model
if "BITMAIN " in model:
model = model.replace("BITMAIN ", "")
return ver, model
async def _get_devdetails_and_version(
self, ip self, ip
) -> Tuple[Union[dict, None], Union[dict, None]]: ) -> Tuple[Union[dict, None], Union[dict, None]]:
version = None version = None
@@ -522,90 +544,7 @@ class MinerFactory(metaclass=Singleton):
return devdetails, version return devdetails, version
@staticmethod @staticmethod
def _parse_model_from_devdetails(devdetails) -> Union[str, None]: async def __get_model_from_ssh(ip: ipaddress.ip_address) -> Union[str, None]:
model = None
if "DEVDETAILS" in devdetails.keys() and not devdetails["DEVDETAILS"] == []:
# check for model, for most miners
if not devdetails["DEVDETAILS"][0]["Model"] == "":
# model of most miners
model = devdetails["DEVDETAILS"][0]["Model"].upper()
# if model fails, try driver
else:
# some avalonminers have model in driver
model = devdetails["DEVDETAILS"][0]["Driver"].upper()
else:
if "s9" in devdetails["STATUS"][0]["Description"]:
model = "ANTMINER S9"
return model
@staticmethod
def _parse_type_from_version(
version,
) -> Tuple[Union[str, None], Union[str, None], Union[str, None],]:
api, model, ver = None, None, None
if "VERSION" in version.keys():
# check if there are any BMMiner strings in any of the dict keys
if any("BMMiner" in string for string in version["VERSION"][0].keys()):
api = "BMMiner"
# check if there are any CGMiner strings in any of the dict keys
elif any("CGMiner" in string for string in version["VERSION"][0].keys()):
api = "CGMiner"
elif any("BTMiner" in string for string in version["VERSION"][0].keys()):
api = "BTMiner"
# check if there are any BOSMiner strings in any of the dict keys
elif any("BOSminer" in string for string in version["VERSION"][0].keys()):
api = "BOSMiner"
if version["VERSION"][0].get("BOSminer"):
if "plus" in version["VERSION"][0]["BOSminer"]:
api = "BOSMiner+"
if "BOSminer+" in version["VERSION"][0].keys():
api = "BOSMiner+"
# if all that fails, check the Description to see if it is a whatsminer
if version.get("Description") and ("whatsminer" in version.get("Description")):
api = "BTMiner"
# If version does not exist in the keys, return None (resulting in Unknown).
# Prevents halting on suspended miners that aren't returning valid information.
if "VERSION" not in version.keys():
return None, None, None
# check for avalonminers
if version["VERSION"][0].get("PROD"):
_data = version["VERSION"][0]["PROD"].split("-")
model = _data[0].upper()
if len(_data) > 1:
ver = _data[1]
elif version["VERSION"][0].get("MODEL"):
_data = version["VERSION"][0]["MODEL"].split("-")
model = f"AvalonMiner {_data[0]}"
if len(_data) > 1:
ver = _data[1]
return api, model, ver
async def _get_model_from_stats(self, ip) -> Union[str, None]:
model = None
stats = await self._send_api_command(str(ip), "stats")
if stats:
if "STATS" in stats.keys():
if stats["STATS"][0].get("Type"):
_model = stats["STATS"][0]["Type"].upper()
if " BB" in _model:
_model = _model.split(" BB")[0]
if " XILINX" in _model:
_model = _model.split(" XILINX")[0]
if "PRO" in _model and not " PRO" in _model:
model = _model.replace("PRO", " PRO")
return model
@staticmethod
async def _get_model_from_ssh(ip: ipaddress.ip_address) -> Union[str, None]:
model = None model = None
async with asyncssh.connect( async with asyncssh.connect(
str(ip), str(ip),
@@ -625,7 +564,7 @@ class MinerFactory(metaclass=Singleton):
return model return model
@staticmethod @staticmethod
async def _get_system_info_from_web(ip) -> dict: async def __get_system_info_from_web(ip) -> dict:
url = f"http://{ip}/cgi-bin/get_system_info.cgi" url = f"http://{ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root") auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:

View File

@@ -13,7 +13,8 @@
# limitations under the License. # limitations under the License.
from pyasic.API.unknown import UnknownAPI from pyasic.API.unknown import UnknownAPI
from pyasic.miners import BaseMiner from pyasic.miners.base import BaseMiner
from pyasic.config import MinerConfig
class UnknownMiner(BaseMiner): class UnknownMiner(BaseMiner):
@@ -57,3 +58,6 @@ class UnknownMiner(BaseMiner):
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
return False return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None