Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions blueprints/automation/managed_switch/notify_switch_events.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) %}
Expand All @@ -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
4 changes: 4 additions & 0 deletions custom_components/managed_switch/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
)
Binary file modified docs/assets/screenshots/card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/card.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand Down
12 changes: 9 additions & 3 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
21 changes: 21 additions & 0 deletions tests/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading