refactor: move lab discovery to fiwicontrol.lab; remote setup to commands

- Add fiwicontrol.lab package (discovery, inventory_config, inventory_verify)
  with default lab INI configs/default.ini (FIWI_LAB_INI override).
- Move remote_setup to fiwicontrol.commands; add python -m fiwicontrol.commands.
- Power CLI keeps --discovery-json, --list-power-devices, --verify-inventory;
  add -c/--config for inventory paths; drop --setup-remote (use commands module).
- Re-export lab symbols from fiwicontrol.power for compatibility.
- Update docs, scripts, and tests.

Made-with: Cursor
This commit is contained in:
Robert McMahon 2026-04-10 18:39:43 -07:00
parent 72048e8a62
commit 4ad0b4b249
19 changed files with 240 additions and 139 deletions

View File

@ -6,8 +6,9 @@ Tools and libraries for managing and testing Umber FiWi networks.
This repository ships that distribution (**`fiwicontrol`** on PyPI / `pip`) with import root **`fiwicontrol`**:
1. **`fiwicontrol.commands`** — run commands on remote rigs via OpenSSH or **`ush`**, with asyncio streaming, timeouts, repeats (`Command`), and a small registry (`CommandManager`). Implementation: `src/fiwicontrol/commands/node_control.py`.
2. **`fiwicontrol.power`** — discovery plus **`Power`** (``.on()`` / ``.off()`` / ``.voltage()`` / ``.current()`` / ``.watts()``) delegating to **`AcronamePower`** and **`MonsoonPower`**. May import **`fiwicontrol.commands`**; the reverse is forbidden.
1. **`fiwicontrol.commands`** — run commands on remote rigs via OpenSSH or **`ush`** (`Command`, `CommandManager`, `ssh_node`). Remote Pi bootstrap: **`python -m fiwicontrol.commands`** (`--setup-remote`). Must not import **`fiwicontrol.power`**.
2. **`fiwicontrol.lab`** — USB discovery (Acroname / Monsoon) and **`configs/*.ini`** inventory load + verify. Imports **`fiwicontrol.commands`** for SSH discovery only.
3. **`fiwicontrol.power`** — **`Power`** (``.on()`` / ``.off()`` / ``.voltage()`` / …) via **`AcronamePower`** and **`MonsoonPower`**, plus a small CLI (**`--discovery-json`**, **`--list-power-devices`**, **`--verify-inventory`** with **`-c`**). Re-exports **`fiwicontrol.lab`** discovery/inventory for compatibility. May import **`fiwicontrol.commands`** and **`fiwicontrol.lab`**; the reverse is forbidden.
**Layout**
@ -24,9 +25,20 @@ FiWiControl/
│ └── fiwicontrol/
│ ├── commands/
│ │ ├── __init__.py
│ │ └── node_control.py
│ │ ├── __main__.py
│ │ ├── node_control.py
│ │ └── remote_setup.py
│ ├── lab/
│ │ ├── __init__.py
│ │ ├── discovery.py
│ │ ├── inventory_config.py
│ │ └── inventory_verify.py
│ └── power/
│ └── __init__.py
│ ├── __init__.py
│ ├── __main__.py
│ ├── acroname.py
│ ├── control.py
│ └── monsoon.py
└── tests/
```
@ -76,12 +88,13 @@ cd ~/Code/FiWiControl
FIWI_REMOTE_IP=192.168.1.39 pytest -q tests/test_node_control.py
```
Power / USB inventory (needs **`brainstem`** and **`pyserial`**, e.g. **`pip install -e ".[power]"`**): see **`docs/power-control-and-inventory.md`** for **what must be installed on the remote** (e.g. Pi) and how discovery over SSH works. **`configs/clubhouse.ini`** lists expected Acroname hubs as **`Stem:ports`** (downstream USB count) and **`monsoon_count`** per **`[inventory.host.*]`**. Compare live discovery to the INI (local host + SSH to the Pi for remote rows):
Power / USB inventory (needs **`brainstem`** and **`pyserial`**, e.g. **`pip install -e ".[power]"`**): see **`docs/power-control-and-inventory.md`**. Lab layout is **`configs/default.ini`** by default (override with **`-c`**). **`configs/clubhouse.ini`** is an alternate example file.
```bash
cd ~/Code/FiWiControl
python -m pip install -e ".[power]"
python -m fiwicontrol.power --verify-inventory configs/clubhouse.ini
python -m fiwicontrol.power --verify-inventory
# or: python -m fiwicontrol.power -c configs/clubhouse.ini --verify-inventory
```
The same check from **pytest** (skipped unless enabled):

