|
1 | 1 | """Wrapper module for uhubctl""" |
| 2 | +from .usb import discover_hubs, Hub, Port |
| 3 | +from . import utils |
2 | 4 |
|
3 | | -import re |
4 | | -import subprocess |
5 | | -from typing import List, Optional |
6 | | - |
7 | | -UHUBCTL_BINARY = "uhubctl" |
8 | | - |
9 | | - |
10 | | -def _uhubctl(args: list = None) -> list: |
11 | | - cmd = UHUBCTL_BINARY.split(" ") |
12 | | - |
13 | | - if args is not None: |
14 | | - cmd += args |
15 | | - |
16 | | - try: |
17 | | - result = subprocess.run(cmd, capture_output=True, check=True) |
18 | | - stdout = result.stdout.decode() |
19 | | - |
20 | | - return stdout.split('\n') |
21 | | - except subprocess.CalledProcessError as exc: |
22 | | - stderr = exc.stderr.decode() |
23 | | - |
24 | | - if stderr.startswith("No compatible devices detected"): |
25 | | - return [] |
26 | | - |
27 | | - raise Exception(f"uhubctl failed: {stderr}") from exc |
28 | | - |
29 | | - |
30 | | -def discover_hubs(): |
31 | | - """ |
32 | | - Return list of all by uhubctl supported USB hubs with their ports |
33 | | -
|
34 | | - Returns: |
35 | | - List of hubs |
36 | | -
|
37 | | - """ |
38 | | - hubs = [] |
39 | | - |
40 | | - pattern = re.compile(r"Current status for hub ([\.\d-]+)") |
41 | | - |
42 | | - for line in _uhubctl(): |
43 | | - regex = pattern.match(line) |
44 | | - |
45 | | - if regex: |
46 | | - hub = Hub(regex.group(1), enumerate_ports=True) |
47 | | - hubs.append(hub) |
48 | | - |
49 | | - return hubs |
50 | | - |
51 | | - |
52 | | -class Hub: |
53 | | - """ |
54 | | - USB hub representation from uhubctl |
55 | | - """ |
56 | | - def __init__(self, path: str, enumerate_ports: bool = False) -> None: |
57 | | - """ |
58 | | - Create new hub instance |
59 | | -
|
60 | | - Arguments: |
61 | | - path: USB hub path identifier |
62 | | - enumerate_ports: Automatically enumerate ports |
63 | | - """ |
64 | | - self.path: str = path |
65 | | - self.ports: List[Port] = [] |
66 | | - |
67 | | - if enumerate_ports: |
68 | | - self.discover_ports() |
69 | | - |
70 | | - def add_port(self, port_number: int) -> 'Port': |
71 | | - """ |
72 | | - Add port to hub by port number |
73 | | -
|
74 | | - Arguments: |
75 | | - port_number: Indentification number of port |
76 | | -
|
77 | | - Returns: |
78 | | - Port |
79 | | -
|
80 | | - """ |
81 | | - port = Port(self, port_number) |
82 | | - self.ports.append(port) |
83 | | - |
84 | | - return port |
85 | | - |
86 | | - def add_ports(self, port_start: int, port_end: int): |
87 | | - """ |
88 | | - Add multiple ports to hub |
89 | | -
|
90 | | - Arguments: |
91 | | - port_start: First port's indentification number |
92 | | - port_end: Last port's ndentification number |
93 | | - """ |
94 | | - for port_number in range(port_start, port_end): |
95 | | - self.add_port(port_number) |
96 | | - |
97 | | - def find_port(self, port_number: int) -> Optional['Port']: |
98 | | - """ |
99 | | - Find port by port number |
100 | | -
|
101 | | - Arguments: |
102 | | - port_number: Identification number of port to find |
103 | | -
|
104 | | - Returns: |
105 | | - Port or None |
106 | | - """ |
107 | | - |
108 | | - for port in self.ports: |
109 | | - if port.port_number == int(port_number): |
110 | | - return port |
111 | | - |
112 | | - return None |
113 | | - |
114 | | - def discover_ports(self) -> None: |
115 | | - """ |
116 | | - Discover ports for this hub instance |
117 | | - """ |
118 | | - pattern = re.compile(r" Port (\d+): \d{4} (power|off)") |
119 | | - |
120 | | - for line in _uhubctl(["-l", self.path]): |
121 | | - regex = pattern.match(line) |
122 | | - |
123 | | - if regex: |
124 | | - port = Port(self, regex.group(1)) |
125 | | - self.ports.append(port) |
126 | | - |
127 | | - def __str__(self) -> str: |
128 | | - return f"USB Hub {self.path}" |
129 | | - |
130 | | - |
131 | | -class Port: |
132 | | - """ |
133 | | - USB port representation from uhubctl |
134 | | - """ |
135 | | - def __init__(self, hub: Hub, port_number: int): |
136 | | - """ |
137 | | - Create new port instance |
138 | | -
|
139 | | - Arguments: |
140 | | - hub: Hub to attach port to |
141 | | - port_number: Number of port to create |
142 | | -
|
143 | | - """ |
144 | | - self.hub = hub |
145 | | - self.port_number = int(port_number) |
146 | | - |
147 | | - @property |
148 | | - def status(self) -> bool: |
149 | | - """ |
150 | | - Port power status |
151 | | - """ |
152 | | - status = None |
153 | | - pattern = re.compile(fr" Port {self.port_number}: \d{{4}} (power|off)") |
154 | | - |
155 | | - args = ["-l", self.hub.path, "-p", str(self.port_number)] |
156 | | - for line in _uhubctl(args): |
157 | | - reg = pattern.match(line) |
158 | | - |
159 | | - if reg: |
160 | | - status = (reg.group(1) == "power") |
161 | | - |
162 | | - if status is None: |
163 | | - raise Exception() |
164 | | - |
165 | | - return status |
166 | | - |
167 | | - @status.setter |
168 | | - def status(self, status: bool) -> None: |
169 | | - args = ["-l", self.hub.path, "-p", str(self.port_number), "-a"] |
170 | | - |
171 | | - if status: |
172 | | - args.append("on") |
173 | | - else: |
174 | | - args.append("off") |
175 | | - |
176 | | - _uhubctl(args) |
177 | | - |
178 | | - def __str__(self) -> str: |
179 | | - return f"USB Port {self.hub.path}.{self.port_number}" |
| 5 | +__all__ = ['discover_hubs', 'Hub', 'Port', 'utils'] |
0 commit comments