power: add support for uhubctl devices
Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
326d23a509
commit
ddc0e76ffc
|
@ -1,5 +1,6 @@
|
||||||
# Raspberry Pi Power Control
|
# Power Switch Control
|
||||||
#
|
#
|
||||||
|
# Copyright (C) 2024 Eric Callahan <arksine.code@gmail.com>
|
||||||
# Copyright (C) 2020 Jordan Ruthe <jordanruthe@gmail.com>
|
# Copyright (C) 2020 Jordan Ruthe <jordanruthe@gmail.com>
|
||||||
#
|
#
|
||||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||||
|
@ -10,6 +11,8 @@ import struct
|
||||||
import socket
|
import socket
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
from urllib.parse import quote, urlencode
|
from urllib.parse import quote, urlencode
|
||||||
from ..utils import json_wrapper as jsonw
|
from ..utils import json_wrapper as jsonw
|
||||||
from ..common import RequestType, KlippyState
|
from ..common import RequestType, KlippyState
|
||||||
|
@ -35,6 +38,7 @@ if TYPE_CHECKING:
|
||||||
from .mqtt import MQTTClient
|
from .mqtt import MQTTClient
|
||||||
from .http_client import HttpClient
|
from .http_client import HttpClient
|
||||||
from .klippy_connection import KlippyConnection
|
from .klippy_connection import KlippyConnection
|
||||||
|
from .shell_command import ShellCommandFactory as ShellCommand
|
||||||
|
|
||||||
class PrinterPower:
|
class PrinterPower:
|
||||||
def __init__(self, config: ConfigHelper) -> None:
|
def __init__(self, config: ConfigHelper) -> None:
|
||||||
|
@ -56,6 +60,7 @@ class PrinterPower:
|
||||||
"smartthings": SmartThings,
|
"smartthings": SmartThings,
|
||||||
"hue": HueDevice,
|
"hue": HueDevice,
|
||||||
"http": GenericHTTP,
|
"http": GenericHTTP,
|
||||||
|
"uhubctl": UHubCtl
|
||||||
}
|
}
|
||||||
|
|
||||||
for section in prefix_sections:
|
for section in prefix_sections:
|
||||||
|
@ -1465,6 +1470,108 @@ class GenericHTTP(HTTPDevice):
|
||||||
async def _send_status_request(self) -> str:
|
async def _send_status_request(self) -> str:
|
||||||
return await self._send_generic_request("status")
|
return await self._send_generic_request("status")
|
||||||
|
|
||||||
|
|
||||||
|
HUB_STATE_PATTERN = r"""
|
||||||
|
(?:Port\s(?P<port>[0-9]+):)
|
||||||
|
(?:\s(?P<bits>[0-9a-f]{4}))
|
||||||
|
(?:\s(?P<pstate>power|off))
|
||||||
|
(?P<flags>(?:\s[0-9a-z]+)+)?
|
||||||
|
(?:\s\[(?P<desc>.+)\])?
|
||||||
|
"""
|
||||||
|
|
||||||
|
class UHubCtl(PowerDevice):
|
||||||
|
_uhubctrl_regex = re.compile(
|
||||||
|
r"^\s*" + HUB_STATE_PATTERN + r"\s*$",
|
||||||
|
re.VERBOSE | re.IGNORECASE
|
||||||
|
)
|
||||||
|
def __init__(self, config: ConfigHelper) -> None:
|
||||||
|
super().__init__(config)
|
||||||
|
self.scmd: ShellCommand = self.server.load_component(config, "shell_command")
|
||||||
|
self.location = config.get("location")
|
||||||
|
self.port = config.getint("port")
|
||||||
|
ret = shutil.which("uhubctl")
|
||||||
|
if ret is None:
|
||||||
|
raise config.error(
|
||||||
|
f"[{config.get_name()}]: failed to locate 'uhubctl' binary. "
|
||||||
|
"Make sure uhubctl is correctly installed on the host machine."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def init_state(self) -> None:
|
||||||
|
async with self.request_lock:
|
||||||
|
await self.refresh_status()
|
||||||
|
cur_state = True if self.state == "on" else False
|
||||||
|
if self.initial_state is not None and cur_state != self.initial_state:
|
||||||
|
await self.set_power("on" if self.initial_state else "off")
|
||||||
|
|
||||||
|
async def refresh_status(self) -> None:
|
||||||
|
try:
|
||||||
|
result = await self._run_uhubctl("info")
|
||||||
|
except self.server.error as e:
|
||||||
|
self.state = "error"
|
||||||
|
output = f"\n{e}"
|
||||||
|
if isinstance(e, self.scmd.error):
|
||||||
|
output += f"\nuhubctrl output: {e.stderr.decode(errors='ignore')}"
|
||||||
|
logging.info(f"Power Device {self.name}: Refresh Error{output}")
|
||||||
|
return
|
||||||
|
logging.debug(f"Power Device {self.name}: uhubctl device info: {result}")
|
||||||
|
self.state = result["state"]
|
||||||
|
|
||||||
|
async def set_power(self, state: str) -> None:
|
||||||
|
try:
|
||||||
|
result = await self._run_uhubctl(state)
|
||||||
|
except self.server.error as e:
|
||||||
|
self.state = "error"
|
||||||
|
msg = f"Power Device {self.name}: Error turning device {state}"
|
||||||
|
output = f"\n{e}"
|
||||||
|
if isinstance(e, self.scmd.error):
|
||||||
|
output += f"\nuhubctrl output: {e.stderr.decode(errors='ignore')}"
|
||||||
|
logging.info(f"{msg}{output}")
|
||||||
|
raise self.server.error(msg) from None
|
||||||
|
logging.debug(f"Power Device {self.name}: uhubctl device info: {result}")
|
||||||
|
self.state = result["state"]
|
||||||
|
|
||||||
|
async def _run_uhubctl(self, action: str) -> Dict[str, Any]:
|
||||||
|
cmd = f"uhubctl -l {self.location} -p {self.port}"
|
||||||
|
search_prefix = "Current status"
|
||||||
|
if action in ["on", "off"]:
|
||||||
|
cmd += f" -a {action}"
|
||||||
|
search_prefix = "New status"
|
||||||
|
resp: str = await self.scmd.exec_cmd(cmd, log_complete=False)
|
||||||
|
for line in resp.splitlines():
|
||||||
|
if search_prefix:
|
||||||
|
if line.startswith(search_prefix):
|
||||||
|
search_prefix = ""
|
||||||
|
continue
|
||||||
|
match = self._uhubctrl_regex.match(line.strip())
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
result = match.groupdict()
|
||||||
|
try:
|
||||||
|
port = int(result["port"])
|
||||||
|
status_bits = int(result["bits"], 16)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if port != self.port:
|
||||||
|
continue
|
||||||
|
if result["pstate"] is None:
|
||||||
|
continue
|
||||||
|
state = "on" if result["pstate"] == "power" else "off"
|
||||||
|
flags: List[str] = []
|
||||||
|
if result["flags"] is not None:
|
||||||
|
flags = result["flags"].strip().split()
|
||||||
|
return {
|
||||||
|
"port": port,
|
||||||
|
"status_bits": status_bits,
|
||||||
|
"state": state,
|
||||||
|
"flags": flags,
|
||||||
|
"desc": result["desc"]
|
||||||
|
}
|
||||||
|
raise self.server.error(
|
||||||
|
f"Failed to receive response for device at location {self.location}, "
|
||||||
|
f"port {self.port}, "
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# The power component has multiple configuration sections
|
# The power component has multiple configuration sections
|
||||||
def load_component(config: ConfigHelper) -> PrinterPower:
|
def load_component(config: ConfigHelper) -> PrinterPower:
|
||||||
return PrinterPower(config)
|
return PrinterPower(config)
|
||||||
|
|
Loading…
Reference in New Issue