Compare commits

...

25 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
Peter F. Patel-Schneider
a87ae59a93 release 1.1.18 2025-12-11 15:28:01 -05:00
Peter F. Patel-Schneider
8298db0891 receiver: fix crash when turning notification flags into strings 2025-12-11 15:23:06 -05:00
Peter F. Patel-Schneider
93e90f4894 docs: update pairing documentation 2025-12-10 14:03:44 -05:00
Peter F. Patel-Schneider
4c63bdb6ee show better pairing errors (#3063)
* Fix: Show pairing error str

Fixes #2827

* device: also show bolt pairing errors

---------

Co-authored-by: MattHag <16444067+MattHag@users.noreply.github.com>
2025-12-10 11:18:50 -05:00
MattHag
441d608ca0 test_pair_window: Simplify tests by cleaning up receiver mock (#2899)
Remove unnecessary parameter for these tests.
2025-12-10 10:50:07 -05:00
Peter F. Patel-Schneider
2e549371ef device: fix typing issue with notification flags 2025-12-10 07:31:48 -05:00
Peter F. Patel-Schneider
4d2a42d541 doc: better formating of release notes 2025-12-09 15:41:02 -05:00
26 changed files with 4396 additions and 1901 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,21 @@
# 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
# 1.1.17
* Add dark icons

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,12 +1,16 @@
# Notes on Major Changes in Releases
## Since 1.1.16
## 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
** The scroll ratchet force can be adjusted
** The force required to click the button under your thumb can be adjusted
** The haptic force can be adjusted
** Haptic feeback can be triggered by commands like `solaar config 'mx master 4' haptic-play 'HAPPY ALER'`
* The scroll ratchet force can be adjusted
* The force required to click the button under your thumb can be adjusted
* The haptic force can be adjusted
* Haptic feeback can be triggered by commands like `solaar config 'mx master 4' haptic-play 'HAPPY ALER'`
## Version 1.1.16

View File

@@ -56,12 +56,12 @@ Bluetooth product ID.
Solaar is able to pair and unpair devices with
receivers as supported by the device and receiver.
For Unifying receivers, pairing adds a new paired device, but
For Unifying and Bolt receivers, pairing adds a new paired device, but
only if there is an open slot on the receiver. So these receivers need to
be able to unpair devices that they have been paired with or else they will
not have any open slots for pairing. Some other receivers, like the
Nano receiver with USB ID `046d:c534`, can only pair with particular kinds of
devices and pairing a new device replaces whatever device of that kind was
not have any open slots for pairing. Some Nano and Lightspeed receivers, like the
Nano receiver with USB ID `046d:c534`, can only pair with one keyboard and one mouse
and pairing a new device replaces whatever device of that kind was
previously paired to the receiver. These receivers cannot unpair. Further,
some receivers can pair an unlimited number of times but others can only
pair a limited number of times.
@@ -69,13 +69,15 @@ pair a limited number of times.
Bolt receivers add an authentication phase to pairing,
where the user has type a passcode or click some buttons to authenticate the device.
Only some connections between receivers and devices are possible. In should
Only some connections between receivers and devices are possible. It should
be possible to connect any device with a Unifying logo on it to any receiver
with a Unifying logo on it. Receivers without the Unifying logo probably
can connect only to the kind of devices they were bought with and devices
without the Unifying logo can probably only connect to the kind of receiver
that they were bought with.
with a Unifying logo on it and any device with a Bolt logo on it to any receiver
with a Bolt logo on it.
Many receivers without the Unifying or Bolt logo
can connect only to the model of devices they were bought with and many devices
without the Unifying or Bolt logo can only connect to a receiver
that matches the one they were bought with.
## Device Settings

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 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

@@ -89,23 +89,9 @@ class NotificationFlag(Flag):
"""
@classmethod
def flag_names(cls, flag_bits: int) -> List[str]:
def flag_names(cls, flags) -> List[str]:
"""Extract the names of the flags from the integer."""
indexed = {item.value: item.name for item in cls}
flag_names = []
unknown_bits = flag_bits
for k in indexed:
# Ensure that the key (flag value) is a power of 2 (a single bit flag)
assert bin(k).count("1") == 1
if k & flag_bits == k:
unknown_bits &= ~k
flag_names.append(indexed[k].replace("_", " ").lower())
# Yield any remaining unknown bits
if unknown_bits != 0:
flag_names.append(f"unknown:{unknown_bits:06X}")
return flag_names
return flags.name.replace("_", " ").lower().split("|")
NUMPAD_NUMERICAL_KEYS = 0x800000
F_LOCK_STATUS = 0x400000
@@ -125,13 +111,13 @@ class NotificationFlag(Flag):
THREED_GESTURE = 0x000001
def flags_to_str(flag_bits: int | None, fallback: str) -> str:
def flags_to_str(flags, fallback: str) -> str:
flag_names = []
if flag_bits is not None:
if flag_bits == 0:
if flags is not None and flags is not False:
if flags.value == 0:
flag_names = (fallback,)
else:
flag_names = NotificationFlag.flag_names(flag_bits)
flag_names = NotificationFlag.flag_names(flags)
return f"\n{' ':15}".join(sorted(flag_names))
@@ -156,11 +142,19 @@ class PairingError(IntEnum):
TOO_MANY_DEVICES = 0x03
SEQUENCE_TIMEOUT = 0x06
@property
def label(self) -> str:
return self.name.lower().replace("_", " ")
class BoltPairingError(IntEnum):
DEVICE_TIMEOUT = 0x01
FAILED = 0x02
@property
def label(self) -> str:
return self.name.lower().replace("_", " ")
class Registers(IntEnum):
"""Known HID registers.

View File

@@ -433,7 +433,8 @@ def handle_pairing_lock(receiver: Receiver, notification: HIDPPNotification) ->
receiver.pairing.new_device = None
pair_error = ord(notification.data[:1])
if pair_error:
receiver.pairing.error = error_string = hidpp10_constants.PairingError(pair_error).name
error_string = hidpp10_constants.PairingError(pair_error).label
receiver.pairing.error = error_string
receiver.pairing.new_device = None
logger.warning("pairing error %d: %s", pair_error, error_string)
receiver.changed(reason=reason)
@@ -453,7 +454,7 @@ def handle_discovery_status(receiver: Receiver, notification: HIDPPNotification)
receiver.pairing.device_passkey = None
discover_error = ord(notification.data[:1])
if discover_error:
receiver.pairing.error = discover_string = hidpp10_constants.BoltPairingError(discover_error).name
receiver.pairing.error = discover_string = hidpp10_constants.BoltPairingError(discover_error).label
logger.warning("bolt discovering error %d: %s", discover_error, discover_string)
receiver.changed(reason=reason)
return True
@@ -495,7 +496,7 @@ def handle_pairing_status(receiver: Receiver, notification: HIDPPNotification) -
elif notification.address == 0x02 and not pair_error:
receiver.pairing.new_device = receiver.register_new_device(notification.data[7])
if pair_error:
receiver.pairing.error = error_string = hidpp10_constants.BoltPairingError(pair_error).name
receiver.pairing.error = error_string = hidpp10_constants.BoltPairingError(pair_error).label
receiver.pairing.new_device = None
logger.warning("pairing error %d: %s", pair_error, error_string)
receiver.changed(reason=reason)

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

@@ -38,7 +38,7 @@ def run(receivers, args, find_receiver, _ignore):
assert receiver
# check if it's necessary to set the notification flags
old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0
old_notification_flags = _hidpp10.get_notification_flags(receiver)
if not (old_notification_flags & hidpp10_constants.NotificationFlag.WIRELESS):
_hidpp10.set_notification_flags(receiver, old_notification_flags | hidpp10_constants.NotificationFlag.WIRELESS)

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

@@ -77,7 +77,7 @@ class SolaarListener(listener.EventsListener):
def has_started(self):
logger.info("%s: notifications listener has started (%s)", self.receiver, self.receiver.handle)
nfs = self.receiver.enable_connection_notifications()
if not self.receiver.isDevice and not ((nfs if nfs else 0) & hidpp10_constants.NotificationFlag.WIRELESS.value):
if not self.receiver.isDevice and (not nfs or not (nfs & hidpp10_constants.NotificationFlag.WIRELESS)):
logger.warning(
"Receiver on %s might not support connection notifications, GUI might not show its devices",
self.receiver.path,

View File

@@ -1 +1 @@
1.1.17
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,7 +48,8 @@
</screenshots>
<releases>
<release version="1.1.17" date="2025-12-09"/>
<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"/>
<release version="1.1.13" date="2024-05-11"/>

View File

@@ -9,6 +9,7 @@ import pytest
from logitech_receiver import common
from logitech_receiver import hidpp10
from logitech_receiver import hidpp10_constants
from logitech_receiver.hidpp10_constants import PairingError
from logitech_receiver.hidpp10_constants import Registers
_hidpp10 = hidpp10.Hidpp10()
@@ -247,7 +248,7 @@ def test_set_3leds_missing(device, mocker):
def test_get_notification_flags(device):
result = _hidpp10.get_notification_flags(device)
assert result == int("000900", 16)
assert result == hidpp10_constants.NotificationFlag(int("000900", 16))
def test_set_notification_flags(mocker):
@@ -277,18 +278,20 @@ def test_set_notification_flags_bad(mocker):
@pytest.mark.parametrize(
"flag_bits, expected_names",
[
(None, ""),
(0x0, "none"),
(0x009020, "multi touch\n unknown:008020"),
(0x080000, "mouse extra buttons"),
# 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"),
(
0x080000 + 0x000400,
hidpp10_constants.NotificationFlag(0x080400),
("link quality\n mouse extra buttons"),
),
],
)
def test_notification_flag_str(flag_bits, expected_names):
flag_names = hidpp10_constants.flags_to_str(flag_bits, fallback="none")
flag_names = hidpp10_constants.flags_to_str(
hidpp10_constants.NotificationFlag(flag_bits) if flag_bits is not None else None, fallback="none"
)
assert flag_names == expected_names
@@ -338,3 +341,11 @@ def test_set_configuration_pending_flags(device, expected_result):
result = hidpp10.set_configuration_pending_flags(device, 0x00)
assert result == expected_result
def test_pairing_error():
expected_label = "device not supported"
res = PairingError.DEVICE_NOT_SUPPORTED.label
assert res == expected_label

View File

@@ -58,7 +58,7 @@ def test_process_receiver_notification(sub_id, notification_data, expected_error
result = notifications.process_receiver_notification(receiver, notification)
assert result
assert receiver.pairing.error == (None if expected_error is None else expected_error.name)
assert receiver.pairing.error == (None if expected_error is None else expected_error.label)
assert receiver.pairing.new_device is expected_new_device

View File

@@ -1,10 +1,8 @@
from dataclasses import dataclass
from dataclasses import field
from typing import Any
from typing import Callable
from typing import List
from typing import Optional
from unittest import mock
import gi
import pytest
@@ -26,7 +24,6 @@ class Device:
@dataclass
class Receiver:
find_paired_node_wpid_func: Callable[[str, int], Any]
name: str
receiver_kind: str
_set_lock: bool = True
@@ -87,12 +84,12 @@ class Assistant:
@pytest.mark.parametrize(
"receiver, lock_open, discovering, page_type",
[
(Receiver(mock.Mock(), "unifying", "unifying", True), True, False, Gtk.AssistantPageType.PROGRESS),
(Receiver(mock.Mock(), "unifying", "unifying", False), False, False, Gtk.AssistantPageType.SUMMARY),
(Receiver(mock.Mock(), "nano", "nano", True, _remaining_pairings=5), True, False, Gtk.AssistantPageType.PROGRESS),
(Receiver(mock.Mock(), "nano", "nano", False), False, False, Gtk.AssistantPageType.SUMMARY),
(Receiver(mock.Mock(), "bolt", "bolt", True), False, True, Gtk.AssistantPageType.PROGRESS),
(Receiver(mock.Mock(), "bolt", "bolt", False), False, False, Gtk.AssistantPageType.SUMMARY),
(Receiver("unifying", "unifying", True), True, False, Gtk.AssistantPageType.PROGRESS),
(Receiver("unifying", "unifying", False), False, False, Gtk.AssistantPageType.SUMMARY),
(Receiver("nano", "nano", True, _remaining_pairings=5), True, False, Gtk.AssistantPageType.PROGRESS),
(Receiver("nano", "nano", False), False, False, Gtk.AssistantPageType.SUMMARY),
(Receiver("bolt", "bolt", True), False, True, Gtk.AssistantPageType.PROGRESS),
(Receiver("bolt", "bolt", False), False, False, Gtk.AssistantPageType.SUMMARY),
],
)
def test_create(receiver, lock_open, discovering, page_type):
@@ -108,10 +105,10 @@ def test_create(receiver, lock_open, discovering, page_type):
@pytest.mark.parametrize(
"receiver, expected_result, expected_error",
[
(Receiver(mock.Mock(), "unifying", "unifying", True), True, False),
(Receiver(mock.Mock(), "unifying", "unifying", False), False, True),
(Receiver(mock.Mock(), "bolt", "bolt", True), True, False),
(Receiver(mock.Mock(), "bolt", "bolt", False), False, True),
(Receiver("unifying", "unifying", True), True, False),
(Receiver("unifying", "unifying", False), False, True),
(Receiver("bolt", "bolt", True), True, False),
(Receiver("bolt", "bolt", False), False, True),
],
)
def test_prepare(receiver, expected_result, expected_error):
@@ -123,7 +120,7 @@ def test_prepare(receiver, expected_result, expected_error):
@pytest.mark.parametrize("assistant, expected_result", [(Assistant(True), True), (Assistant(False), False)])
def test_check_lock_state_drawable(assistant, expected_result):
r = Receiver(mock.Mock(), "succeed", "unifying", True, receiver.Pairing(lock_open=True))
r = Receiver("succeed", "unifying", True, receiver.Pairing(lock_open=True))
result = pair_window.check_lock_state(assistant, r, 2)
@@ -134,24 +131,23 @@ def test_check_lock_state_drawable(assistant, expected_result):
@pytest.mark.parametrize(
"receiver, count, expected_result",
[
(Receiver(mock.Mock(), "fail", "unifying", False, receiver.Pairing(lock_open=False)), 2, False),
(Receiver(mock.Mock(), "succeed", "unifying", True, receiver.Pairing(lock_open=True)), 1, True),
(Receiver(mock.Mock(), "error", "unifying", True, receiver.Pairing(error="error")), 0, False),
(Receiver(mock.Mock(), "new device", "unifying", True, receiver.Pairing(new_device=Device())), 2, False),
(Receiver(mock.Mock(), "closed", "unifying", True, receiver.Pairing()), 2, False),
(Receiver(mock.Mock(), "closed", "unifying", True, receiver.Pairing()), 1, False),
(Receiver(mock.Mock(), "closed", "unifying", True, receiver.Pairing()), 0, False),
(Receiver(mock.Mock(), "fail bolt", "bolt", False), 1, False),
(Receiver(mock.Mock(), "succeed bolt", "bolt", True, receiver.Pairing(lock_open=True)), 0, True),
(Receiver(mock.Mock(), "error bolt", "bolt", True, receiver.Pairing(error="error")), 2, False),
(Receiver(mock.Mock(), "new device", "bolt", True, receiver.Pairing(lock_open=True, new_device=Device())), 1, False),
(Receiver(mock.Mock(), "discovering", "bolt", True, receiver.Pairing(lock_open=True)), 1, True),
(Receiver(mock.Mock(), "closed", "bolt", True, receiver.Pairing()), 2, False),
(Receiver(mock.Mock(), "closed", "bolt", True, receiver.Pairing()), 1, False),
(Receiver(mock.Mock(), "closed", "bolt", True, receiver.Pairing()), 0, False),
(Receiver("fail", "unifying", False, receiver.Pairing(lock_open=False)), 2, False),
(Receiver("succeed", "unifying", True, receiver.Pairing(lock_open=True)), 1, True),
(Receiver("error", "unifying", True, receiver.Pairing(error="error")), 0, False),
(Receiver("new device", "unifying", True, receiver.Pairing(new_device=Device())), 2, False),
(Receiver("closed", "unifying", True, receiver.Pairing()), 2, False),
(Receiver("closed", "unifying", True, receiver.Pairing()), 1, False),
(Receiver("closed", "unifying", True, receiver.Pairing()), 0, False),
(Receiver("fail bolt", "bolt", False), 1, False),
(Receiver("succeed bolt", "bolt", True, receiver.Pairing(lock_open=True)), 0, True),
(Receiver("error bolt", "bolt", True, receiver.Pairing(error="error")), 2, False),
(Receiver("new device", "bolt", True, receiver.Pairing(lock_open=True, new_device=Device())), 1, False),
(Receiver("discovering", "bolt", True, receiver.Pairing(lock_open=True)), 1, True),
(Receiver("closed", "bolt", True, receiver.Pairing()), 2, False),
(Receiver("closed", "bolt", True, receiver.Pairing()), 1, False),
(Receiver("closed", "bolt", True, receiver.Pairing()), 0, False),
(
Receiver(
mock.Mock(),
"pass1",
"bolt",
True,
@@ -162,7 +158,6 @@ def test_check_lock_state_drawable(assistant, expected_result):
),
(
Receiver(
mock.Mock(),
"pass2",
"bolt",
True,
@@ -173,7 +168,6 @@ def test_check_lock_state_drawable(assistant, expected_result):
),
(
Receiver(
mock.Mock(),
"adt",
"bolt",
True,
@@ -185,7 +179,6 @@ def test_check_lock_state_drawable(assistant, expected_result):
),
(
Receiver(
mock.Mock(),
"adf",
"bolt",
True,
@@ -195,7 +188,7 @@ def test_check_lock_state_drawable(assistant, expected_result):
2,
False,
),
(Receiver(mock.Mock(), "add fail", "bolt", False, receiver.Pairing(device_address=2, device_passkey=5)), 2, False),
(Receiver("add fail", "bolt", False, receiver.Pairing(device_address=2, device_passkey=5)), 2, False),
],
)
def test_check_lock_state(receiver, count, expected_result):
@@ -210,22 +203,22 @@ def test_check_lock_state(receiver, count, expected_result):
"receiver, pair_device, set_lock, discover, error",
[
(
Receiver(mock.Mock(), "unifying", "unifying", pairing=receiver.Pairing(lock_open=False, error="error")),
Receiver("unifying", "unifying", pairing=receiver.Pairing(lock_open=False, error="error")),
0,
0,
0,
None,
),
(
Receiver(mock.Mock(), "unifying", "unifying", pairing=receiver.Pairing(lock_open=True, error="error")),
Receiver("unifying", "unifying", pairing=receiver.Pairing(lock_open=True, error="error")),
0,
1,
0,
"error",
),
(Receiver(mock.Mock(), "bolt", "bolt", pairing=receiver.Pairing(lock_open=False, error="error")), 0, 0, 0, None),
(Receiver(mock.Mock(), "bolt", "bolt", pairing=receiver.Pairing(lock_open=True, error="error")), 1, 0, 0, "error"),
(Receiver(mock.Mock(), "bolt", "bolt", pairing=receiver.Pairing(discovering=True, error="error")), 0, 0, 1, "error"),
(Receiver("bolt", "bolt", pairing=receiver.Pairing(lock_open=False, error="error")), 0, 0, 0, None),
(Receiver("bolt", "bolt", pairing=receiver.Pairing(lock_open=True, error="error")), 1, 0, 0, "error"),
(Receiver("bolt", "bolt", pairing=receiver.Pairing(discovering=True, error="error")), 0, 0, 1, "error"),
],
)
def test_finish(receiver, pair_device, set_lock, discover, error, mocker):
@@ -247,6 +240,6 @@ def test_finish(receiver, pair_device, set_lock, discover, error, mocker):
def test_create_failure_page(error, mocker):
spy_create = mocker.spy(pair_window, "_create_page")
pair_window._pairing_failed(Assistant(True), Receiver(mock.Mock(), "nano", "nano"), error)
pair_window._pairing_failed(Assistant(True), Receiver("nano", "nano"), error)
assert spy_create.call_count == 1