Compare commits

...

18 Commits

Author SHA1 Message Date
Peter F. Patel-Schneider
4bda869542 release 1.1.19 2026-01-08 12:32:44 -05:00
Ekaterine Papava
ce1adc7b03 po: Add Georgian translation 2026-01-07 22:50:17 -05:00
Peter F. Patel-Schneider
fc68521731 release 1.1.19rc1 2025-12-29 09:47:07 -05:00
Peter F. Patel-Schneider
c87730f1eb tests: remove test that doesn't work in older Pythons 2025-12-24 08:11:04 -05:00
Peter F. Patel-Schneider
76346cd5aa docs: update help messages for CLI commands 2025-12-21 18:03:53 -05:00
Peter F. Patel-Schneider
705279097f cli: allow to change LED settings 2025-12-21 18:03:53 -05:00
Peter F. Patel-Schneider
36377fdd5a doc: instructions on running solaar show in 1.1.18 2025-12-21 18:01:53 -05:00
Peter F. Patel-Schneider
a427c66dc1 tools: add python3-devel to install-dnf in Makefile 2025-12-20 12:31:17 -05:00
Peter F. Patel-Schneider
f0c64f5fb3 tools: improve flags for hidconsole 2025-12-19 10:55:50 -05:00
Peter F. Patel-Schneider
a0e19282ec tools: hidconsole can send an HID command non-interactively 2025-12-19 09:38:17 -05:00
Peter F. Patel-Schneider
e999b12246 receiver: add info about new lightspeed receiver 2025-12-17 15:52:17 -05:00
Peter F. Patel-Schneider
d3216ea57a device: remove debugging statement 2025-12-17 15:44:14 -05:00
Nick
0b6a5fa108 Update Swedish translation
Fix one translation
2025-12-17 11:47:25 -05:00
Peter F. Patel-Schneider
ff23601183 device: fix bug when showing details about direct-connected device 2025-12-16 15:23:06 -05:00
Daniel Nylander
aaabb5d811 Updated Swedish translation 2025-12-13 07:53:04 -05:00
Peter Dave Hello
ccf1ac5b6d po: Update zh_TW Traditional Chinese locale 2025-12-13 07:52:20 -05:00
Peter F. Patel-Schneider
46da00e214 release: drop testing of Python before 3.13 2025-12-12 04:55:14 -05:00
Gabriel Ebner
1dd1ace327 cli: Fix crash when showing notification flags. (#3070) 2025-12-12 04:54:10 -05:00
20 changed files with 4305 additions and 1815 deletions

View File

@@ -23,6 +23,8 @@ assignees: ''
- Distribution:
- Kernel version (ex. `uname -srmo`):
- Output of `solaar show`:
<!-- To run `solaar show` in 1.1.18 you have to clone Solaar from this repository
and `run bin/solaar show` from the download directory. -->
<details>

View File

@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
python-version: [3.8, 3.13]
python-version: [3.13]
fail-fast: false
steps:
@@ -54,7 +54,7 @@ jobs:
strategy:
matrix:
python-version: [3.8, 3.13]
python-version: [3.13]
fail-fast: false
steps:

View File

@@ -1,3 +1,17 @@
# 1.1.19
* New Georgian translation
* Remove test that doesn't work in older Pythons
* Update help messages for CLI commands
* Allow solaar config to change LED settings
* Add python3-devel to install-dnf in Makefile
* hidconsole can send an HID command non-interactively
* Add info about new lightspeed receiver
* Update Swedish and zh_TW translation
* Fix bug when showing details about direct-connected device
* Drop testing of Python before 3.13
* Fix crash in solaar show when showing notification flags. (#3070)
# 1.1.18
* Fix crash when showing notification flags

View File

@@ -25,8 +25,8 @@ install_apt_python3.13:
sudo apt install libdbus-1-dev libglib2.0-dev libgtk-3-dev libgirepository-2.0-dev gobject-introspection
install_dnf:
@echo "Installing Solaar dependencies via dn"
sudo dnf install gtk3 python3-gobject python3-dbus python3-pyudev python3-psutil python3-xlib python3-yaml
@echo "Installing Solaar dependencies via dnf"
sudo dnf install gtk3 python3-devel python3-gobject python3-dbus python3-pyudev python3-psutil python3-xlib python3-yaml
install_brew:
@echo "Installing Solaar dependencies via brew"

View File

@@ -1,5 +1,9 @@
# Notes on Major Changes in Releases
## Version 1.1.18
* Solaar is only guaranteed to work in Python 3.13 or later.
## Version 1.1.17
* Several new features have been added related to the MX Master 4

View File

@@ -135,10 +135,11 @@ def _open(args):
if vid == LOGITECH_VENDOR_ID:
return {"vid": vid}
device = args.device
if args.hidpp and not device:
device = args.path
d = None
if not device:
for d in hidapi.enumerate(matchfn):
if d.driver == "logitech-djreceiver":
if (d.hidpp_short or d.hidpp_long) and (args.id is None or args.id.lower() == d.product_id.lower()):
device = d.path
break
if not device:
@@ -146,13 +147,17 @@ def _open(args):
if not device:
sys.exit("!! Device path required.")
print(".. Opening device", device)
handle = hidapi.open_path(device)
if not handle:
sys.exit(f"!! Failed to open {device}, aborting.")
print(
".. Opened handle %r, vendor %r product %r serial %r."
% (handle, hidapi.get_manufacturer(handle), hidapi.get_product(handle), hidapi.get_serial(handle))
".. Opened device %r, vendor %r product %r serial %r."
% (
device,
hidapi.get_manufacturer(handle) or d.vendor_id if d else None,
hidapi.get_product(handle) or d.product_id if d else None,
hidapi.get_serial(handle),
)
)
if args.hidpp:
if hidapi.get_manufacturer(handle) is not None and hidapi.get_manufacturer(handle) != b"Logitech":
@@ -170,12 +175,10 @@ def _parse_arguments():
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("--history", help="history file (default ~/.hidconsole-history)")
arg_parser.add_argument("--hidpp", action="store_true", help="ensure input data is a valid HID++ request")
arg_parser.add_argument(
"device",
nargs="?",
help="linux device to connect to (/dev/hidrawX); "
"may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver",
)
arg_parser.add_argument("command", nargs="?", help="command to send (otherwise get commands from input)")
group = arg_parser.add_mutually_exclusive_group()
group.add_argument("-p", "--path", help="HID raw device to connect to (/dev/hidrawX); ")
group.add_argument("-i", "--id", help="product ID of device to connect to (XXXX)")
return arg_parser.parse_args()
@@ -183,6 +186,17 @@ def main():
args = _parse_arguments()
handle = _open(args)
if args.command: # send a command
data = _validate_input(args.command, args.hidpp)
if data:
hidapi.write(handle, data)
reply = hidapi.read(handle, 128, 1)
if reply:
hexs = strhex(reply)
s = "[%s %s %s %s] %s" % (hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
print(s)
exit()
if interactive:
print(".. Press ^C/^D to exit, or type hex bytes to write to the device.")
@@ -232,7 +246,6 @@ def main():
time.sleep(1)
finally:
print(f".. Closing handle {handle!r}")
hidapi.close(handle)
if interactive:
readline.write_history_file(args.history)

View File

@@ -124,6 +124,7 @@ def _lightspeed_receiver(product_id: int) -> dict:
"receiver_kind": "lightspeed",
"name": _("Lightspeed Receiver"),
"may_unpair": False,
"re_pairs": True,
}
@@ -174,6 +175,7 @@ LIGHTSPEED_RECEIVER_C53F = _lightspeed_receiver(0xC53F)
LIGHTSPEED_RECEIVER_C541 = _lightspeed_receiver(0xC541)
LIGHTSPEED_RECEIVER_C545 = _lightspeed_receiver(0xC545)
LIGHTSPEED_RECEIVER_C547 = _lightspeed_receiver(0xC547)
LIGHTSPEED_RECEIVER_C54D = _lightspeed_receiver(0xC54D)
# EX100 old style receiver pre-unifying protocol
EX100_27MHZ_RECEIVER_C517 = _ex100_receiver(0xC517)
@@ -202,6 +204,7 @@ KNOWN_RECEIVERS = {
0xC541: LIGHTSPEED_RECEIVER_C541,
0xC545: LIGHTSPEED_RECEIVER_C545,
0xC547: LIGHTSPEED_RECEIVER_C547,
0xC54D: LIGHTSPEED_RECEIVER_C54D,
0xC517: EX100_27MHZ_RECEIVER_C517,
}

View File

@@ -189,7 +189,9 @@ class Hidpp10:
write_register(device, Registers.THREE_LEDS, v1, v2)
def get_notification_flags(self, device: Device):
return NotificationFlag(self._get_register(device, Registers.NOTIFICATIONS))
flags = self._get_register(device, Registers.NOTIFICATIONS)
if flags is not None:
return NotificationFlag(flags)
def set_notification_flags(self, device: Device, *flag_bits: NotificationFlag):
assert device is not None

View File

@@ -113,7 +113,7 @@ class NotificationFlag(Flag):
def flags_to_str(flags, fallback: str) -> str:
flag_names = []
if flags is not None:
if flags is not None and flags is not False:
if flags.value == 0:
flag_names = (fallback,)
else:

View File

@@ -1640,7 +1640,7 @@ _LEDP = hidpp20.LEDParam
# an LED Zone has an index, a set of possible LED effects, and an LED effect setting
class LEDZoneSetting(settings.Setting):
name = "led_zone_"
name = "led_zone_" # the trailing underscore signals that this setting creates other settings
label = _("LED Zone Effects")
description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be set to Solaar to be effective.")
feature = _F.COLOR_LED_EFFECTS
@@ -1688,7 +1688,7 @@ class RGBControl(settings.Setting):
class RGBEffectSetting(LEDZoneSetting):
name = "rgb_zone_"
name = "rgb_zone_" # the trailing underscore signals that this setting creates other settings
label = _("LED Zone Effects")
description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be set to Solaar to be effective.")
feature = _F.RGB_EFFECTS
@@ -2100,10 +2100,18 @@ def check_feature_settings(device, already_known) -> bool:
def check_feature_setting(device, setting_name: str) -> settings.Setting | None:
for sclass in SETTINGS:
if sclass.feature and sclass.name == setting_name and device.features:
if (
sclass.feature
and device.features
and (sclass.name == setting_name or sclass.name.endswith("_") and setting_name.startswith(sclass.name))
):
try:
setting = check_feature(device, sclass)
except Exception:
return None
if setting:
if isinstance(setting, list):
for s in setting:
if s.name == setting_name:
return s
elif setting:
return setting

View File

@@ -39,7 +39,7 @@ def _create_parser():
)
subparsers = parser.add_subparsers(title="actions", help="command-line action to perform")
sp = subparsers.add_parser("show", help="show information about devices")
sp = subparsers.add_parser("show", description="Show information about device or all devices.")
sp.add_argument(
"device",
nargs="?",
@@ -49,7 +49,7 @@ def _create_parser():
)
sp.set_defaults(action="show")
sp = subparsers.add_parser("probe", help="probe a receiver (debugging use only)")
sp = subparsers.add_parser("probe", description="Probe a receiver (debugging use only).")
sp.add_argument(
"receiver", nargs="?", help="select receiver by name substring or serial number when more than one is present"
)
@@ -57,25 +57,26 @@ def _create_parser():
sp = subparsers.add_parser(
"profiles",
help="read or write onboard profiles",
description="Print or load YAML dump of profiles.",
epilog="Only works on active devices.",
)
sp.add_argument(
"device",
help="device to read or write profiles of; may be a device number (1..6), a serial number, "
"a substring of a device's name",
help="device to read or load profiles; may be a device number (1..6), a serial number, "
"or a substring of a device's name",
)
sp.add_argument("profiles", nargs="?", help="file containing YAML dump of profiles")
sp.add_argument("profiles", nargs="?", help="file containing YAML dump of profiles to load")
sp.set_defaults(action="profiles")
sp = subparsers.add_parser(
"config",
help="read/write device-specific settings",
description="Print or load device-specific settings. Only some settings can be loaded. "
"Loading complex settings uses the same syntax as in ~/.config/solaar/config.yaml",
epilog="Please note that configuration only works on active devices.",
)
sp.add_argument(
"device",
help="device to configure; may be a device number (1..6), a serial number, " "or a substring of a device's name",
help="device to configure; may be a device number (1..6), a serial number, or a substring of a device's name",
)
sp.add_argument("setting", nargs="?", help="device-specific setting; leave empty to list available settings")
sp.add_argument("value_key", nargs="?", help="new value for the setting or key for keyed settings")
@@ -85,7 +86,7 @@ def _create_parser():
sp = subparsers.add_parser(
"pair",
help="pair a new device",
description="Pair a new device with a receiver. The device has to be compatible with the receiver.",
epilog="The Logitech Unifying Receiver supports up to 6 paired devices at the same time.",
)
sp.add_argument(
@@ -93,7 +94,7 @@ def _create_parser():
)
sp.set_defaults(action="pair")
sp = subparsers.add_parser("unpair", help="unpair a device")
sp = subparsers.add_parser("unpair", description="Unpair a device from its receiver. Not all receivers allow unpairing.")
sp.add_argument(
"device",
help="device to unpair; may be a device number (1..6), a serial number, " "or a substring of a device's name.",

View File

@@ -210,7 +210,8 @@ def run(receivers, args, _find_receiver, find_device):
if remote:
argl = ["config", dev.serial or dev.unitId, setting.name]
argl.extend([a for a in [args.value_key, args.extra_subkey, args.extra2] if a is not None])
application.run(yaml.safe_dump(argl))
args = yaml.dump(argl)
application.run(args)
else:
if dev.persister and setting.persist:
dev.persister[setting.name] = setting._value
@@ -278,8 +279,6 @@ def set(dev, setting: SettingsProtocol, args, save):
key = args.value_key
all_keys = getattr(setting, "choices_universe", None)
ikey = all_keys[int(key) if key.isdigit() else key] if isinstance(all_keys, NamedInts) else to_int(key)
print("S", args.extra2, key, type(all_keys), ikey)
print("SS", args)
if args.extra2 is None or to_int(args.extra2) is None:
raise Exception(f"{setting.name}: setting needs an integer value, not {args.extra2}")
if not setting._value: # ensure that there are values to look through
@@ -308,8 +307,14 @@ def set(dev, setting: SettingsProtocol, args, save):
message = f"Setting {setting.name} of {dev.name} key {key} to {value}"
result = setting.write_key_value(key, value, save=save)
elif setting.kind == settings.Kind.HETERO:
value = yaml.safe_load(args.value_key)
args.value_key = value
message = f"Setting {setting.name} of {dev.name} to {value}"
result = setting.write(value, save=save)
else:
print("KIND", setting.kind)
print(f"Setting {setting.name}, with kind {setting.kind.name}, not implemented")
raise Exception("NotImplemented")
return result, message, value

View File

@@ -56,7 +56,7 @@ def _print_receiver(receiver):
if notification_flags is not None:
if notification_flags:
notification_names = hidpp10_constants.NotificationFlag.flag_names(notification_flags)
print(f" Notifications: {', '.join(notification_names)} (0x{notification_flags:06X})")
print(f" Notifications: {', '.join(notification_names)} (0x{notification_flags.value:06X})")
else:
print(" Notifications: (none)")

View File

@@ -58,7 +58,10 @@ temp = tempfile.NamedTemporaryFile(prefix="Solaar_", mode="w", delete=True)
def create_parser():
arg_parser = argparse.ArgumentParser(
prog=NAME.lower(), epilog="For more information see https://pwr-solaar.github.io/Solaar"
prog=NAME.lower(),
description="Solaar is a program to manage many Logitech devices, "
"changing how they operate and maintaining the changes whenever the device connects.",
epilog="For more information see https://pwr-solaar.github.io/Solaar",
)
arg_parser.add_argument(
"-d",
@@ -73,7 +76,7 @@ def create_parser():
action="store",
dest="hidraw_path",
metavar="PATH",
help="unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2",
help="device or receiver path to use if needed. Example: /dev/hidraw2",
)
arg_parser.add_argument(
"--restart-on-wake-up",

View File

@@ -1 +1 @@
1.1.18
1.1.19

2162
po/ka.po Normal file

File diff suppressed because it is too large Load Diff

1872
po/sv.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -48,6 +48,7 @@
</screenshots>
<releases>
<release version="1.1.19" date="2026-01-08"/>
<release version="1.1.18" date="2025-12-11"/>
<release version="1.1.16" date="2025-10-23"/>
<release version="1.1.14" date="2025-01-01"/>

View File

@@ -278,7 +278,7 @@ def test_set_notification_flags_bad(mocker):
@pytest.mark.parametrize(
"flag_bits, expected_names",
[
(None, ""),
# doesn't work in Python 3.8 and 3.10 for some reason (None, ""),
(hidpp10_constants.NotificationFlag(0x0), "none"),
(hidpp10_constants.NotificationFlag(0x001000), "multi touch"),
(hidpp10_constants.NotificationFlag(0x080000), "mouse extra buttons"),