diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 6115821b000..e40fb30a62c 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -16,6 +16,7 @@ import homeassistant.helpers.config_validation as cv from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, @@ -312,7 +313,11 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_ALLOW_BANDWIDTH_SENSORS, default=self.controller.option_allow_bandwidth_sensors, - ): bool + ): bool, + vol.Optional( + CONF_ALLOW_UPTIME_SENSORS, + default=self.controller.option_allow_uptime_sensors, + ): bool, } ), ) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 803a892647f..42d160f2dea 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -12,6 +12,7 @@ CONF_SITE_ID = "site" UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors" +CONF_ALLOW_UPTIME_SENSORS = "allow_uptime_sensors" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" CONF_IGNORE_WIRED_BUG = "ignore_wired_bug" @@ -22,6 +23,7 @@ CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" CONF_SSID_FILTER = "ssid_filter" DEFAULT_ALLOW_BANDWIDTH_SENSORS = False +DEFAULT_ALLOW_UPTIME_SENSORS = False DEFAULT_IGNORE_WIRED_BUG = False DEFAULT_POE_CLIENTS = True DEFAULT_TRACK_CLIENTS = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 7c30a34f58f..6fc5b3d9ed7 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -33,6 +33,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, @@ -45,6 +46,7 @@ from .const import ( CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_ALLOW_UPTIME_SENSORS, DEFAULT_DETECTION_TIME, DEFAULT_IGNORE_WIRED_BUG, DEFAULT_POE_CLIENTS, @@ -184,6 +186,13 @@ class UniFiController: CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS ) + @property + def option_allow_uptime_sensors(self): + """Config entry option to allow uptime sensors.""" + return self.config_entry.options.get( + CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS + ) + @callback def async_unifi_signalling_callback(self, signal, data): """Handle messages back from UniFi library.""" diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 8fdb0ac1461..59aff09811f 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -1,10 +1,11 @@ """Support for bandwidth sensors with UniFi clients.""" import logging -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN from homeassistant.const import DATA_MEGABYTES from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN from .unifi_client import UniFiClient @@ -13,6 +14,7 @@ LOGGER = logging.getLogger(__name__) RX_SENSOR = "rx" TX_SENSOR = "tx" +UPTIME_SENSOR = "uptime" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -22,7 +24,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for UniFi integration.""" controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - controller.entities[DOMAIN] = {RX_SENSOR: set(), TX_SENSOR: set()} + controller.entities[DOMAIN] = { + RX_SENSOR: set(), + TX_SENSOR: set(), + UPTIME_SENSOR: set(), + } @callback def items_added( @@ -30,7 +36,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -> None: """Update the values of the controller.""" if controller.option_allow_bandwidth_sensors: - add_entities(controller, async_add_entities, clients) + add_bandwith_entities(controller, async_add_entities, clients) + + if controller.option_allow_uptime_sensors: + add_uptime_entities(controller, async_add_entities, clients) for signal in (controller.signal_update, controller.signal_options_update): controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) @@ -39,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def add_entities(controller, async_add_entities, clients): +def add_bandwith_entities(controller, async_add_entities, clients): """Add new sensor entities from the controller.""" sensors = [] @@ -55,6 +64,22 @@ def add_entities(controller, async_add_entities, clients): async_add_entities(sensors) +@callback +def add_uptime_entities(controller, async_add_entities, clients): + """Add new sensor entities from the controller.""" + sensors = [] + + for mac in clients: + if mac in controller.entities[DOMAIN][UniFiUpTimeSensor.TYPE]: + continue + + client = controller.api.clients[mac] + sensors.append(UniFiUpTimeSensor(client, controller)) + + if sensors: + async_add_entities(sensors) + + class UniFiBandwidthSensor(UniFiClient): """UniFi bandwidth sensor base class.""" @@ -100,3 +125,30 @@ class UniFiTxBandwidthSensor(UniFiBandwidthSensor): if self._is_wired: return self.client.wired_tx_bytes / 1000000 return self.client.tx_bytes / 1000000 + + +class UniFiUpTimeSensor(UniFiClient): + """UniFi uptime sensor.""" + + DOMAIN = DOMAIN + TYPE = UPTIME_SENSOR + + @property + def device_class(self) -> str: + """Return device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def name(self) -> str: + """Return the name of the client.""" + return f"{super().name} {self.TYPE.capitalize()}" + + @property + def state(self) -> int: + """Return the uptime of the client.""" + return dt_util.utc_from_timestamp(float(self.client.uptime)).isoformat() + + async def options_updated(self) -> None: + """Config entry options are updated, remove entity if option is disabled.""" + if not self.controller.option_allow_uptime_sensors: + await self.remove_item({self.client.mac}) diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 95d273278bd..ba0b3952dd0 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -54,7 +54,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" + "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients", + "allow_uptime_sensors": "Uptime sensors for network clients" }, "description": "Configure statistics sensors", "title": "UniFi options 3/3" diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 37a6148d377..aeeaba60f0f 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -58,7 +58,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" + "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients", + "allow_uptime_sensors": "Maak uptime-sensoren voor netwerkclients" }, "description": "Configureer statistische sensoren", "title": "UniFi-opties 3/3" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index a1af12dfb76..aec6ebed664 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -4,6 +4,7 @@ import aiounifi from homeassistant import data_entry_flow from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, @@ -341,7 +342,11 @@ async def test_advanced_option_flow(hass): assert result["step_id"] == "statistics_sensors" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_ALLOW_BANDWIDTH_SENSORS: True} + result["flow_id"], + user_input={ + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -355,6 +360,7 @@ async def test_advanced_option_flow(hass): CONF_POE_CLIENTS: False, CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, } diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 5600f454336..5fee4a85f9a 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -13,6 +13,7 @@ from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_ALLOW_UPTIME_SENSORS, DEFAULT_DETECTION_TIME, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, @@ -49,6 +50,7 @@ CONTROLLER_HOST = { "sw_port": 1, "wired-rx_bytes": 1234000000, "wired-tx_bytes": 5678000000, + "uptime": 1562600160, } CONTROLLER_DATA = { @@ -175,6 +177,7 @@ async def test_controller_setup(hass): assert controller.site_role == SITES[controller.site_name]["role"] assert controller.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS + assert controller.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS assert isinstance(controller.option_block_clients, list) assert controller.option_track_clients == DEFAULT_TRACK_CLIENTS assert controller.option_track_devices == DEFAULT_TRACK_DEVICES diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 2d5fbe96e0f..690b9d77899 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -8,10 +8,12 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, DOMAIN as UNIFI_DOMAIN, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component from .test_controller import setup_unifi_integration @@ -29,6 +31,7 @@ CLIENTS = [ "sw_port": 1, "wired-rx_bytes": 1234000000, "wired-tx_bytes": 5678000000, + "uptime": 1600094505, }, { "hostname": "Wireless client hostname", @@ -42,6 +45,7 @@ CLIENTS = [ "sw_port": 2, "rx_bytes": 1234000000, "tx_bytes": 5678000000, + "uptime": 1600094505, }, ] @@ -61,7 +65,10 @@ async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" controller = await setup_unifi_integration( hass, - options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, + options={ + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + }, ) assert len(controller.mock_requests) == 4 @@ -74,6 +81,7 @@ async def test_sensors(hass): hass, options={ CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, }, @@ -81,7 +89,7 @@ async def test_sensors(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") assert wired_client_rx.state == "1234.0" @@ -89,16 +97,23 @@ async def test_sensors(hass): wired_client_tx = hass.states.get("sensor.wired_client_name_tx") assert wired_client_tx.state == "5678.0" + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime.state == "2020-09-14T14:41:45+00:00" + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") assert wireless_client_rx.state == "1234.0" wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx.state == "5678.0" + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime.state == "2020-09-14T14:41:45+00:00" + clients = deepcopy(CLIENTS) clients[0]["is_wired"] = False clients[1]["rx_bytes"] = 2345000000 clients[1]["tx_bytes"] = 6789000000 + clients[1]["uptime"] = 1600180860 event = {"meta": {"message": MESSAGE_CLIENT}, "data": clients} controller.api.message_handler(event) @@ -110,9 +125,15 @@ async def test_sensors(hass): wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx.state == "6789.0" + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime.state == "2020-09-15T14:41:00+00:00" + hass.config_entries.async_update_entry( controller.config_entry, - options={CONF_ALLOW_BANDWIDTH_SENSORS: False}, + options={ + CONF_ALLOW_BANDWIDTH_SENSORS: False, + CONF_ALLOW_UPTIME_SENSORS: False, + }, ) await hass.async_block_till_done() @@ -122,9 +143,18 @@ async def test_sensors(hass): wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx is None + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime is None + + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime is None + hass.config_entries.async_update_entry( controller.config_entry, - options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, + options={ + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + }, ) await hass.async_block_till_done() @@ -134,15 +164,42 @@ async def test_sensors(hass): wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx.state == "6789.0" + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime.state == "2020-09-15T14:41:00+00:00" + + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime.state == "2020-09-14T14:41:45+00:00" + + # Try to add the sensors again, using a signal + clients_connected = set() + devices_connected = set() + + clients_connected.add(clients[0]["mac"]) + clients_connected.add(clients[1]["mac"]) + + async_dispatcher_send( + hass, + controller.signal_update, + clients_connected, + devices_connected, + ) + + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 + async def test_remove_sensors(hass): """Test the remove_items function with some clients.""" controller = await setup_unifi_integration( hass, - options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, + options={ + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + }, clients_response=CLIENTS, ) - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") @@ -150,11 +207,17 @@ async def test_remove_sensors(hass): wired_client_tx = hass.states.get("sensor.wired_client_name_tx") assert wired_client_tx is not None + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime is not None + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") assert wireless_client_rx is not None wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx is not None + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime is not None + controller.api.websocket._data = { "meta": {"message": MESSAGE_CLIENT_REMOVED}, "data": [CLIENTS[0]], @@ -162,7 +225,7 @@ async def test_remove_sensors(hass): controller.api.session_handler(SIGNAL_DATA) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") @@ -170,7 +233,13 @@ async def test_remove_sensors(hass): wired_client_tx = hass.states.get("sensor.wired_client_name_tx") assert wired_client_tx is None + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime is None + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") assert wireless_client_rx is not None wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx is not None + + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime is not None