Add UniFi Protect light platform (#63137)

This commit is contained in:
Christopher Bailey 2021-12-31 16:21:29 -05:00 committed by GitHub
parent b17120a511
commit 0de3a299d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 296 additions and 3 deletions

View File

@ -41,4 +41,4 @@ DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT}
MIN_REQUIRED_PROTECT_V = Version("1.20.0") MIN_REQUIRED_PROTECT_V = Version("1.20.0")
OUTDATED_LOG_MESSAGE = "You are running v%s of UniFi Protect. Minimum required version is v%s. Please upgrade UniFi Protect and then retry" OUTDATED_LOG_MESSAGE = "You are running v%s of UniFi Protect. Minimum required version is v%s. Please upgrade UniFi Protect and then retry"
PLATFORMS = [Platform.BUTTON, Platform.CAMERA, Platform.MEDIA_PLAYER] PLATFORMS = [Platform.BUTTON, Platform.CAMERA, Platform.LIGHT, Platform.MEDIA_PLAYER]

View File

@ -0,0 +1,89 @@
"""This component provides Lights for UniFi Protect."""
from __future__ import annotations
import logging
from typing import Any
from pyunifiprotect.data import Light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up lights for UniFi Protect integration."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
entities = [
ProtectLight(
data,
device,
)
for device in data.api.bootstrap.lights.values()
]
if not entities:
return
async_add_entities(entities)
def unifi_brightness_to_hass(value: int) -> int:
"""Convert unifi brightness 1..6 to hass format 0..255."""
return min(255, round((value / 6) * 255))
def hass_to_unifi_brightness(value: int) -> int:
"""Convert hass brightness 0..255 to unifi 1..6 scale."""
return max(1, round((value / 255) * 6))
class ProtectLight(ProtectDeviceEntity, LightEntity):
"""A Ubiquiti UniFi Protect Light Entity."""
def __init__(
self,
data: ProtectData,
device: Light,
) -> None:
"""Initialize an UniFi light."""
self.device: Light = device
super().__init__(data)
self._attr_icon = "mdi:spotlight-beam"
self._attr_supported_features = SUPPORT_BRIGHTNESS
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
self._attr_is_on = self.device.is_light_on
self._attr_brightness = unifi_brightness_to_hass(
self.device.light_device_settings.led_level
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
hass_brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness)
unifi_brightness = hass_to_unifi_brightness(hass_brightness)
_LOGGER.debug("Turning on light with brightness %s", unifi_brightness)
await self.device.set_light(True, unifi_brightness)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
_LOGGER.debug("Turning off light")
await self.device.set_light(False)

View File

@ -10,8 +10,7 @@ from typing import Any, Callable
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
import pytest import pytest
from pyunifiprotect.data import Camera, Version from pyunifiprotect.data import Camera, Light, Version, WSSubscriptionMessage
from pyunifiprotect.data.websocket import WSSubscriptionMessage
from homeassistant.components.unifiprotect.const import DOMAIN, MIN_REQUIRED_PROTECT_V from homeassistant.components.unifiprotect.const import DOMAIN, MIN_REQUIRED_PROTECT_V
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -131,6 +130,17 @@ def mock_camera():
yield Camera.from_unifi_dict(**data) yield Camera.from_unifi_dict(**data)
@pytest.fixture
def mock_light():
"""Mock UniFi Protect Camera device."""
path = Path(__file__).parent / "sample_data" / "sample_light.json"
with open(path, encoding="utf-8") as json_file:
data = json.load(json_file)
yield Light.from_unifi_dict(**data)
async def time_changed(hass: HomeAssistant, seconds: int) -> None: async def time_changed(hass: HomeAssistant, seconds: int) -> None:
"""Trigger time changed.""" """Trigger time changed."""
next_update = dt_util.utcnow() + timedelta(seconds) next_update = dt_util.utcnow() + timedelta(seconds)

View File

