Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 52 additions & 7 deletions pyasic/miners/backends/luxminer.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,58 @@ async def atm_enabled(self) -> bool | None:
pass
return None

@staticmethod
def _match_profile_name(name: str, profile_names: list[str]) -> str | None:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When calling set_profile, will accept 190MHz, 190mhz, or 190

"""Match user input to an available profile name.

Tries exact match first, then case-insensitive, then checks if
appending "MHz" yields a match (so "190" matches "190MHz").
"""
if name in profile_names:
return name

lower = name.lower()
for p in profile_names:
if p.lower() == lower:
return p

for p in profile_names:
if p.lower() == lower + "mhz":
return p

return None

async def set_profile(self, name: str) -> bool:
config = await self.get_config()

# Validate profile name exists
if not hasattr(config.mining_mode, "available_presets"):
logging.warning(f"{self} - Mining mode does not support profiles")
return False

available_presets = getattr(config.mining_mode, "available_presets", [])
profile_names = [p.name for p in available_presets if p.name is not None]

matched_name = self._match_profile_name(name, profile_names)
if matched_name is None:
logging.warning(
f"{self} - Profile '{name}' not found in available profiles: {profile_names}"
)
return False

try:
result = await self.rpc.profileset(matched_name)
except APIError:
raise
except Exception as e:
logging.warning(f"{self} - Failed to set profile: {e}")
return False

if result["PROFILE"][0]["Profile"] == matched_name:
return True
else:
return False

async def set_power_limit(self, wattage: int) -> bool:
config = await self.get_config()

Expand All @@ -201,17 +253,10 @@ async def set_power_limit(self, wattage: int) -> bool:
return False

# Set power to highest preset <= wattage
# If ATM enabled, must disable it before setting power limit
new_preset = max(valid_presets, key=lambda x: valid_presets[x])

re_enable_atm = False
try:
if await self.atm_enabled():
re_enable_atm = True
await self.rpc.atmset(enabled=False)
result = await self.rpc.profileset(new_preset)
if re_enable_atm:
await self.rpc.atmset(enabled=True)
except APIError:
raise
except Exception as e:
Expand Down
11 changes: 11 additions & 0 deletions pyasic/miners/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,17 @@ async def resume_mining(self) -> bool:
"""
return False

async def set_profile(self, name: str) -> bool:
"""Set the mining profile by name.

Parameters:
name: The name of the profile to switch to.

Returns:
A boolean value of the success of setting the profile.
"""
return False

async def set_power_limit(self, wattage: int) -> bool:
"""Set the power limit to be used by the miner.

Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Tests for LuxOS set_profile() method."""

import unittest
from unittest.mock import AsyncMock, MagicMock

from pyasic.config.mining.presets import MiningPreset


def _make_preset(name, power=1000, frequency=400, voltage=8.9):
"""Helper to create a MiningPreset."""
return MiningPreset(
name=name,
power=power,
hashrate=80.0,
tuned=True,
modded_psu=False,
frequency=frequency,
voltage=voltage,
)


def _make_config(
active_preset_name="415MHz", available_preset_names=("190MHz", "415MHz", "565MHz")
):
"""Helper to create a mock config with presets."""
presets = [_make_preset(n) for n in available_preset_names]
active = next((p for p in presets if p.name == active_preset_name), presets[0])
config = MagicMock()
config.mining_mode.available_presets = presets
config.mining_mode.active_preset = active
return config


class TestSetProfile(unittest.IsolatedAsyncioTestCase):
"""Test LuxOS set_profile() behavior."""

def _make_miner(self, config=None, profileset_result=None):
"""Create a mock LuxOS miner with controllable behavior."""
from pyasic.miners.backends.luxminer import LUXMiner

miner = LUXMiner.__new__(LUXMiner)
miner.rpc = MagicMock()
miner.rpc.profileset = AsyncMock(
return_value=profileset_result or {"PROFILE": [{"Profile": "415MHz"}]}
)
miner.get_config = AsyncMock(return_value=config or _make_config())
miner.ip = "192.168.1.237"
return miner

async def test_switches_profile(self):
"""Should call profileset directly (ATM handled natively by LuxOS)."""
miner = self._make_miner(
profileset_result={"PROFILE": [{"Profile": "190MHz"}]},
)

result = await miner.set_profile("190MHz")

self.assertTrue(result)
miner.rpc.profileset.assert_called_once_with("190MHz")

async def test_invalid_preset_name_returns_false(self):
"""Should return False when preset name doesn't exist."""
miner = self._make_miner()

result = await miner.set_profile("nonexistent_profile")

self.assertFalse(result)
miner.rpc.profileset.assert_not_called()

async def test_fuzzy_match_case_insensitive(self):
"""Should match preset name case-insensitively."""
miner = self._make_miner(
profileset_result={"PROFILE": [{"Profile": "190MHz"}]},
)

result = await miner.set_profile("190mhz")

self.assertTrue(result)
miner.rpc.profileset.assert_called_once_with("190MHz")

async def test_fuzzy_match_number_only(self):
"""Should match '190' to '190MHz'."""
miner = self._make_miner(
profileset_result={"PROFILE": [{"Profile": "190MHz"}]},
)

result = await miner.set_profile("190")

self.assertTrue(result)
miner.rpc.profileset.assert_called_once_with("190MHz")

async def test_profileset_failure_returns_false(self):
"""If RPC profileset fails, should return False."""
miner = self._make_miner()
miner.rpc.profileset.side_effect = Exception("RPC error")

result = await miner.set_profile("190MHz")

self.assertFalse(result)