Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
fda7571
feature: add pyasic miner adapter
b-rowan Aug 29, 2025
02f3074
refactor: enhance PyASIC miner controller to ensure miner instance re…
markoceri Sep 3, 2025
65353e3
feat: add PyASIC miner controller to adapter service for enhanced min…
markoceri Sep 3, 2025
d2301de
refactor: streamline import statements in adapter_service.py for impr…
markoceri Sep 3, 2025
a16e00e
fix: update default IP address in MinerControllerPyASICConfig and cle…
markoceri Sep 3, 2025
6dd5576
fix: add PyASIC configuration to miner adapter maps for proper integr…
markoceri Sep 3, 2025
681fd67
feat: add validation schema for MinerControllerPyASICConfig with IP a…
markoceri Sep 3, 2025
63a8fdd
feat: add handler for PyASIC Miner Controller configuration in CLI menu
markoceri Sep 3, 2025
78985a6
fix: update pydantic and homeassistant_api versions in dependencies t…
markoceri Sep 3, 2025
7f09d60
fix: add "pyasic" to cSpell words in settings.json for improved spell…
markoceri Sep 3, 2025
7295dfe
Merge branch 'main' into feature-add-pyasic-miner-adapter
markoceri Sep 3, 2025
e9cb492
fix: update MINER_CONTROLLER_CONFIG_SCHEMA_MAP to include MinerContro…
markoceri Sep 4, 2025
bd7d0c5
Merge branch 'dev' into feature-add-pyasic-miner-adapter
markoceri Sep 4, 2025
ab57b97
feat: add utility function to run asynchronous functions from synchro…
markoceri Sep 8, 2025
3a8eff0
fix: async miner retrieval into mixed sync/async context
markoceri Sep 8, 2025
c7a312b
refactor: remove miner retrieval in PyASICMinerController initialization
markoceri Sep 8, 2025
0ae888f
fix: update miner instance checks and replace asyncio.run with utilit…
markoceri Sep 8, 2025
a6a20cd
Merge branch 'dev' into feature-add-pyasic-miner-adapter
markoceri Sep 10, 2025
4a88ca7
feature: add pyasic miner adapter
b-rowan Aug 29, 2025
86fdb6a
refactor: enhance PyASIC miner controller to ensure miner instance re…
markoceri Sep 3, 2025
1e079f7
feat: add PyASIC miner controller to adapter service for enhanced min…
markoceri Sep 3, 2025
07fb5d4
refactor: streamline import statements in adapter_service.py for impr…
markoceri Sep 3, 2025
b16b8ec
fix: update default IP address in MinerControllerPyASICConfig and cle…
markoceri Sep 3, 2025
1bc2b38
fix: add PyASIC configuration to miner adapter maps for proper integr…
markoceri Sep 3, 2025
1fdd4ab
feat: add validation schema for MinerControllerPyASICConfig with IP a…
markoceri Sep 3, 2025
ed8e3c9
feat: add handler for PyASIC Miner Controller configuration in CLI menu
markoceri Sep 3, 2025
6df10ef
fix: update pydantic and homeassistant_api versions in dependencies t…
markoceri Sep 3, 2025
0181a67
fix: add "pyasic" to cSpell words in settings.json for improved spell…
markoceri Sep 3, 2025
e82419d
feature: add password to pyasic miner adaptor
b-rowan Sep 22, 2025
1d124f4
Merge branch 'dev' into brett
markoceri Oct 23, 2025
3e59e88
fix: enhance PyASIC miner controller CLI configuration to use existin…
markoceri Oct 23, 2025
0198706
feat: add password field to MinerControllerPyASICConfigSchema with va…
markoceri Oct 23, 2025
c880531
fix: adapters path for import statements in adapter_service
markoceri Oct 23, 2025
3b744fb
Merge branch 'dev' into main
markoceri Oct 23, 2025
e5bf443
fix: added password parameter to pyasic config schema, handled defaul…
markoceri Oct 23, 2025
0c062fe
fix: Attribute "_miner" already defined
markoceri Oct 23, 2025
11ea05a
Merge branch 'feature-add-pyasic-miner-adapter' into brett-mod
markoceri Oct 23, 2025
94417ea
feat: enhance MinerControllerPyASICConfig with additional parameters …
markoceri Oct 31, 2025
882e6ff
fix: initialize external_service variable in AdapterService
markoceri Oct 31, 2025
880ad20
feat: add protocol, port, and username parameters to PyASICMinerContr…
markoceri Oct 31, 2025
821f7d4
feat: enhance _get_miner method to set additional parameters for RPC …
markoceri Oct 31, 2025
59084be
fix: change debug logs to error logs for miner instance retrieval fai…
markoceri Oct 31, 2025
cb50328
refactor: simplify run_async_func parameter type and usage in pyasic …
markoceri Oct 31, 2025
588666f
fix: improve from_model method in MinerSchema for hash_rate and hash_…
markoceri Oct 31, 2025
967cf52
feat: add support for additional parameters in CLI configuration of P…
markoceri Oct 31, 2025
d1ed6f9
fix: add logger check before logging unknown protocol errors in PyASI…
markoceri Oct 31, 2025
3e795af
fix: updated MinerControllerPyASICConfigSchema with additional fields…
markoceri Nov 4, 2025
75b6fe1
fix: better error logging for PyASIC miner controller
markoceri Nov 5, 2025
697cf9b
fix: enhance protocol handling in PyASICMinerController with SSH support
markoceri Nov 5, 2025
b624041
fix: update import statement for BaseMinerRPCAPI in PyASICMinerContro…
markoceri Nov 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"sheduler",
"satoshi",
"hashrate",
"homeassistant"
"homeassistant",
"pyasic"
]
}
66 changes: 65 additions & 1 deletion edge_mining/adapters/domain/miner/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
)
from edge_mining.application.interfaces import ConfigurationServiceInterface
from edge_mining.domain.common import EntityId, Watts
from edge_mining.domain.miner.common import MinerControllerAdapter, MinerStatus
from edge_mining.domain.miner.common import MinerControllerAdapter, MinerControllerProtocol, MinerStatus
from edge_mining.domain.miner.entities import Miner, MinerController
from edge_mining.domain.miner.value_objects import HashRate
from edge_mining.shared.adapter_configs.miner import (
MinerControllerDummyConfig,
MinerControllerGenericSocketHomeAssistantAPIConfig,
MinerControllerPyASICConfig,
)
from edge_mining.shared.adapter_maps.miner import MINER_CONTROLLER_TYPE_EXTERNAL_SERVICE_MAP
from edge_mining.shared.external_services.entities import ExternalService
Expand Down Expand Up @@ -567,6 +568,67 @@ def handle_miner_controller_generic_socket_home_assistant_api_config(
)