@ -0,0 +1,53 @@
{
"mac": "D7F1C8D3FCDD",
"host": "192.168.10.86",
"connectionHost": "192.168.178.217",
"type": "UP FloodLight",
"name": "Byyfbpe Ufoka",
"upSince": 1638128991022,
"uptime": 1894890,
"lastSeen": 1640023881022,
"connectedSince": 1640020579711,
"state": "CONNECTED",
"hardwareRevision": null,
"firmwareVersion": "1.9.3",
"latestFirmwareVersion": "1.9.3",
"firmwareBuild": "g990c553.211105.251",
"isUpdating": false,
"isAdopting": false,
"isAdopted": true,
"isAdoptedByOther": false,
"isProvisioned": false,
"isRebooting": false,
"isSshEnabled": true,
"canAdopt": false,
"isAttemptingToConnect": false,
"isPirMotionDetected": false,
"lastMotion": 1640022006069,
"isDark": false,
"isLightOn": false,
"isLocating": false,
"wiredConnectionState": {
"phyRate": 100
},
"lightDeviceSettings": {
"isIndicatorEnabled": true,
"ledLevel": 6,
"luxSensitivity": "medium",
"pirDuration": 120000,
"pirSensitivity": 46
},
"lightOnSettings": {
"isLedForceOn": false
},
"lightModeSettings": {
"mode": "off",
"enableAt": "fulltime"
},
"camera": "193be66559c03ec5629f54cd",
"id": "37dd610720816cfb5c547967",
"isConnected": true,
"isCameraPaired": true,
"marketName": "UP FloodLight",
"modelKey": "light"
}

View File

@ -0,0 +1,141 @@
"""Test the UniFi Protect light platform."""
# pylint: disable=protected-access
from __future__ import annotations
from copy import copy
from unittest.mock import AsyncMock, Mock, patch
import pytest
from pyunifiprotect.data import Light
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_ENTITY_ID,
STATE_OFF,
STATE_ON,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import MockEntityFixture
@pytest.fixture(name="light")
async def light_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
):
"""Fixture for a single light with only the button platform active, no extra setup."""
# disable pydantic validation so mocking can happen
Light.__config__.validate_assignment = False
light_obj = mock_light.copy(deep=True)
light_obj._api = mock_entry.api
light_obj.name = "Test Light"
light_obj.is_light_on = False
mock_entry.api.bootstrap.lights = {
light_obj.id: light_obj,
}
with patch("homeassistant.components.unifiprotect.PLATFORMS", [Platform.LIGHT]):
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
assert len(hass.states.async_all()) == 1
assert len(entity_registry.entities) == 1
yield (light_obj, "light.test_light")
Light.__config__.validate_assignment = True
async def test_light_setup(
hass: HomeAssistant,
light: tuple[Light, str],
):
"""Test light entity setup."""
unique_id = light[0].id
entity_id = light[1]
entity_registry = er.async_get(hass)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.unique_id == unique_id
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_light_update(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
light: tuple[Light, str],
):
"""Test light entity update."""
new_bootstrap = copy(mock_entry.api.bootstrap)
new_light = light[0].copy()
new_light.is_light_on = True
new_light.light_device_settings.led_level = 3
mock_msg = Mock()
mock_msg.new_obj = new_light
new_bootstrap.lights = {new_light.id: new_light}
mock_entry.api.bootstrap = new_bootstrap
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done()
state = hass.states.get(light[1])
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_BRIGHTNESS] == 128
async def test_light_turn_on(
hass: HomeAssistant,
light: tuple[Light, str],
):
"""Test light entity turn off."""
entity_id = light[1]
light[0].__fields__["set_light"] = Mock()
light[0].set_light = AsyncMock()
await hass.services.async_call(
"light",
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128},
blocking=True,
)
light[0].set_light.assert_called_once_with(True, 3)
async def test_light_turn_off(
hass: HomeAssistant,
light: tuple[Light, str],
):
"""Test light entity turn on."""
entity_id = light[1]
light[0].__fields__["set_light"] = Mock()
light[0].set_light = AsyncMock()
await hass.services.async_call(
"light",
"turn_off",
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
light[0].set_light.assert_called_once_with(False)