View File

@ -3,7 +3,7 @@
# [node.*] — SSH targets (passwordless root for automation).
# [inventory.host.*] — expected Acroname hubs (stem_class:downstream_usb_ports)
# and Monsoon serial devices (count). Used by:
# python -m fiwicontrol.power --verify-inventory configs/clubhouse.ini
# python -m fiwicontrol.power -c configs/clubhouse.ini --verify-inventory
[site]
name = ClubHouse

37
configs/default.ini Normal file
View File

@ -0,0 +1,37 @@
# FiWiControl — ClubHouse lab nodes + power / USB inventory
#
# [node.*] — SSH targets (passwordless root for automation).
# [inventory.host.*] — expected Acroname hubs (stem_class:downstream_usb_ports)
# and Monsoon serial devices (count). Used by:
# python -m fiwicontrol.power --verify-inventory
# (default INI: configs/default.ini; override: -c configs/clubhouse.ini)
[site]
name = ClubHouse
[node.localhost]
label = local
ipaddr = 127.0.0.1
sshtype = ssh
description = Fedora workstation (local BrainStem over USB).
[node.raspberry_pi5]
label = pi5
ipaddr = 192.168.1.39
sshtype = ssh
description = Raspberry Pi 5 — two USBHub3p (8 downstream USB each), one USBHub2x4, Monsoon HVPM on USB.
# --- Expected USB lab devices (multiset match vs discovery) ---
[inventory.host.localhost]
mode = local
# One programmable 4-port USB hub (USBHub2x4) on this machine.
acroname = USBHub2x4:4
monsoon_count = 0
[inventory.host.pi5]
mode = remote
node = raspberry_pi5
# Match ``python3 -m fiwicontrol.power --discovery-json`` on the Pi (stem_class:downstream_ports); one Monsoon.
acroname = USBHub3p:8, USBHub3p:8, USBHub2x4:4
monsoon_count = 1

View File

