mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-07-09 07:17:13 -04:00
@@ -195,7 +195,9 @@ This workflow automatically archives devices if a new device is discovered with
|
||||
|
||||
### 📋 Use Case
|
||||
|
||||
This workflow is useful if you are assigning static IPs to your devices. This workflow can also help with archiving device entries with [random MAC addresses](./RANDOM_MAC.md).
|
||||
This workflow is useful if you are assigning static IPs to your devices. This workflow can also help with archiving device entries with [random MAC addresses](./RANDOM_MAC.md).
|
||||
|
||||
The `not_equals` condition on `devMac` ensures the device that triggered the workflow is never archived — only the *other* device sharing the same IP is targeted.
|
||||
|
||||
### ⚙️ Workflow Configuration
|
||||
|
||||
@@ -224,6 +226,11 @@ This workflow is useful if you are assigning static IPs to your devices. This wo
|
||||
"field": "devLastIP",
|
||||
"operator": "equals",
|
||||
"value": "{{trigger.devLastIP}}"
|
||||
},
|
||||
{
|
||||
"field": "devMac",
|
||||
"operator": "not_equals",
|
||||
"value": "{{trigger.devMac}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -240,15 +247,15 @@ This workflow is useful if you are assigning static IPs to your devices. This wo
|
||||
|
||||
* `N/A`
|
||||
|
||||
* **Target Conditions**:
|
||||
* **Target Conditions** (evaluated as AND):
|
||||
|
||||
* `devLastIP` of the target is the same as the newly discovered device's `devLastIP` value.
|
||||
* `devLastIP` equals the triggering device's IP (`{{trigger.devLastIP}}`).
|
||||
* `devMac` **not equals** the triggering device's MAC (`{{trigger.devMac}}`).
|
||||
|
||||
* **Actions**:
|
||||
|
||||
1. Sets `devIsArchived` to `1` (mark target device as archived).
|
||||
|
||||
|
||||
### ✅ Result
|
||||
|
||||
Any newly detected device that has the same IP as an existing device will automatically trigger the archival of the old device.
|
||||
Any existing device that shares the same IP as the newly discovered device is automatically archived. The triggering device itself is excluded from the target query, so it is never archived by its own workflow.
|
||||
|
||||
@@ -66,7 +66,7 @@ let wfEnabledOptions = [
|
||||
];
|
||||
|
||||
let operatorTypes = [
|
||||
"equals", "contains" , "regex"
|
||||
"equals", "not_equals", "contains", "not_contains", "regex"
|
||||
];
|
||||
|
||||
let actionTypes = [
|
||||
|
||||
@@ -106,7 +106,7 @@ class DeviceInstance:
|
||||
"""Query Devices using a list of condition dicts.
|
||||
|
||||
Each condition dict must have ``field``, ``operator``, and ``value`` keys.
|
||||
Supported operators: ``equals``, ``contains``.
|
||||
Supported operators: ``equals``, ``not_equals``, ``contains``, ``not_contains``.
|
||||
|
||||
Returns a list of device dicts (may be empty). Only fields present in
|
||||
the Devices schema are accepted; unrecognised fields are skipped with a
|
||||
@@ -131,9 +131,15 @@ class DeviceInstance:
|
||||
if operator == "equals":
|
||||
clauses.append(f"{field} = ?")
|
||||
params.append(value)
|
||||
elif operator == "not_equals":
|
||||
clauses.append(f"{field} != ?")
|
||||
params.append(value)
|
||||
elif operator == "contains":
|
||||
clauses.append(f"{field} LIKE ?")
|
||||
params.append(f"%{value}%")
|
||||
elif operator == "not_contains":
|
||||
clauses.append(f"{field} NOT LIKE ?")
|
||||
params.append(f"%{value}%")
|
||||
else:
|
||||
mylog("none", [f"[WF] queryByConditions: unsupported operator '{operator}' — skipped"])
|
||||
continue
|
||||
|
||||
@@ -36,8 +36,12 @@ class Condition:
|
||||
# process based on operators
|
||||
if self.operator == "equals":
|
||||
result = str(obj_value) == str(self.value)
|
||||
elif self.operator == "not_equals":
|
||||
result = str(obj_value) != str(self.value)
|
||||
elif self.operator == "contains":
|
||||
result = str(self.value).lower() in str(obj_value).lower()
|
||||
elif self.operator == "not_contains":
|
||||
result = str(self.value).lower() not in str(obj_value).lower()
|
||||
elif self.operator == "regex":
|
||||
result = bool(re.match(self.value, str(obj_value)))
|
||||
else:
|
||||
|
||||
@@ -241,6 +241,39 @@ class TestQueryByConditions(unittest.TestCase):
|
||||
])
|
||||
self.assertEqual(results, [])
|
||||
|
||||
def test_not_equals_operator(self):
|
||||
inst = self._instance()
|
||||
results = inst.queryByConditions([
|
||||
{"field": "devLastIP", "operator": "not_equals", "value": "192.168.1.10"}
|
||||
])
|
||||
macs = {r["devMac"] for r in results}
|
||||
self.assertNotIn("aa:bb:cc:dd:ee:01", macs)
|
||||
self.assertNotIn("aa:bb:cc:dd:ee:02", macs)
|
||||
self.assertIn("aa:bb:cc:dd:ee:03", macs)
|
||||
|
||||
def test_not_contains_operator(self):
|
||||
inst = self._instance()
|
||||
results = inst.queryByConditions([
|
||||
{"field": "devLastIP", "operator": "not_contains", "value": "192.168.1.1"}
|
||||
])
|
||||
macs = {r["devMac"] for r in results}
|
||||
# .10 contains "192.168.1.1", .20 does not
|
||||
self.assertNotIn("aa:bb:cc:dd:ee:01", macs)
|
||||
self.assertNotIn("aa:bb:cc:dd:ee:02", macs)
|
||||
self.assertIn("aa:bb:cc:dd:ee:03", macs)
|
||||
|
||||
def test_not_equals_combined_with_equals(self):
|
||||
"""Exclude trigger device by MAC while matching on IP — the core use case."""
|
||||
inst = self._instance()
|
||||
results = inst.queryByConditions([
|
||||
{"field": "devLastIP", "operator": "equals", "value": "192.168.1.10"},
|
||||
{"field": "devMac", "operator": "not_equals", "value": "aa:bb:cc:dd:ee:01"},
|
||||
])
|
||||
macs = {r["devMac"] for r in results}
|
||||
self.assertNotIn("aa:bb:cc:dd:ee:01", macs)
|
||||
self.assertIn("aa:bb:cc:dd:ee:02", macs)
|
||||
self.assertNotIn("aa:bb:cc:dd:ee:03", macs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UpdateFieldAction — boolean cast
|
||||
|
||||
Reference in New Issue
Block a user