Repository: pwr-Solaar/Solaar

Issue:
Originating Device Condition Rule
**Information**

Currently, there appears to be no way to write a rule that applies only to a specific originating device. In the case of the Pop Keyboard and mouse, there is a conflict for diversion of the `Open Emoji Panel (0108)` button, which emits the same code when pressed on both the keyboard and the mouse.

It'd be nice to have the button on the mouse perform a different action than on the keyboard.

**Describe the solution you'd like**
I'm assuming there is no way to change the code that is emitted on the mouse. However, if it were possible to write a rule that was something like `when originating device is "pop mouse" and key is `Open Emoji Panel (0108)` (pressed)`, I think that would do the trick.

**Describe alternatives you've considered**
I did play a bit with trying to set the button to be the gesture button, then writing a divert rule for that. I assume this doesn't work because all the gesture button diversion does is try to divert "`Open Emoji Panel (0108)` (pressed)` to `mouse gesture button`.

**Additional context**

Other than for the pop mouse and keyboard, I'm not sure how much usefulness this feature would have otherwise. It's purely a consequence of Logitech deciding to use identical codes on both the mouse and keyboard.

Other than this issue, the mouse and keyboard, as well as diversion of all the special keys, works perfectly for me.



Discussion:
Looking at https://github.com/pwr-Solaar/Solaar/blob/master/lib/logitech_receiver/diversion.py, it seems like the general approach to this would be to create a new rule like https://github.com/pwr-Solaar/Solaar/blob/master/lib/logitech_receiver/diversion.py#L949, but with `evaluate` testing the passed in device (https://github.com/pwr-Solaar/Solaar/blob/master/lib/logitech_receiver/device.py) for whether or not it is the desired device id.

The UI portion I imagine would be very similar to how active is: https://github.com/pwr-Solaar/Solaar/blob/master/lib/solaar/ui/diversion_rules.py#L2222, since we'll also need to enumerate available devices to show to the user.

I'm a bit rusty with Python, but may give this a shot if there is a lack of interest.
Yes.  I'm most of the way through doing the non-GUI part of this.
PR #1996 adds a Device condition.  No GUI support yet so you have to edit `/.config/solaar/rules.yaml`.  Here is an example rule with a Device condition:

```
---
- Device: 198E3EB8
- Execute: [notify-send, devicex]
...
```

To clone and use Solar from its GitHub repository
```
git clone https://github.com/pwr-Solaar/Solaar.git
cd Solaar
```
Run Solaar as bin/solaar from this directory.


To run PR #1996, first clone Solaar if you have not already done so and cd to the clone directory.  The first time you download the pull request, fetch it into a new branch and checkout that branch, as in:
```
git fetch origin pull/1996/head:pull_1996
git checkout pull_1996
```
To download a new version of the pull request, fetch it and then set your pull branch to the new fetch, as in:
```
git checkout pull_1996
git fetch origin pull/1996/head
git reset --hard FETCH_HEAD
```


Resolving patch:
diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py
--- a/lib/logitech_receiver/diversion.py
+++ b/lib/logitech_receiver/diversion.py
@@ -968,6 +968,28 @@ def data(self):
         return {'Active': self.devID}
 
 
+class Device(Condition):
+
+    def __init__(self, devID, warn=True):
+        if not (isinstance(devID, str)):
+            if warn:
+                _log.warn('rule Device argument not a string: %s', devID)
+            self.devID = ''
+        self.devID = devID
+
+    def __str__(self):
+        return 'Device: ' + str(self.devID)
+
+    def evaluate(self, feature, notification, device, status, last_result):
+        if _log.isEnabledFor(_DEBUG):
+            _log.debug('evaluate condition: %s', self)
+        dev = _Device.find(self.devID)
+        return device == dev
+
+    def data(self):
+        return {'Device': self.devID}
+
+
 class Action(RuleComponent):
 
     def __init__(self, *args):
@@ -1265,6 +1287,7 @@ def data(self):
     'TestBytes': TestBytes,
     'MouseGesture': MouseGesture,
     'Active': Active,
+    'Device': Device,
     'KeyPress': KeyPress,
     'MouseScroll': MouseScroll,
     'MouseClick': MouseClick,
diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py
--- a/lib/solaar/ui/diversion_rules.py
+++ b/lib/solaar/ui/diversion_rules.py
@@ -520,6 +520,7 @@ def _menu_insert(self, m, it, below=False):
                         (_('Key'), _DIV.Key, ''),
                         (_('KeyIsDown'), _DIV.KeyIsDown, ''),
                         (_('Active'), _DIV.Active, ''),
+                        (_('Device'), _DIV.Device, ''),
                         (_('Setting'), _DIV.Setting, [None, '', None]),
                         (_('Test'), _DIV.Test, next(iter(_DIV.TESTS))),
                         (_('Test bytes'), _DIV.TestBytes, [0, 1, 0]),
@@ -2191,6 +2192,13 @@ def _all_settings():
 class _DeviceUI:
     label_text = ''
 
+    def show(self, component, editable):
+        super().show(component, editable)
+        with self.ignore_changes():
+            same = not component.devID
+            device = _all_devices[component.devID]
+            self.device_field.set_value(device.id if device else '' if same else component.devID or '')
+
     def create_widgets(self):
         self.widgets = {}
         self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
@@ -2218,19 +2226,6 @@ def _update_device_list(self):
         with self.ignore_changes():
             self.device_field.set_all_values([(d.id, d.display_name, *d.identifiers[1:]) for d in _all_devices])
 
-
-class ActiveUI(_DeviceUI, ConditionUI):
-
-    CLASS = _DIV.Active
-    label_text = _('Device is active and its settings can be changed.')
-
-    def show(self, component, editable):
-        super().show(component, editable)
-        with self.ignore_changes():
-            same = not component.devID
-            device = _all_devices[component.devID]
-            self.device_field.set_value(device.id if device else '' if same else component.devID or '')
-
     def collect_value(self):
         device_str = self.device_field.get_value()
         same = device_str in ['', _('Originating device')]
@@ -2238,14 +2233,30 @@ def collect_value(self):
         device_value = device.id if device else None if same else device_str
         return device_value
 
+    @classmethod
+    def right_label(cls, component):
+        device = _all_devices[component.devID]
+        return device.display_name if device else shlex_quote(component.devID)
+
+
+class ActiveUI(_DeviceUI, ConditionUI):
+
+    CLASS = _DIV.Active
+    label_text = _('Device is active and its settings can be changed.')
+
     @classmethod
     def left_label(cls, component):
         return _('Active')
 
+
+class DeviceUI(_DeviceUI, ConditionUI):
+
+    CLASS = _DIV.Device
+    label_text = _('Device originated the current notification.')
+
     @classmethod
-    def right_label(cls, component):
-        device = _all_devices[component.devID]
-        return device.display_name if device else shlex_quote(component.devID)
+    def left_label(cls, component):
+        return _('Device')
 
 
 class _SettingWithValueUI:
@@ -2612,6 +2623,7 @@ def _on_update(self, *_args):
     _DIV.Process: ProcessUI,
     _DIV.MouseProcess: MouseProcessUI,
     _DIV.Active: ActiveUI,
+    _DIV.Device: DeviceUI,
     _DIV.Feature: FeatureUI,
     _DIV.Report: ReportUI,
     _DIV.Modifiers: ModifiersUI,


Test patch: