Merge pull request #1693 from netalertx/next_release

Next release
This commit is contained in:
Jokob @NetAlertX
2026-07-01 18:31:14 +10:00
committed by GitHub
5 changed files with 57 additions and 7 deletions

View File

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

View File

@@ -66,7 +66,7 @@ let wfEnabledOptions = [
];
let operatorTypes = [
"equals", "contains" , "regex"
"equals", "not_equals", "contains", "not_contains", "regex"
];
let actionTypes = [

View File

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

View File

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

View File

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