Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bda869542 | ||
|
|
ce1adc7b03 | ||
|
|
fc68521731 | ||
|
|
c87730f1eb | ||
|
|
76346cd5aa | ||
|
|
705279097f | ||
|
|
36377fdd5a | ||
|
|
a427c66dc1 | ||
|
|
f0c64f5fb3 | ||
|
|
a0e19282ec | ||
|
|
e999b12246 | ||
|
|
d3216ea57a | ||
|
|
0b6a5fa108 | ||
|
|
ff23601183 | ||
|
|
aaabb5d811 | ||
|
|
ccf1ac5b6d | ||
|
|
46da00e214 | ||
|
|
1dd1ace327 | ||
|
|
a87ae59a93 | ||
|
|
8298db0891 | ||
|
|
93e90f4894 | ||
|
|
4c63bdb6ee | ||
|
|
441d608ca0 | ||
|
|
2e549371ef | ||
|
|
4d2a42d541 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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>
|
||||
|
||||
|
||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -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:
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -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
|
||||
|
||||
4
Makefile
4
Makefile
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.1.17
|
||||
1.1.19
|
||||
|
||||
1946
po/zh_TW.po
1946
po/zh_TW.po
File diff suppressed because it is too large
Load Diff
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user