def handle_miner_controller_pyasic_config(
miner: Optional[Miner], current_config: Optional[MinerControllerConfig] = None
) -> MinerControllerConfig:
"""Handle configuration for the PyASIC Miner Controller."""
click.echo(click.style("\n--- PyASIC Miner Controller Configuration ---", fg="yellow"))

# Default values from hardcoded values
default_ip = "192.168.1.100"
default_port: Optional[int] = None
default_username: Optional[str] = None
default_password: Optional[str] = None
default_protocol: MinerControllerProtocol = MinerControllerProtocol.WEB

# Try to get defaults from current_config
if current_config and current_config.is_valid(MinerControllerAdapter.PYASIC):
config: MinerControllerPyASICConfig = cast(MinerControllerPyASICConfig, current_config)
default_ip = config.ip or default_ip
default_port = config.port or default_port
default_username = config.username or default_username
default_password = config.password or default_password
default_protocol = config.protocol or default_protocol

ip: str = click.prompt(
"IP address of the PyASIC miner (eg. 192.168.1.100)",
type=str,
default=default_ip,
)

protocol: MinerControllerProtocol = click.prompt(
"Protocol to use to connect to the PyASIC miner",
type=click.Choice([p.value for p in MinerControllerProtocol]),
default=default_protocol.value,
)
protocol = MinerControllerProtocol(protocol)

port_input = click.prompt(
"Port of the PyASIC miner (eg. 80, press Enter for default)",
type=str,
default="",
)
port: Optional[int] = None if port_input == "" else int(port_input)

username_input = click.prompt(
"Username of the PyASIC miner (eg. root, press Enter for default)",
type=str,
default="",
)
username: Optional[str] = username_input if username_input != "" else default_username

password_input = click.prompt(
"Password of the PyASIC miner (empty represents 'use the default miner password')",
type=str,
default="",
)
password: Optional[str] = password_input if password_input != "" else default_password
if password == "":
password = None

