From 82798730185dfef0e5c11b5ba006372872982cc8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Dec 2021 03:47:10 +0100 Subject: [PATCH] Extend entities provided by Tailscale (#60785) --- .../components/tailscale/binary_sensor.py | 44 +++++++++++ homeassistant/components/tailscale/sensor.py | 19 ++++- .../tailscale/test_binary_sensor.py | 79 ++++++++++++++++++- tests/components/tailscale/test_sensor.py | 24 ++++++ 4 files changed, 163 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py index 17a40d04241..fff4cfbf908 100644 --- a/homeassistant/components/tailscale/binary_sensor.py +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TailscaleEntity @@ -38,8 +39,51 @@ BINARY_SENSORS: tuple[TailscaleBinarySensorEntityDescription, ...] = ( key="update_available", name="Client", device_class=BinarySensorDeviceClass.UPDATE, + entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.update_available, ), + TailscaleBinarySensorEntityDescription( + key="client_supports_hair_pinning", + name="Supports Hairpinning", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.hair_pinning, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_ipv6", + name="Supports IPv6", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.ipv6, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_pcp", + name="Supports PCP", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.pcp, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_pmp", + name="Supports NAT-PMP", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.pmp, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_udp", + name="Supports UDP", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.udp, + ), + TailscaleBinarySensorEntityDescription( + key="client_supports_upnp", + name="Supports UPnP", + icon="mdi:wan", + entity_category=EntityCategory.DIAGNOSTIC, + is_on_fn=lambda device: device.client_connectivity.client_supports.upnp, + ), ) diff --git a/homeassistant/components/tailscale/sensor.py b/homeassistant/components/tailscale/sensor.py index d6ffdd177bf..07f7dbe91cc 100644 --- a/homeassistant/components/tailscale/sensor.py +++ b/homeassistant/components/tailscale/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TailscaleEntity @@ -24,7 +25,7 @@ from .const import DOMAIN class TailscaleSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[TailscaleDevice], datetime | None] + value_fn: Callable[[TailscaleDevice], datetime | str | None] @dataclass @@ -39,8 +40,22 @@ SENSORS: tuple[TailscaleSensorEntityDescription, ...] = ( key="expires", name="Expires", device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.expires, ), + TailscaleSensorEntityDescription( + key="ip", + name="IP Address", + icon="mdi:ip-network", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.addresses[0] if device.addresses else None, + ), + TailscaleSensorEntityDescription( + key="last_seen", + name="Last Seen", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda device: device.last_seen, + ), ) @@ -68,6 +83,6 @@ class TailscaleSensorEntity(TailscaleEntity, SensorEntity): entity_description: TailscaleSensorEntityDescription @property - def native_value(self) -> datetime | None: + def native_value(self) -> datetime | str | None: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data[self.device_id]) diff --git a/tests/components/tailscale/test_binary_sensor.py b/tests/components/tailscale/test_binary_sensor.py index e3d0c9c9348..9caeb7b8eba 100644 --- a/tests/components/tailscale/test_binary_sensor.py +++ b/tests/components/tailscale/test_binary_sensor.py @@ -1,9 +1,14 @@ """Tests for the sensors provided by the Tailscale integration.""" -from homeassistant.components.binary_sensor import STATE_ON, BinarySensorDeviceClass +from homeassistant.components.binary_sensor import ( + STATE_OFF, + STATE_ON, + BinarySensorDeviceClass, +) from homeassistant.components.tailscale.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.common import MockConfigEntry @@ -21,11 +26,83 @@ async def test_tailscale_binary_sensors( assert entry assert state assert entry.unique_id == "123456_update_available" + assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_ON assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Client" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.UPDATE assert ATTR_ICON not in state.attributes + state = hass.states.get("binary_sensor.frencks_iphone_supports_hairpinning") + entry = entity_registry.async_get( + "binary_sensor.frencks_iphone_supports_hairpinning" + ) + assert entry + assert state + assert entry.unique_id == "123456_client_supports_hair_pinning" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frencks-iPhone Supports Hairpinning" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_ipv6") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_ipv6") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_ipv6" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports IPv6" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_pcp") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_pcp") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_pcp" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports PCP" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_nat_pmp") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_nat_pmp") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_pmp" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports NAT-PMP" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_udp") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_udp") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_udp" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_ON + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports UDP" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("binary_sensor.frencks_iphone_supports_upnp") + entry = entity_registry.async_get("binary_sensor.frencks_iphone_supports_upnp") + assert entry + assert state + assert entry.unique_id == "123456_client_supports_upnp" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frencks-iPhone Supports UPnP" + assert state.attributes.get(ATTR_ICON) == "mdi:wan" + assert ATTR_DEVICE_CLASS not in state.attributes + assert entry.device_id device_entry = device_registry.async_get(entry.device_id) assert device_entry diff --git a/tests/components/tailscale/test_sensor.py b/tests/components/tailscale/test_sensor.py index 0a8918f04d1..911de0eb64a 100644 --- a/tests/components/tailscale/test_sensor.py +++ b/tests/components/tailscale/test_sensor.py @@ -4,6 +4,7 @@ from homeassistant.components.tailscale.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.common import MockConfigEntry @@ -21,11 +22,34 @@ async def test_tailscale_sensors( assert entry assert state assert entry.unique_id == "123457_expires" + assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "2022-02-25T09:49:06+00:00" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Expires" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes + state = hass.states.get("sensor.router_last_seen") + entry = entity_registry.async_get("sensor.router_last_seen") + assert entry + assert state + assert entry.unique_id == "123457_last_seen" + assert entry.entity_category is None + assert state.state == "2021-11-15T20:37:03+00:00" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Last Seen" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.router_ip_address") + entry = entity_registry.async_get("sensor.router_ip_address") + assert entry + assert state + assert entry.unique_id == "123457_ip" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "100.11.11.112" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router IP Address" + assert state.attributes.get(ATTR_ICON) == "mdi:ip-network" + assert ATTR_DEVICE_CLASS not in state.attributes + assert entry.device_id device_entry = device_registry.async_get(entry.device_id) assert device_entry