diff --git a/CHANGELOG.md b/CHANGELOG.md index abbb119..06b4319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project are documented here. The format follows ## [Unreleased] +### Added + +- `new_device` tells whether the device is alone on its port (`direct`, `devices_on_port`). The + blueprint reports only devices plugged in directly unless *Only devices plugged in directly* + is off; a phone joining the router's Wi-Fi was reported as new on the port towards the router + ([#19](https://github.com/eizedev/ha-managed-switch/issues/19)). + ## [0.2.0] - 2026-10-30 ### Added diff --git a/blueprints/automation/managed_switch/notify_switch_events.yaml b/blueprints/automation/managed_switch/notify_switch_events.yaml index f8a9ad9..628068c 100644 --- a/blueprints/automation/managed_switch/notify_switch_events.yaml +++ b/blueprints/automation/managed_switch/notify_switch_events.yaml @@ -30,6 +30,15 @@ blueprint: value: restarted - label: New device value: new_device + direct_only: + name: Only devices plugged in directly + description: >- + Report a new device only when it is alone on its port. A port to a router, an access + point or an unmanaged switch carries many devices; with this off, a phone joining the + router's Wi-Fi is reported as "seen via" the port that leads to the router. + default: true + selector: + boolean: {} notify_action: name: Notification description: What to do, for example a notification to your phone. @@ -51,6 +60,7 @@ triggers: variables: event_types: !input event_types + direct_only: !input direct_only event_type: "{{ trigger.to_state.attributes.event_type }}" switch: >- {% set device = device_id(trigger.entity_id) %} @@ -62,11 +72,14 @@ variables: {% if event_type == 'uplink_down' %}Port {{ port }} (uplink or trunk) lost its link. {% elif event_type == 'speed_dropped' %}Port {{ port }} runs at {{ a.speed }} Mbit/s instead of {{ a.previous }} Mbit/s. {% elif event_type == 'restarted' %}The switch restarted. - {% elif event_type == 'new_device' %}New device {{ a.name or a.mac }} on port {{ port }}. + {% elif event_type == 'new_device' and a.direct | default(true) %}New device {{ a.name or a.mac }} on port {{ port }}. + {% elif event_type == 'new_device' %}New device {{ a.name or a.mac }} seen via port {{ port }} (with {{ a.devices_on_port - 1 }} other devices). {% endif %} conditions: - condition: template - value_template: "{{ event_type in event_types }}" + value_template: >- + {{ event_type in event_types and not (event_type == 'new_device' and direct_only + and not trigger.to_state.attributes.direct | default(true)) }} actions: !input notify_action diff --git a/custom_components/managed_switch/event.py b/custom_components/managed_switch/event.py index ad748e5..52c2b79 100644 --- a/custom_components/managed_switch/event.py +++ b/custom_components/managed_switch/event.py @@ -138,5 +138,9 @@ def _new_device(self, location: MacLocation) -> None: "mac": location.address, "name": registry_name(self.hass, location.address), "ip_addresses": list(location.ip_addresses), + # More than one device on the port: the device is behind a router, access point + # or unmanaged switch there, maybe not plugged in at this switch at all. + "devices_on_port": location.devices_on_port, + "direct": location.devices_on_port == 1, }, ) diff --git a/docs/assets/screenshots/card.png b/docs/assets/screenshots/card.png index 1ed9881..4ec0953 100644 Binary files a/docs/assets/screenshots/card.png and b/docs/assets/screenshots/card.png differ diff --git a/docs/card.md b/docs/card.md index ba6c0eb..ca8ce6d 100644 --- a/docs/card.md +++ b/docs/card.md @@ -4,7 +4,7 @@ web interface: every port coloured by link and speed, uplinks to other switches, trunk members, PoE delivering power, and the device on each port. -![Three switches as cards: green ports at 1 Gbit/s, orange at 100 Mbit/s, white without link, arrows on the links between the switches, the device name under each port](assets/screenshots/card.png) +![Three switches as cards: green ports at 1 Gbit/s, orange at 100 Mbit/s, empty without link, a link symbol on the ports between the switches, the device name under each port](assets/screenshots/card.png) ## Install @@ -31,7 +31,7 @@ Install **Managed Switch Card** and reload the browser. Then edit a dashboard, * | blue | link, speed unknown (the port's speed sensor is off or has no value) | | empty | no link | | grey | unknown: the switch did not answer, or the port's link sensor is off | -| arrow | link to another of your switches | +| ⇄ | link to another of your switches (LLDP) | | flash | PoE delivering power | | T | member of a trunk; the trunks are listed below the ports | diff --git a/docs/events.md b/docs/events.md index 99e0aca..f622dfc 100644 --- a/docs/events.md +++ b/docs/events.md @@ -8,19 +8,25 @@ attribute `event_type` says which: | `uplink_down` | a link to another of your switches, a trunk or a trunk member lost its link | `port`, `if_index` | | `speed_dropped` | a port is up at a lower speed than it had before (a bad cable, a port that negotiated 100 Mbit/s) | `port`, `if_index`, `speed`, `previous` | | `restarted` | the switch booted since the last reading | `boot_time` | -| `new_device` | a device is seen for the first time on any of your switches | `port`, `if_index`, `mac`, `name`, `ip_addresses` | +| `new_device` | a device is seen for the first time on any of your switches | `port`, `if_index`, `mac`, `name`, `ip_addresses`, `devices_on_port`, `direct` | Nothing fires for the state at startup. New devices are remembered across restarts (the 5000 seen most recently); on the very first run, and for a switch that is added or whose address table is read for the first time, all devices are taken as known. Phones and laptops that use a new random MAC -address for a network now and then appear as new each time. The event's attributes, including +address for a network now and then appear as new each time. + +`direct` is true when the device is alone on its port. On a port that carries several devices +(`devices_on_port`), it sits behind a router, an access point or an unmanaged switch there: a +phone joining the router's Wi-Fi shows up on the port that leads to the router, although nothing +was plugged in. The blueprint reports only direct devices unless you turn that off. The event's attributes, including MAC and IP addresses, are kept in Home Assistant's history like any other state. ## Blueprint [![Open your Home Assistant instance and show the blueprint import dialog.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Feizedev%2Fha-managed-switch%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fmanaged_switch%2Fnotify_switch_events.yaml) -*Managed switch events* sends a notification for the events you choose. Pick the events entity +*Managed switch events* sends a notification for the events you choose; new devices only when +plugged in directly, unless you turn *Only devices plugged in directly* off. Pick the events entity of a switch and an action, for example a notification to your phone; by default it creates a persistent notification. diff --git a/tests/test_event.py b/tests/test_event.py index 10a4f9c..6c5782b 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -91,9 +91,30 @@ async def test_new_device(hass: HomeAssistant) -> None: attributes = last(hass) assert attributes["event_type"] == "new_device" assert (attributes["mac"], attributes["port"]) == (forgotten, location.port_name) + assert attributes["devices_on_port"] == location.devices_on_port + assert attributes["direct"] is (location.devices_on_port == 1) assert entry.entry_id == location.switch +async def test_new_device_alone_or_shared(hass: HomeAssistant) -> None: + entries = await setup(hass, "192.0.2.1", "192.0.2.2", "192.0.2.3") + events = {e.entry_id: f"event.{e.title.replace('-', '_')}_switch_events" for e in entries} + hub = get_hub(hass) + assert hub._known is not None + seen = {} + for alone in (True, False): + mac, loc = next( + (m, loc) for m, loc in hub.locations.items() if (loc.devices_on_port == 1) is alone + ) + hub._known.pop(mac, None) + hub.async_update() + await hass.async_block_till_done() + attributes = state(hass, events[loc.switch]).attributes + seen[alone] = (attributes["mac"], attributes["direct"]) + assert attributes["devices_on_port"] == loc.devices_on_port + assert seen[alone] == (mac, alone) + + async def test_new_device_on_another_switch(hass: HomeAssistant) -> None: await setup(hass, "192.0.2.1", "192.0.2.3") hub = get_hub(hass)