Add --json output to solaar show for machine-readable device info

This commit is contained in:
Itay Avraham 2026-08-15 18:53:06 +03:00
parent 8a941c5553
commit 4411e4e4e1
2 changed files with 104 additions and 0 deletions

View File

@ -47,6 +47,11 @@ def _create_parser():
help="device to show information about; may be a device number (1..6), a serial number, "
'a substring of a device\'s name, or "all" (the default)',
)
sp.add_argument(
"--json",
action="store_true",
help="output the device information as JSON on a single document, for consumption by other programs",
)
sp.set_defaults(action="show")
sp = subparsers.add_parser("probe", description="Probe a receiver (debugging use only).")

View File

@ -14,6 +14,8 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import json
from logitech_receiver import common
from logitech_receiver import exceptions
from logitech_receiver import hidpp10
@ -23,6 +25,8 @@ from logitech_receiver import hidpp20_constants
from logitech_receiver import receiver
from logitech_receiver import settings_templates
from logitech_receiver.common import LOGITECH_VENDOR_ID
from logitech_receiver.common import BatteryLevelApproximation
from logitech_receiver.common import BatteryStatus
from logitech_receiver.common import NamedInt
from logitech_receiver.common import strhex
from logitech_receiver.device import CenturionReceiver
@ -146,6 +150,95 @@ def _battery_line(dev):
print(" Battery status unavailable.")
def _battery_json(battery):
"""Serialize a Battery as a JSON-friendly dict, or None if unavailable."""
if battery is None:
return None
level = battery.level
if isinstance(level, BatteryLevelApproximation):
# A qualitative level (e.g. "good"), not an actual percentage
level_kind = "reported"
elif isinstance(level, int):
level_kind = "level"
else:
level_kind = None
status = battery.status
return {
"level": int(level) if level is not None else None,
"level_kind": level_kind,
"next_level": int(battery.next_level) if battery.next_level is not None else None,
"status": status.name.lower().replace("_", " ") if isinstance(status, BatteryStatus) else None,
"voltage": battery.voltage,
}
def _receiver_json(receiver):
"""Serialize the receiver a device is paired to, or None for standalone devices."""
if receiver is None:
return None
return {
"name": receiver.name,
"serial": receiver.serial,
"path": receiver.path,
}
def _device_json(dev):
"""Serialize a device as a JSON-friendly dict, or None if the device is gone."""
try:
online = dev.ping()
except exceptions.NoSuchDevice:
return None
battery = None
if online:
try:
battery = dev.battery()
except Exception:
battery = None
protocol = float(dev.protocol) if dev.protocol else None
receiver = getattr(dev, "receiver", None)
return {
"name": dev.name,
"number": dev.number,
"receiver": _receiver_json(receiver),
"serial": dev.serial,
"unitId": dev.unitId,
"modelId": dev.modelId,
"codename": dev.codename,
"kind": str(dev.kind) if dev.kind is not None else None,
"protocol": protocol,
"online": bool(online),
"battery": _battery_json(battery),
}
def _json_output(devices, device_name, find_receiver, find_device):
result = {"solaar_version": __version__, "devices": []}
def add(device):
if device is not None:
result["devices"].append(device)
if device_name == "all":
for d in devices:
if isinstance(d, (receiver.Receiver, CenturionReceiver)):
for dev in d:
add(_device_json(dev))
else:
add(_device_json(d))
else:
dev = find_receiver(devices, device_name)
if dev and not dev.isDevice:
for child in dev:
add(_device_json(child))
else:
dev = next(find_device(devices, device_name), None)
if not dev:
raise Exception(f"no device found matching '{device_name}'")
add(_device_json(dev))
print(json.dumps(result, indent=2))
def _print_device(dev, num=None):
assert dev is not None
is_centurion = getattr(dev, "centurion", False)
@ -464,6 +557,12 @@ def run(devices, args, find_receiver, find_device):
assert devices
assert args.device
if args.json:
_json_output(devices, args.device.lower(), find_receiver, find_device)
for d in Device.instances:
d.close()
return
print(f"{NAME.lower()} version {__version__}")
print("")