return MinerControllerPyASICConfig(ip=ip, port=port, username=username, password=password, protocol=protocol)


def handle_miner_controller_configuration(
adapter_type: MinerControllerAdapter,
miner: Optional[Miner],
Expand All @@ -581,6 +643,8 @@ def handle_miner_controller_configuration(
config = handle_miner_controller_generic_socket_home_assistant_api_config(
miner=miner, current_config=current_config
)
elif adapter_type.value == MinerControllerAdapter.PYASIC.value:
config = handle_miner_controller_pyasic_config(miner=miner, current_config=current_config)
else:
click.echo(click.style("Unsupported controller type selected. Aborting.", fg="red"))
return config
Expand Down
268 changes: 268 additions & 0 deletions edge_mining/adapters/domain/miner/controllers/pyasic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
"""
pyasic adapter (Implementation of Port)
that controls a miner via pyasic.
"""

from typing import Dict, Optional, cast

import pyasic
from pyasic import AnyMiner
from pyasic.device.algorithm.hashrate import AlgoHashRate
from pyasic.rpc.base import BaseMinerRPCAPI
from pyasic.web.base import BaseWebAPI
from pyasic.ssh.base import BaseSSH

from edge_mining.adapters.utils import run_async_func
from edge_mining.domain.common import Watts
from edge_mining.domain.miner.common import MinerControllerProtocol, MinerStatus
from edge_mining.domain.miner.entities import Miner
from edge_mining.domain.miner.exceptions import MinerControllerConfigurationError
from edge_mining.domain.miner.ports import MinerControlPort
from edge_mining.domain.miner.value_objects import HashRate
from edge_mining.shared.adapter_configs.miner import MinerControllerPyASICConfig
from edge_mining.shared.external_services.ports import ExternalServicePort
from edge_mining.shared.interfaces.config import Configuration
from edge_mining.shared.interfaces.factories import MinerControllerAdapterFactory
from edge_mining.shared.logging.port import LoggerPort


class PyASICMinerControllerAdapterFactory(MinerControllerAdapterFactory):
"""
Create a factory for pyasic Miner Controller Adapter.
This factory is used to create instances of the adapter.
"""

def __init__(self):
self._miner: Optional[Miner] = None

def from_miner(self, miner: Miner):
"""Set the miner for this controller."""
self._miner = miner

def create(
self,
config: Optional[Configuration] = None,
logger: Optional[LoggerPort] = None,
external_service: Optional[ExternalServicePort] = None,
) -> MinerControlPort:
"""Create a miner controller adapter instance."""

if not isinstance(config, MinerControllerPyASICConfig):
raise MinerControllerConfigurationError("Invalid configuration for pyasic Miner Controller.")

# Get the config from the provided configuration
miner_controller_configuration: MinerControllerPyASICConfig = config

return PyASICMinerController(
ip=miner_controller_configuration.ip,
protocol=miner_controller_configuration.protocol,
port=miner_controller_configuration.port,
username=miner_controller_configuration.username,
password=miner_controller_configuration.password,
logger=logger,
)


class PyASICMinerController(MinerControlPort):
"""Controls a miner via pyasic."""

def __init__(
self,
ip: str,
port: Optional[int] = None,
username: Optional[str] = None,
password: Optional[str] = None,
protocol: Optional[MinerControllerProtocol] = None,
logger: Optional[LoggerPort] = None,
):
self.logger = logger

self.ip = ip
self.password = password
self.port = port
self.username = username
self.protocol = protocol

self._miner: Optional[AnyMiner] = None

self._log_configuration()

def _log_configuration(self):
if self.logger:
self.logger.debug(f"Entities Configured: IP={self.ip}")

def _get_miner(self) -> None:
"""Retrieve the pyasic miner instance."""
if self._miner is None:
try:
miner = run_async_func(pyasic.get_miner(self.ip))
if miner is not None:
self._miner = cast(AnyMiner, miner)

# Set additional parameters like protocol, password,port
if self.protocol == MinerControllerProtocol.RPC:
if isinstance(self._miner.rpc, BaseMinerRPCAPI):
if self.port:
self._miner.rpc.port = self.port
if self.password:
self._miner.rpc.pwd = self.password
else:
if self.logger:
self.logger.error("Unknown PyASIC Miner Controller RPC Protocol")
elif self.protocol == MinerControllerProtocol.WEB:
if isinstance(self._miner.web, BaseWebAPI):
if self.port:
self._miner.web.port = self.port
if self.password:
self._miner.web.pwd = self.password
if self.username:
self._miner.web.username = self.username
else:
if self.logger:
self.logger.error("Unknown PyASIC Miner Controller Web Protocol")
elif self.protocol == MinerControllerProtocol.SSH:
if isinstance(self._miner.ssh, BaseSSH):
if self.port:
self._miner.ssh.port = self.port
if self.password:
self._miner.ssh.pwd = self.password
if self.username:
self._miner.ssh.username = self.username
else:
if self.logger:
self.logger.error("Unknown PyASIC Miner Controller SSH Protocol")
else:
if self.logger:
self.logger.error(f"Unknown PyASIC Miner Controller Protocol: {self.protocol}")

if self.logger:
self.logger.debug(f"Successfully retrieved miner instance from {self.ip}")
except Exception as e:
if self.logger:
self.logger.error(f"Failed to retrieve miner instance from {self.ip}: {e}")

def get_miner_hashrate(self) -> Optional[HashRate]:
"""
Gets the current hash rate, if available.
"""

if self.logger:
self.logger.debug(f"Fetching hashrate from from {self.ip}...")

# Get pyasic miner instance
self._get_miner()

if not self._miner:
if self.logger:
self.logger.error(f"Failed to retrieve miner instance from {self.ip}...")
return None

miner = self._miner
hashrate: Optional[AlgoHashRate] = run_async_func(miner.get_hashrate())
if hashrate is None:
if self.logger:
self.logger.debug(f"Failed to fetch hashrate from {self.ip}...")
return None
real_hashrate = HashRate(value=float(hashrate), unit=str(hashrate.unit))

if self.logger:
self.logger.debug(f"Hashrate fetched: {real_hashrate}")

return real_hashrate

def get_miner_power(self) -> Optional[Watts]:
"""Gets the current power consumption, if available."""
if self.logger:
self.logger.debug(f"Fetching power consumption from from {self.ip}...")

# Get pyasic miner instance
self._get_miner()

if not self._miner:
if self.logger:
self.logger.error(f"Failed to retrieve miner instance from {self.ip}...")
return None

miner = self._miner
wattage = run_async_func(miner.get_wattage())
if wattage is None:
if self.logger:
self.logger.debug(f"Failed to fetch power consumption from {self.ip}...")
return None
power_watts = Watts(wattage)

if self.logger:
self.logger.debug(f"Power consumption fetched: {power_watts}")

return power_watts

def get_miner_status(self) -> MinerStatus:
"""Gets the current operational status of the miner."""
if self.logger:
self.logger.debug(f"Fetching miner status from {self.ip}...")

# Get pyasic miner instance
self._get_miner()

if not self._miner:
if self.logger:
self.logger.error(f"Failed to retrieve miner instance from {self.ip}...")
return MinerStatus.UNKNOWN

miner = self._miner
mining_state = run_async_func(miner.is_mining())

state_map: Dict[Optional[bool], MinerStatus] = {
True: MinerStatus.ON,
False: MinerStatus.OFF,
None: MinerStatus.UNKNOWN,
}

miner_status = state_map.get(mining_state, MinerStatus.UNKNOWN)

if self.logger:
self.logger.debug(f"Miner status fetched: {miner_status}")

return miner_status

def stop_miner(self) -> bool:
"""Attempts to stop the specified miner. Returns True on success request."""
if self.logger:
self.logger.debug(f"Sending stop command to miner at {self.ip}...")

# Get pyasic miner instance
self._get_miner()

if not self._miner:
if self.logger:
self.logger.error(f"Failed to retrieve miner instance from {self.ip}...")
return False

miner = self._miner
success = run_async_func(miner.stop_mining())

if self.logger:
self.logger.debug(f"Stop command sent. Success: {success}")

return success or False

def start_miner(self) -> bool:
"""Attempts to start the miner. Returns True on success request."""
if self.logger:
self.logger.debug(f"Sending start command to miner at {self.ip}...")

# Get pyasic miner instance
self._get_miner()

if not self._miner:
if self.logger:
self.logger.error(f"Failed to retrieve miner instance from {self.ip}...")
return False

miner = self._miner
success = run_async_func(miner.resume_mining())

if self.logger:
self.logger.debug(f"Start command sent. Success: {success}")

return success or False
Loading