From 4411e4e4e1b2065cf124cd776ae6d69904f2157b Mon Sep 17 00:00:00 2001 From: Itay Avraham Date: Sat, 15 Aug 2026 18:53:06 +0300 Subject: [PATCH] Add --json output to solaar show for machine-readable device info --- lib/solaar/cli/__init__.py | 5 ++ lib/solaar/cli/show.py | 99 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/lib/solaar/cli/__init__.py b/lib/solaar/cli/__init__.py index 3e4928c9..91d50c82 100644 --- a/lib/solaar/cli/__init__.py +++ b/lib/solaar/cli/__init__.py @@ -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).") diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index 0f41b3f1..9e48582d 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -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("")