From cf47a6c515caf37a3ef0562509d6f86572ca5469 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Sep 2023 11:12:44 +0200 Subject: [PATCH] Add UniFi device uptime and temperature sensors (#99307) * Add UniFi device uptime and temperature sensors * Add native_unit_of_measurement to temperature Remove seconds and milliseconds from device uptime --- homeassistant/components/unifi/sensor.py | 50 ++++++++++- tests/components/unifi/test_sensor.py | 107 ++++++++++++++++++++++- 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 142bd587853a74..7cb0b2bbfe3d6e 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -27,6 +27,7 @@ SensorDeviceClass, SensorEntity, SensorEntityDescription, + UnitOfTemperature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfInformation, UnitOfPower @@ -88,6 +89,16 @@ def async_wlan_client_value_fn(controller: UniFiController, wlan: Wlan) -> int: ) +@callback +def async_device_uptime_value_fn( + controller: UniFiController, device: Device +) -> datetime: + """Calculate the uptime of the device.""" + return (dt_util.now() - timedelta(seconds=device.uptime)).replace( + second=0, microsecond=0 + ) + + @callback def async_device_outlet_power_supported_fn( controller: UniFiController, obj_id: str @@ -178,7 +189,7 @@ class UnifiSensorEntityDescription( value_fn=lambda _, obj: obj.poe_power if obj.poe_mode != "off" else "0", ), UnifiSensorEntityDescription[Clients, Client]( - key="Uptime sensor", + key="Client uptime", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, has_entity_name=True, @@ -272,6 +283,43 @@ class UnifiSensorEntityDescription( unique_id_fn=lambda controller, obj_id: f"ac_power_conumption-{obj_id}", value_fn=lambda controller, device: device.outlet_ac_power_consumption, ), + UnifiSensorEntityDescription[Devices, Device]( + key="Device uptime", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + has_entity_name=True, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + name_fn=lambda device: "Uptime", + object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=False, + supported_fn=lambda controller, obj_id: True, + unique_id_fn=lambda controller, obj_id: f"device_uptime-{obj_id}", + value_fn=async_device_uptime_value_fn, + ), + UnifiSensorEntityDescription[Devices, Device]( + key="Device temperature", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + has_entity_name=True, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + name_fn=lambda device: "Temperature", + object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=False, + supported_fn=lambda ctrlr, obj_id: ctrlr.api.devices[obj_id].has_temperature, + unique_id_fn=lambda controller, obj_id: f"device_temperature-{obj_id}", + value_fn=lambda ctrlr, device: device.general_temperature, + ), ) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 7ed87512f2bad7..7b6a3bc1edc6a7 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -566,7 +566,7 @@ async def test_poe_port_switches( ) -> None: """Test the update_items function with some clients.""" await setup_unifi_integration(hass, aioclient_mock, devices_response=[DEVICE_1]) - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 ent_reg = er.async_get(hass) ent_reg_entry = ent_reg.async_get("sensor.mock_name_port_1_poe_power") @@ -788,8 +788,8 @@ async def test_outlet_power_readings( """Test the outlet power reporting on PDU devices.""" await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1]) - assert len(hass.states.async_all()) == 9 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 + assert len(hass.states.async_all()) == 10 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 ent_reg = er.async_get(hass) ent_reg_entry = ent_reg.async_get(f"sensor.{entity_id}") @@ -809,3 +809,104 @@ async def test_outlet_power_readings( sensor_data = hass.states.get(f"sensor.{entity_id}") assert sensor_data.state == expected_update_value + + +async def test_device_uptime( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket +) -> None: + """Verify that uptime sensors are working as expected.""" + device = { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "uptime": 60, + "version": "4.0.42.10433", + } + + now = datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) + + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 + assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" + + ent_reg = er.async_get(hass) + assert ( + ent_reg.async_get("sensor.device_uptime").entity_category + is EntityCategory.DIAGNOSTIC + ) + + # Verify normal new event doesn't change uptime + # 4 seconds has passed + + device["uptime"] = 64 + now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + await hass.async_block_till_done() + + assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" + + # Verify new event change uptime + # 1 month has passed + + device["uptime"] = 60 + now = datetime(2021, 2, 1, 1, 1, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + await hass.async_block_till_done() + + assert hass.states.get("sensor.device_uptime").state == "2021-02-01T01:00:00+00:00" + + +async def test_device_temperature( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket +) -> None: + """Verify that temperature sensors are working as expected.""" + device = { + "board_rev": 3, + "device_id": "mock-id", + "general_temperature": 30, + "has_fan": True, + "has_temperature": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "uptime": 60, + "version": "4.0.42.10433", + } + + await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + assert hass.states.get("sensor.device_temperature").state == "30" + + ent_reg = er.async_get(hass) + assert ( + ent_reg.async_get("sensor.device_temperature").entity_category + is EntityCategory.DIAGNOSTIC + ) + + # Verify new event change temperature + device["general_temperature"] = 60 + mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + await hass.async_block_till_done() + assert hass.states.get("sensor.device_temperature").state == "60"