@ -17,7 +17,7 @@ Use this order the first time you (or someone else) joins the project or replace
| 2 | Workstation | **Python 3.11+** and an **editable install**: **`python3 -m pip install -e ".[dev]"`**, then **`python3 -m pip install -e ".[power]"`** if you use Acroname/Monsoon or remote power tests (see **Workstation** below). |
| 3 | Workstation → rig | **Passwordless SSH as root:** **`ssh -o BatchMode=yes root@192.168.1.39 true`** must exit **0** with no password prompt. On the rig, install your workstations **public key** in **`/root/.ssh/authorized_keys`** and ensure **`sshd`** allows **`root`** with pubkey auth. |
| 4 | Rig (e.g. Pi) | **`git clone`** the same repo where you will run power/discovery (often **`/root/Code/FiWiControl`**). Your **`--remote-repo`** path must match. |
| 5 | Workstation | From your clone, run **`--setup-remote`** (PEP 668 / Raspberry Pi OS needs **`--break-system-packages`** or **`FIWI_REMOTE_PIP_FLAGS`**). See **Remote host setup** §3§4 below. |
| 5 | Workstation | From your clone, run **`python3 -m fiwicontrol.commands`** with the Pi IP and **`--remote-repo`** (PEP 668 / Raspberry Pi OS needs **`--break-system-packages`** or **`FIWI_REMOTE_PIP_FLAGS`**). See **Remote host setup** §3§4 below. |
| 6 | Workstation | **Verify** (minimal): **`pytest -q tests/test_package_layout.py`**, then **`FIWI_REMOTE_IP=… pytest -q tests/test_node_control.py`**, then **`pytest -q tests/test_remote_power_dependencies.py`**. Default **`FIWI_REMOTE_IP`** is **`192.168.1.39`** if unset. For a full post-install checklist (INI, **`--verify-inventory`**, …), see **`docs/power-control-and-inventory.md`** → **“Verification checklist (after install)”**. |
**SSH user:** **`ssh_node`** ( **`sshtype="ssh"`** ) always uses an explicit **`root@<ipaddr>`** (or whatever **`ssh_session`** **`user`** is set to). It does **not** open **`ssh 192.168.1.39`** with your local login name, so your manual tests should use **`root@…`** as well.
@ -99,7 +99,7 @@ Then:
```bash
cd ~/Code/FiWiControl
python3 -m fiwicontrol.power --setup-remote 192.168.1.39 --remote-repo /root/Code/FiWiControl --break-system-packages
python3 -m fiwicontrol.commands 192.168.1.39 --remote-repo /root/Code/FiWiControl --break-system-packages
```
**Raspberry Pi OS / Debian (PEP 668):** system Python blocks **`pip install`** without **`--break-system-packages`**. Prefer the **`--break-system-packages`** flag above.
@ -110,7 +110,7 @@ python3 -m fiwicontrol.power --setup-remote 192.168.1.39 --remote-repo /root/Cod
- **`--pip-flags=--break-system-packages`** (equals form), or
- **`export FIWI_REMOTE_PIP_FLAGS='--break-system-packages'`** and omit **`--pip-flags`**.
**Wrapper:** **`scripts/setup_pi_power.sh`** sets **`PYTHONPATH=src`** on Fedora if needed, then runs **`--setup-remote`** using **`FIWI_REMOTE_IP`** and **`FIWI_REMOTE_REPO`**:
**Wrapper:** **`scripts/setup_pi_power.sh`** sets **`PYTHONPATH=src`** on Fedora if needed, then runs **`python3 -m fiwicontrol.commands`** (**`--setup-remote`**-style args) using **`FIWI_REMOTE_IP`** and **`FIWI_REMOTE_REPO`**:
```bash
cd ~/Code/FiWiControl
@ -131,7 +131,7 @@ If **`pip show fiwicontrol`** reports **`Location: …/python3.11/…`** but pla
export FIWI_REMOTE_PYTHON=python3.11 # or full path, e.g. /usr/bin/python3.11
```
That variable is read by **`discover_devices_remote_async`**, **`tests/test_remote_power_dependencies.py`**, and **`python3 -m fiwicontrol.power --setup-remote --remote-python …`**.
That variable is read by **`discover_devices_remote_async`**, **`tests/test_remote_power_dependencies.py`**, and **`python3 -m fiwicontrol.commands … --remote-python …`** (remote setup).
### 5. Verify after install

View File

