feature: added the ability to call btminer privileged commands as their own function, and abstracted out most of the complexity.

This commit is contained in:
UpstreamData
2022-10-31 09:46:09 -06:00
parent 7bd5e49412
commit 5c0ac4e665
3 changed files with 128 additions and 216 deletions

View File

@@ -46,8 +46,8 @@ class BaseMinerAPI:
# each function in self
dir(self)
if callable(getattr(self, func)) and
# no __ methods
not func.startswith("__") and
# no __ or _ methods
not func.startswith("__") and not func.startswith("_") and
# remove all functions that are in this base class
func
not in [
@@ -71,14 +71,11 @@ If you are sure you want to use this command please use API.send_command("{comma
)
return return_commands
async def multicommand(
self, *commands: str, ignore_x19_error: bool = False
) -> dict:
async def multicommand(self, *commands: str) -> dict:
"""Creates and sends multiple commands as one command to the miner.
Parameters:
*commands: The commands to send as a multicommand to the miner.
ignore_x19_error: Whether or not to ignore errors raised by x19 miners when using the "+" delimited style.
"""
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
# make sure we can actually run each command, otherwise they will fail
@@ -87,26 +84,44 @@ If you are sure you want to use this command please use API.send_command("{comma
# doesnt work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, x19_command=ignore_x19_error)
data = await self.send_command(command)
except APIError:
logging.debug(f"{self.ip}: Handling X19 multicommand.")
data = await self._x19_multicommand(*command.split("+"))
return {}
logging.debug(f"{self.ip}: Received multicommand data.")
return data
async def _x19_multicommand(self, *commands):
data = None
async def _send_bytes(self, data: bytes) -> bytes:
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, x19_command=True))
except APIError as e:
raise APIError(e)
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
# handle OSError 121
except OSError as e:
if e.winerror == "121":
logging.warning("Semaphore Timeout has Expired.")
return b"{}"
# send the command
writer.write(data)
await writer.drain()
# instantiate data
ret_data = b""
# loop to receive all the data
try:
while True:
d = await reader.read(4096)
if not d:
break
ret_data += d
except Exception as e:
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
return data
logging.warning(f"{self.ip}: API Command Error: - {e}")
# close the connection
writer.close()
await writer.wait_closed()
return ret_data
async def send_command(
self,
@@ -126,43 +141,16 @@ If you are sure you want to use this command please use API.send_command("{comma
Returns:
The return data from the API command parsed from JSON into a dict.
"""
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
# handle OSError 121
except OSError as e:
if e.winerror == "121":
logging.warning("Semaphore Timeout has Expired.")
return {}
# create the command
cmd = {"command": command}
if parameters:
cmd["parameter"] = parameters
# send the command
writer.write(json.dumps(cmd).encode("utf-8"))
await writer.drain()
# instantiate data
data = b""
# loop to receive all the data
try:
while True:
d = await reader.read(4096)
if not d:
break
data += d
except Exception as e:
logging.warning(f"{self.ip}: API Command Error: - {e}")
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
data = self._load_api_data(data)
# close the connection
writer.close()
await writer.wait_closed()
# check for if the user wants to allow errors to return
if not ignore_errors:
# validate the command succeeded
@@ -174,6 +162,9 @@ If you are sure you want to use this command please use API.send_command("{comma
return data
async def send_privileged_command(self, *args, **kwargs) -> dict:
return await self.send_command(*args, **kwargs)
@staticmethod
def _validate_command_output(data: dict) -> tuple:
# check if the data returned is correct or an error

View File

@@ -11,8 +11,10 @@
# 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 logging
from pyasic.API import BaseMinerAPI
from pyasic.API import APIError
class BMMinerAPI(BaseMinerAPI):
@@ -36,6 +38,37 @@ class BMMinerAPI(BaseMinerAPI):
def __init__(self, ip: str, port: int = 4028) -> None:
super().__init__(ip, port)
async def multicommand(
self, *commands: str, ignore_x19_error: bool = False
) -> dict:
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesnt work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, x19_command=ignore_x19_error)
except APIError:
logging.debug(f"{self.ip}: Handling X19 multicommand.")
data = await self._x19_multicommand(*command.split("+"))
logging.debug(f"{self.ip}: Received multicommand data.")
return data
async def _x19_multicommand(self, *commands):
data = None
try:
data = {}
# send all commands individually
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, x19_command=True))
except APIError as e:
raise APIError(e)
except Exception as e:
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
return data
async def version(self) -> dict:
"""Get miner version info.
<details>

View File

@@ -187,57 +187,24 @@ class BTMinerAPI(BaseMinerAPI):
self.pwd = pwd
self.current_token = None
async def send_command(
self,
command: Union[str, bytes],
parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
**kwargs,
async def send_privileged_command(
self, command: Union[str, bytes], ignore_errors: bool = False, **kwargs
) -> dict:
# check if command is a string
# if its bytes its encoded and needs to be sent raw
if isinstance(command, str):
# if it is a string, put it into the standard command format
command = json.dumps({"command": command}).encode("utf-8")
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
# handle OSError 121
except OSError as e:
if e.winerror == "121":
print("Semaphore Timeout has Expired.")
return {}
command = {"cmd": command}
for kwarg in kwargs:
if kwargs[kwarg]:
command[kwarg] = kwargs[kwarg]
# send the command
writer.write(command)
await writer.drain()
# instantiate data
data = b""
# loop to receive all the data
try:
while True:
d = await reader.read(4096)
if not d:
break
data += d
except Exception as e:
logging.info(f"{str(self.ip)}: {e}")
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
data = await self._send_bytes(enc_command)
data = self._load_api_data(data)
# close the connection
writer.close()
await writer.wait_closed()
# check if the returned data is encoded
if "enc" in data.keys():
# try to parse the encoded data
try:
data = parse_btminer_priviledge_data(self.current_token, data)
except Exception as e:
logging.info(f"{str(self.ip)}: {e}")
try:
data = parse_btminer_priviledge_data(self.current_token, data)
except Exception as e:
logging.info(f"{str(self.ip)}: {e}")
if not ignore_errors:
# if it fails to validate, it is likely an error
@@ -320,46 +287,18 @@ class BTMinerAPI(BaseMinerAPI):
A dict from the API to confirm the pools were updated.
</details>
"""
# get the token and password from the miner
token_data = await self.get_token()
# parse pool data
if not pool_1:
raise APIError("No pools set.")
elif pool_2 and pool_3:
command = {
"cmd": "update_pools",
"pool1": pool_1,
"worker1": worker_1,
"passwd1": passwd_1,
"pool2": pool_2,
"worker2": worker_2,
"passwd2": passwd_2,
"pool3": pool_3,
"worker3": worker_3,
"passwd3": passwd_3,
}
elif pool_2:
command = {
"cmd": "update_pools",
"pool1": pool_1,
"worker1": worker_1,
"passwd1": passwd_1,
"pool2": pool_2,
"worker2": worker_2,
"passwd2": passwd_2,
}
else:
command = {
"cmd": "update_pools",
"pool1": pool_1,
"worker1": worker_1,
"passwd1": passwd_1,
}
# encode the command with the token data
enc_command = create_privileged_cmd(token_data, command)
# send the command
return await self.send_command(enc_command)
return await self.send_privileged_command(
"update_pools",
pool1=pool_1,
worker1=worker_1,
passwd1=passwd_1,
pool2=pool_2,
worker2=worker_2,
passwd2=passwd_2,
pool3=pool_3,
worker3=worker_3,
passwd3=passwd_3,
)
async def restart(self) -> dict:
"""Restart BTMiner using the API.
@@ -373,10 +312,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the restart.
</details>
"""
command = {"cmd": "restart_btminer"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("restart_btminer")
async def power_off(self, respbefore: bool = True) -> dict:
"""Power off the miner using the API.
@@ -393,12 +329,8 @@ class BTMinerAPI(BaseMinerAPI):
</details>
"""
if respbefore:
command = {"cmd": "power_off", "respbefore": "true"}
else:
command = {"cmd": "power_off", "respbefore": "false"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("power_off", respbefore="true")
return await self.send_privileged_command("power_off", respbefore="false")
async def power_on(self) -> dict:
"""Power on the miner using the API.
@@ -413,10 +345,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of powering on.
</details>
"""
command = {"cmd": "power_on"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("power_on")
async def reset_led(self) -> dict:
"""Reset the LED on the miner using the API.
@@ -431,10 +360,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of resetting the LED.
</details>
"""
command = {"cmd": "set_led", "param": "auto"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.set_led(auto=True)
async def set_led(
self,
@@ -462,19 +388,11 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting the LED.
</details>
"""
if not auto:
command = {
"cmd": "set_led",
"color": color,
"period": period,
"duration": duration,
"start": start,
}
else:
command = {"cmd": "set_led", "param": "auto"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command, ignore_errors=True)
if auto:
return await self.send_privileged_command("set_led", param=auto)
return await self.send_privileged_command(
"set_led", color=color, period=period, duration=duration, start=start
)
async def set_low_power(self) -> dict:
"""Set low power mode on the miner using the API.
@@ -489,10 +407,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting low power mode.
</details>
"""
command = {"cmd": "set_low_power"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("set_low_power")
async def update_firmware(self): # noqa - static
"""Not implemented."""
@@ -510,10 +425,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of the reboot.
</details>
"""
command = {"cmd": "reboot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("reboot")
async def factory_reset(self) -> dict:
"""Reset the miner to factory defaults.
@@ -525,10 +437,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of the reset.
</details>
"""
command = {"cmd": "factory_reset"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("factory_reset")
async def update_pwd(self, old_pwd: str, new_pwd: str) -> dict:
"""Update the admin user's password.
@@ -555,11 +464,10 @@ class BTMinerAPI(BaseMinerAPI):
f"New password too long, the max length is 8. "
f"Password size: {len(new_pwd.encode('utf-8'))}"
)
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
try:
data = await self.send_command(enc_command)
data = await self.send_privileged_command(
"update_pwd", old=old_pwd, new=new_pwd
)
except APIError as e:
raise e
self.pwd = new_pwd
@@ -588,10 +496,9 @@ class BTMinerAPI(BaseMinerAPI):
f"range. Please set a % between -10 and "
f"100"
)
command = {"cmd": "set_target_freq", "percent": str(percent)}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command(
"set_target_freq", percent=str(percent)
)
async def enable_fast_boot(self) -> dict:
"""Turn on fast boot.
@@ -607,10 +514,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of enabling fast boot.
</details>
"""
command = {"cmd": "enable_btminer_fast_boot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("enable_btminer_fast_boot")
async def disable_fast_boot(self) -> dict:
"""Turn off fast boot.
@@ -626,10 +530,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of disabling fast boot.
</details>
"""
command = {"cmd": "disable_btminer_fast_boot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("disable_btminer_fast_boot")
async def enable_web_pools(self) -> dict:
"""Turn on web pool updates.
@@ -645,10 +546,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of enabling web pools.
</details>
"""
command = {"cmd": "enable_web_pools"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("enable_web_pools")
async def disable_web_pools(self) -> dict:
"""Turn off web pool updates.
@@ -664,10 +562,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of disabling web pools.
</details>
"""
command = {"cmd": "disable_web_pools"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("disable_web_pools")
async def set_hostname(self, hostname: str) -> dict:
"""Set the hostname of the miner.
@@ -685,10 +580,7 @@ class BTMinerAPI(BaseMinerAPI):
A reply informing of the status of setting the hostname.
</details>
"""
command = {"cmd": "set_hostname", "hostname": hostname}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("set_hostname", hostname=hostname)
async def set_power_pct(self, percent: int) -> dict:
"""Set the power percentage of the miner.
@@ -713,10 +605,7 @@ class BTMinerAPI(BaseMinerAPI):
f"range. Please set a % between 0 and "
f"100"
)
command = {"cmd": "set_power_pct", "percent": str(percent)}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command("set_power_pct", percent=str(percent))
async def pre_power_on(self, complete: bool, msg: str) -> dict:
"""Configure or check status of pre power on.
@@ -747,13 +636,12 @@ class BTMinerAPI(BaseMinerAPI):
'"adjust continue"]'
)
if complete:
complete = "true"
else:
complete = "false"
command = {"cmd": "pre_power_on", "complete": complete, "msg": msg}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
return await self.send_privileged_command(
"pre_power_on", complete="true", msg=msg
)
return await self.send_privileged_command(
"pre_power_on", complete="false", msg=msg
)
#### END privileged COMMANDS ####