@ -1,7 +1,7 @@
# Power control, discovery, and INI inventory (FiWiControl)
**Package:** **`fiwicontrol.power`**
**Code:** `src/fiwicontrol/power/` (discovery, **`AcronamePower`**, **`MonsoonPower`**, **`Power`** facade, INI inventory helpers).
**Packages:** **`fiwicontrol.lab`** (discovery + INI inventory), **`fiwicontrol.power`** (**`Power`**, **`AcronamePower`**, **`MonsoonPower`**, CLI wrappers).
**Code:** `src/fiwicontrol/lab/`, `src/fiwicontrol/power/`.
This document describes **what must be installed on remote rigs** (e.g. a Raspberry Pi 5) when you use **remote discovery** or **`--verify-inventory`** with `mode = remote` in your INI. It also explains **how to edit the lab INI** and **how to verify** a new install end-to-end.
@ -42,7 +42,7 @@ Every remote **`[inventory.host.*]`** must set **`node = <id>`** where **`<id>`*
| **`acroname`** | no (default empty) | Comma-separated list of **`StemClass:downstream_usb_ports`**. Each entry is one expected Acroname / BrainStem module (by **stem class** and **downstream USB port count**). Order does not matter; duplicates are allowed (**multiset** match vs discovery). Example: **`USBHub2x4:4, USBHub3p:8`**. |
| **`monsoon_count`** | no (default **`0`**) | Exact number of Monsoon-class USB serial devices discovery must find on that host. |
**Finding stem names and port counts:** run **`python3 -m fiwicontrol.power --discovery-json`** on the host (or **`--list-power-devices your.ini`** once you have a draft INI) and read each Acroname rows **`stem_class`** and **`downstream_usb_ports`**. Those strings and integers must match your **`acroname`** entries.
**Finding stem names and port counts:** run **`python3 -m fiwicontrol.power --discovery-json`** on the host (or **`python3 -m fiwicontrol.power --list-power-devices`** with **`-c`** pointing at your draft INI) and read each Acroname rows **`stem_class`** and **`downstream_usb_ports`**. Those strings and integers must match your **`acroname`** entries.
**Empty hardware:** **`acroname`** may be left blank and **`monsoon_count = 0`** if that host has no power/USB inventory to track.
@ -99,7 +99,8 @@ If **`fiwicontrol`** is only on `PYTHONPATH` (no install), ensure the remote she
**List attached power/USB devices on every inventory host** (this machine + each `mode=remote` SSH target; addresses in each section header):
```bash
python3 -m fiwicontrol.power --list-power-devices configs/clubhouse.ini
python3 -m fiwicontrol.power --list-power-devices
# or: python3 -m fiwicontrol.power -c configs/clubhouse.ini --list-power-devices
```
**Sanity on the Pi** (after install):
@ -198,7 +199,8 @@ From your **workstation** (repo root, passwordless **`ssh root@<pi-ip>`** workin
```bash
cd ~/Code/FiWiControl
python3 -m fiwicontrol.power --verify-inventory configs/clubhouse.ini
python3 -m fiwicontrol.power --verify-inventory
# or: python3 -m fiwicontrol.power -c configs/clubhouse.ini --verify-inventory
echo "exit code: $?"
```
@ -250,8 +252,8 @@ Run these **in order** on your **workstation** (repo root, **`pip install -e ".[
8. **INI matches your lab (workstation)**
Edit **`configs/*.ini`** (see **Lab INI file reference** above). Then:
**`python3 -m fiwicontrol.power --list-power-devices configs/your.ini`** — confirms discovery per host with headers showing addresses.
**`python3 -m fiwicontrol.power --verify-inventory configs/your.ini`** — must print **`OK: discovery matches INI…`** (exit **0**). If you see **`MISMATCH`**, adjust **`acroname`** / **`monsoon_count`** or fix hardware/cabling.
**`python3 -m fiwicontrol.power --list-power-devices`** (optional **`-c path/to.ini`**; default **`configs/default.ini`**) — confirms discovery per host with headers showing addresses.
**`python3 -m fiwicontrol.power --verify-inventory`** — same **`-c`** rules; must print **`OK: discovery matches INI…`** (exit **0**). If you see **`MISMATCH`**, adjust **`acroname`** / **`monsoon_count`** or fix hardware/cabling.
9. **Optional strict pytest (workstation)**
**`FIWI_VERIFY_POWER_INI=1 pytest -q tests/test_inventory_verify_live.py`** — opt-in live test that compares discovery to **`configs/clubhouse.ini`** (edit the test or your INI if your lab uses a different file).
@ -276,7 +278,7 @@ See **`configs/clubhouse.ini`** for a full example and inline comments.
```bash
cd ~/Code/FiWiControl
python3 -m pip install -e ".[power]"
python3 -m fiwicontrol.power --verify-inventory configs/clubhouse.ini
python3 -m fiwicontrol.power --verify-inventory
```
**Full inventory match including hardware** (pytest, opt-in):

View File

@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "fiwicontrol"
version = "0.1.0"
description = "FiWiControl repo: fiwicontrol.commands (SSH/ush) and fiwicontrol.power in one installable package."
description = "FiWiControl repo: commands (SSH/ush), lab (USB discovery + INI inventory), power (Acroname/Monsoon)."
readme = "README.md"
requires-python = ">=3.11"
license = { file = "LICENSE" }

View File

@ -9,4 +9,4 @@ export FIWI_REMOTE_REPO="${FIWI_REMOTE_REPO:-/root/Code/FiWiControl}"
if ! python3 -c "import fiwicontrol.power" 2>/dev/null; then
export PYTHONPATH="${ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}"
fi
exec python3 -m fiwicontrol.power --setup-remote "$FIWI_REMOTE_IP" --remote-repo "$FIWI_REMOTE_REPO" "$@"
exec python3 -m fiwicontrol.commands "$FIWI_REMOTE_IP" --remote-repo "$FIWI_REMOTE_REPO" "$@"

View File

@ -0,0 +1,19 @@
# Copyright (c) 2026 Umber
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""``python -m fiwicontrol.commands`` — remote lab host bootstrap (``--setup-remote``)."""
from __future__ import annotations
import sys
from fiwicontrol.commands.remote_setup import main_setup
def main() -> int:
return main_setup(sys.argv[1:])
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,65 @@
# Copyright (c) 2026 Umber
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Lab USB topology: BrainStem / Monsoon discovery and INI inventory."""
from fiwicontrol.lab.discovery import (
MONSOON_USB_PIDS,
MONSOON_USB_VID,
AcronameModuleInfo,
MonsoonPortInfo,
acroname_module_info_from_spec,
discover_acroname_modules,
discover_acroname_modules_async,
discover_devices_remote_async,
discover_monsoon_serial_ports,
discover_monsoon_serial_ports_async,
discovery_json,
discovery_payload_dict,
parse_remote_discovery_payload,
)
from fiwicontrol.lab.inventory_config import (
HostInventorySpec,
InventoryDocument,
NodeSection,
default_lab_ini_path,
load_inventory_ini,
)
from fiwicontrol.lab.inventory_verify import (
compare_host_inventory,
dump_inventory_discovery_ini_sync,
main_dump_discovery,
main_verify,
verify_inventory_document,
verify_inventory_ini,
verify_inventory_ini_sync,
)
__all__ = [
"MONSOON_USB_PIDS",
"MONSOON_USB_VID",
"AcronameModuleInfo",
"MonsoonPortInfo",
"HostInventorySpec",
"InventoryDocument",
"NodeSection",
"acroname_module_info_from_spec",
"compare_host_inventory",
"default_lab_ini_path",
"discover_acroname_modules",
"discover_acroname_modules_async",
"discover_devices_remote_async",
"discover_monsoon_serial_ports",
"discover_monsoon_serial_ports_async",
"discovery_json",
"discovery_payload_dict",
"dump_inventory_discovery_ini_sync",
"load_inventory_ini",
"main_dump_discovery",
"main_verify",
"parse_remote_discovery_payload",
"verify_inventory_document",
"verify_inventory_ini",
"verify_inventory_ini_sync",
]

View File

@ -7,10 +7,16 @@
from __future__ import annotations
import configparser
import os
from dataclasses import dataclass
from pathlib import Path
def default_lab_ini_path() -> Path:
"""Default lab inventory INI (``configs/default.ini``, or ``FIWI_LAB_INI``)."""
return Path(os.environ.get("FIWI_LAB_INI", "configs/default.ini")).expanduser()
@dataclass(frozen=True)
class NodeSection:
"""Resolved ``[node.ID]`` entry."""

View File

@ -13,13 +13,13 @@ import sys
from pathlib import Path
from typing import Any, TextIO
from fiwicontrol.power.discovery import (
from fiwicontrol.lab.discovery import (
AcronameModuleInfo,
discovery_payload_dict,
discover_devices_remote_async,
parse_remote_discovery_payload,
)
from fiwicontrol.power.inventory_config import HostInventorySpec, InventoryDocument, load_inventory_ini
from fiwicontrol.lab.inventory_config import HostInventorySpec, InventoryDocument, default_lab_ini_path, load_inventory_ini
def _workstation_ipv4_addrs() -> str:
@ -80,15 +80,22 @@ def main_dump_discovery(argv: list[str] | None) -> int:
import argparse
p = argparse.ArgumentParser(description="Print BrainStem/Monsoon discovery per [inventory.host.*] entry.")
p.add_argument("ini", type=Path, help="INI path (e.g. configs/clubhouse.ini)")
p.add_argument(
"-c",
"--config",
type=Path,
default=None,
help="Lab inventory INI (default: configs/default.ini or $FIWI_LAB_INI).",
)
p.add_argument(
"--ssh-controlmaster",
action="store_true",
help="Use ssh_node ControlMaster mode for remote hosts.",
)
args = p.parse_args(argv)
ini = args.config if args.config is not None else default_lab_ini_path()
try:
dump_inventory_discovery_ini_sync(args.ini, ssh_controlmaster=args.ssh_controlmaster)
dump_inventory_discovery_ini_sync(ini, ssh_controlmaster=args.ssh_controlmaster)
except Exception as exc:
print("Failed: {}".format(exc), file=sys.stderr)
return 1
@ -184,15 +191,22 @@ def main_verify(argv: list[str] | None) -> int:
import argparse
parser = argparse.ArgumentParser(description="Verify power/USB discovery vs INI inventory.")
parser.add_argument("ini", type=Path, help="Path to INI (e.g. configs/clubhouse.ini)")
parser.add_argument(
"-c",
"--config",
type=Path,
default=None,
help="Lab inventory INI (default: configs/default.ini or $FIWI_LAB_INI).",
)
parser.add_argument(
"--ssh-controlmaster",
action="store_true",
help="Use ssh_node ControlMaster mode for remote hosts (matches some lab setups).",
)
args = parser.parse_args(argv)
ini = args.config if args.config is not None else default_lab_ini_path()
try:
doc = load_inventory_ini(args.ini)
doc = load_inventory_ini(ini)
except Exception as exc:
print("Failed to load INI: {}".format(exc), file=sys.stderr)
return 2
@ -207,7 +221,7 @@ def main_verify(argv: list[str] | None) -> int:
)
)
errs = verify_inventory_ini_sync(args.ini, ssh_controlmaster=args.ssh_controlmaster)
errs = verify_inventory_ini_sync(ini, ssh_controlmaster=args.ssh_controlmaster)
if errs:
print("MISMATCH:", file=sys.stderr)
for line in errs:

View File

@ -1,16 +1,15 @@
"""
Power switching, monitoring, and discovery (Acroname, Monsoon, ).
Power switching, monitoring (Acroname, Monsoon, ).
May import ``fiwicontrol.commands``; the reverse is forbidden.
Lab USB discovery and INI inventory live in ``fiwicontrol.lab``; this package re-exports
them for compatibility. May import ``fiwicontrol.commands``; the reverse is forbidden.
"""
from fiwicontrol.power.acroname import AcronamePower
from fiwicontrol.power.control import Power
from fiwicontrol.power.discovery import (
AcronameModuleInfo,
MonsoonPortInfo,
from fiwicontrol.lab.discovery import (
MONSOON_USB_PIDS,
MONSOON_USB_VID,
AcronameModuleInfo,
MonsoonPortInfo,
acroname_module_info_from_spec,
discover_acroname_modules,
discover_acroname_modules_async,
@ -21,13 +20,21 @@ from fiwicontrol.power.discovery import (
discovery_payload_dict,
parse_remote_discovery_payload,
)
from fiwicontrol.power.inventory_config import InventoryDocument, HostInventorySpec, NodeSection, load_inventory_ini
from fiwicontrol.power.inventory_verify import (
from fiwicontrol.lab.inventory_config import (
HostInventorySpec,
InventoryDocument,
NodeSection,
default_lab_ini_path,
load_inventory_ini,
)
from fiwicontrol.lab.inventory_verify import (
compare_host_inventory,
verify_inventory_document,
verify_inventory_ini,
verify_inventory_ini_sync,
)
from fiwicontrol.power.acroname import AcronamePower
from fiwicontrol.power.control import Power
from fiwicontrol.power.monsoon import MonsoonPower, MonsoonReading
__all__ = [
@ -39,6 +46,7 @@ __all__ = [
"NodeSection",
"Power",
"compare_host_inventory",
"default_lab_ini_path",
"discovery_payload_dict",
"load_inventory_ini",
"verify_inventory_document",

View File

@ -2,26 +2,34 @@
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
#
"""CLI: ``--discovery-json``, ``--list-power-devices``, ``--verify-inventory``, ``--setup-remote``."""
"""CLI: ``--discovery-json``, ``--list-power-devices``, ``--verify-inventory`` (lab INI via ``-c``)."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from fiwicontrol.power.discovery import discovery_json
from fiwicontrol.power.inventory_verify import main_dump_discovery, main_verify
from fiwicontrol.power.remote_setup import main_setup
from fiwicontrol.lab.discovery import discovery_json
from fiwicontrol.lab.inventory_config import default_lab_ini_path
from fiwicontrol.lab.inventory_verify import main_dump_discovery, main_verify
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="python -m fiwicontrol.power",
epilog=(
"Remote Pi (PEP 668): use --break-system-packages or --pip-flags=--break-system-packages "
"(do not put a space before --break-system-packages after --pip-flags)."
"Remote Pi install: use ``python -m fiwicontrol.commands --setup-remote …`` "
"(PEP 668: ``--break-system-packages`` or ``--pip-flags=--break-system-packages``)."
),
)
parser.add_argument(
"-c",
"--config",
type=Path,
default=None,
help="Lab inventory INI for --list-power-devices / --verify-inventory (default: configs/default.ini or $FIWI_LAB_INI).",
)
parser.add_argument(
"--discovery-json",
action="store_true",
@ -29,100 +37,32 @@ def main(argv: list[str] | None = None) -> int:
)
parser.add_argument(
"--list-power-devices",
dest="list_power_devices_ini",
metavar="INI",
type=str,
default=None,
help=(
"Load INI and print BrainStem/Monsoon discovery for each [inventory.host.*] "
"(local: workstation addresses; remote: SSH target IP from [node.*])."
),
action="store_true",
help="Print discovery JSON for each [inventory.host.*] in the lab INI (-c).",
)
parser.add_argument(
"--verify-inventory",
dest="verify_ini",
metavar="INI",
type=str,
default=None,
help="Compare discovery (local + remote per INI) to [inventory.host.*] expectations.",
action="store_true",
help="Compare live discovery to the lab INI (-c).",
)
parser.add_argument(
"--ssh-controlmaster",
action="store_true",
help="With --verify-inventory: use ControlMaster on ssh_node for remote hosts.",
)
parser.add_argument(
"--setup-remote",
dest="setup_remote",
action="store_true",
help="SSH to the Pi (or other host) and pip install -e '.[power]' in --remote-repo.",
)
parser.add_argument(
"--remote-repo",
default=None,
help="With --setup-remote: FiWiControl path on the remote (default: $FIWI_REMOTE_REPO or /root/Code/FiWiControl).",
)
parser.add_argument(
"--pip-flags",
default=None,
metavar="ARGS",
help=(
"With --setup-remote: extra args for remote pip (default: $FIWI_REMOTE_PIP_FLAGS). "
"If the value starts with --, use equals form, e.g. --pip-flags=--break-system-packages."
),
)
parser.add_argument(
"--break-system-packages",
dest="pip_break_system",
action="store_true",
help="With --setup-remote: same as --pip-flags=--break-system-packages (PEP 668 / Raspberry Pi OS).",
)
parser.add_argument(
"--remote-python",
default=None,
metavar="EXE",
help="With --setup-remote: remote interpreter for pip and checks (default: $FIWI_REMOTE_PYTHON or python3).",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="With --setup-remote: print ssh remote script only.",
)
parser.add_argument(
"setup_host",
nargs="?",
default=None,
help="With --setup-remote: host or user@host (default: $FIWI_REMOTE_IP or 192.168.1.39).",
help="With --list-power-devices / --verify-inventory: use ControlMaster on ssh_node.",
)
args = parser.parse_args(argv)
if args.list_power_devices_ini:
sub = [args.list_power_devices_ini]
ini = args.config if args.config is not None else default_lab_ini_path()
if args.list_power_devices:
sub: list[str] = ["-c", str(ini)]
if args.ssh_controlmaster:
sub.append("--ssh-controlmaster")
return main_dump_discovery(sub)
if args.setup_remote:
sargv: list[str] = []
if args.setup_host:
sargv.append(args.setup_host)
if args.remote_repo:
sargv.extend(["--remote-repo", args.remote_repo])
pip_merged: list[str] = []
if args.pip_flags:
pip_merged.append(args.pip_flags.strip())
if getattr(args, "pip_break_system", False):
pip_merged.append("--break-system-packages")
if pip_merged:
sargv.append("--pip-flags=" + " ".join(pip_merged))
if args.remote_python:
sargv.extend(["--remote-python", args.remote_python])
if args.dry_run:
sargv.append("--dry-run")
return main_setup(sargv)
if args.verify_ini:
vargv = [args.verify_ini]
if args.verify_inventory:
sub = ["-c", str(ini)]
if args.ssh_controlmaster:
vargv.append("--ssh-controlmaster")
return main_verify(vargv)
sub.append("--ssh-controlmaster")
return main_verify(sub)
if args.discovery_json:
sys.stdout.write(discovery_json())
sys.stdout.write("\n")

View File

@ -10,12 +10,12 @@ from pathlib import Path
import pytest
from fiwicontrol.power.inventory_config import load_inventory_ini
from fiwicontrol.power.inventory_verify import compare_host_inventory, dump_inventory_discovery_ini_sync
from fiwicontrol.lab.inventory_config import HostInventorySpec, load_inventory_ini
from fiwicontrol.lab.inventory_verify import compare_host_inventory, dump_inventory_discovery_ini_sync
def test_load_clubhouse_ini(tmp_path: Path) -> None:
src = Path(__file__).resolve().parents[1] / "configs" / "clubhouse.ini"
def test_load_default_lab_ini(tmp_path: Path) -> None:
src = Path(__file__).resolve().parents[1] / "configs" / "default.ini"
doc = load_inventory_ini(src)
assert doc.site_name == "ClubHouse"
assert "raspberry_pi5" in doc.nodes
@ -33,8 +33,6 @@ def test_load_clubhouse_ini(tmp_path: Path) -> None:
def test_compare_host_inventory_match() -> None:
from fiwicontrol.power.inventory_config import HostInventorySpec
spec = HostInventorySpec(
name="t",
mode="local",
@ -75,8 +73,6 @@ def test_compare_host_inventory_match() -> None:
def test_compare_host_inventory_mismatch() -> None:
from fiwicontrol.power.inventory_config import HostInventorySpec
spec = HostInventorySpec(
name="t",
mode="local",

View File

@ -2,7 +2,7 @@
#
# Licensed under the Apache License, Version 2.0; see LICENSE.
"""Live inventory check vs ``configs/clubhouse.ini`` (opt-in via env)."""
"""Live inventory check vs ``configs/default.ini`` (opt-in via env)."""
from __future__ import annotations
@ -11,17 +11,17 @@ from pathlib import Path
import pytest
INI = Path(__file__).resolve().parents[1] / "configs" / "clubhouse.ini"
INI = Path(__file__).resolve().parents[1] / "configs" / "default.ini"
@pytest.mark.skipif(
os.environ.get("FIWI_VERIFY_POWER_INI", "").lower() not in ("1", "true", "yes"),
reason="Set FIWI_VERIFY_POWER_INI=1 to run local+remote discovery vs configs/clubhouse.ini (needs hardware + SSH).",
reason="Set FIWI_VERIFY_POWER_INI=1 to run local+remote discovery vs configs/default.ini (needs hardware + SSH).",
)
def test_inventory_matches_ini_live() -> None:
pytest.importorskip("brainstem")
pytest.importorskip("serial")
from fiwicontrol.power.inventory_verify import verify_inventory_ini_sync
from fiwicontrol.lab.inventory_verify import verify_inventory_ini_sync
errs = verify_inventory_ini_sync(INI, ssh_controlmaster=False)
assert errs == [], ";\n".join(errs)

View File

@ -1,6 +1,7 @@
def test_import_subpackages() -> None:
import fiwicontrol
import fiwicontrol.commands
import fiwicontrol.lab
import fiwicontrol.power
assert fiwicontrol.__version__

View File

@ -10,7 +10,7 @@ from unittest import mock
import pytest
from fiwicontrol.power.discovery import (
from fiwicontrol.lab.discovery import (
MONSOON_USB_VID,
MONSOON_USB_PIDS,
_discover_monsoon_sysfs_usb,
@ -94,8 +94,8 @@ def test_discover_monsoon_serial_ports_filters_vid_pid():
def test_discovery_json_is_parseable():
pytest.importorskip("brainstem")
pytest.importorskip("serial")
with mock.patch("fiwicontrol.power.discovery.discover_acroname_modules", return_value=[]):
with mock.patch("fiwicontrol.power.discovery.discover_monsoon_serial_ports", return_value=[]):
with mock.patch("fiwicontrol.lab.discovery.discover_acroname_modules", return_value=[]):
with mock.patch("fiwicontrol.lab.discovery.discover_monsoon_serial_ports", return_value=[]):
data = json.loads(discovery_json())
assert data["acroname"] == []
assert data["monsoon"] == []