Light control support to Axis devices (#36611)

* IR light support to Axis devices

* Change how to read light state

* Add tests

* Bump dependency to v32

* Assert variables passed to set_intensity
This commit is contained in:
Robert Svensson 2020-06-18 23:27:08 +02:00 committed by GitHub
parent e92e26b73a
commit 02e03340df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 301 additions and 8 deletions

View File

@ -42,7 +42,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add binary sensor from Axis device."""
event = device.api.event[event_id]
if event.CLASS != CLASS_OUTPUT:
if event.CLASS != CLASS_OUTPUT and not (
event.CLASS == CLASS_LIGHT and event.TYPE == "Light"
):
async_add_entities([AxisBinarySensor(event, device)], True)
device.listeners.append(

View File

@ -3,6 +3,7 @@ import logging
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
LOGGER = logging.getLogger(__package__)
@ -19,4 +20,4 @@ DEFAULT_EVENTS = True
DEFAULT_STREAM_PROFILE = "No stream profile"
DEFAULT_TRIGGER_TIME = 0
PLATFORMS = [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, SWITCH_DOMAIN]
PLATFORMS = [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, LIGHT_DOMAIN, SWITCH_DOMAIN]

View File

@ -0,0 +1,116 @@
"""Support for Axis lights."""
from axis.event_stream import CLASS_LIGHT
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS,
LightEntity,
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .axis_base import AxisEventBase
from .const import DOMAIN as AXIS_DOMAIN
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Axis light."""
device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
if not device.api.vapix.light_control:
return
@callback
def async_add_sensor(event_id):
"""Add light from Axis device."""
event = device.api.event[event_id]
if event.CLASS == CLASS_LIGHT and event.TYPE == "Light":
async_add_entities([AxisLight(event, device)], True)
device.listeners.append(
async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor)
)
class AxisLight(AxisEventBase, LightEntity):
"""Representation of a light Axis event."""
def __init__(self, event, device):
"""Initialize the Axis light."""
super().__init__(event, device)
self.light_id = f"led{self.event.id}"
self.current_intensity = 0
self.max_intensity = 0
self._features = SUPPORT_BRIGHTNESS
async def async_added_to_hass(self) -> None:
"""Subscribe lights events."""
await super().async_added_to_hass()
def get_light_capabilities():
"""Get light capabilities."""
current_intensity = self.device.api.vapix.light_control.get_current_intensity(
self.light_id
)
self.current_intensity = current_intensity["data"]["intensity"]
max_intensity = self.device.api.vapix.light_control.get_valid_intensity(
self.light_id
)
self.max_intensity = max_intensity["data"]["ranges"][0]["high"]
await self.hass.async_add_executor_job(get_light_capabilities)
@property
def supported_features(self):
"""Flag supported features."""
return self._features
@property
def name(self):
"""Return the name of the light."""
light_type = self.device.api.vapix.light_control[self.light_id].light_type
return f"{self.device.name} {light_type} {self.event.TYPE} {self.event.id}"
@property
def is_on(self):
"""Return true if light is on."""
return self.event.is_tripped
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return int((self.current_intensity / self.max_intensity) * 255)
def turn_on(self, **kwargs):
"""Turn on light."""
if not self.is_on:
self.device.api.vapix.light_control.activate_light(self.light_id)
if ATTR_BRIGHTNESS in kwargs:
intensity = int((kwargs[ATTR_BRIGHTNESS] / 255) * self.max_intensity)
self.device.api.vapix.light_control.set_manual_intensity(
self.light_id, intensity
)
def turn_off(self, **kwargs):
"""Turn off light."""
if self.is_on:
self.device.api.vapix.light_control.deactivate_light(self.light_id)
def update(self):
"""Update brightness."""
current_intensity = self.device.api.vapix.light_control.get_current_intensity(
self.light_id
)
self.current_intensity = current_intensity["data"]["intensity"]
@property
def should_poll(self):
"""Brightness needs polling."""
return True

View File

@ -3,7 +3,7 @@
"name": "Axis",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/axis",
"requirements": ["axis==31"],
"requirements": ["axis==32"],
"zeroconf": ["_axis-video._tcp.local."],
"after_dependencies": ["mqtt"],
"codeowners": ["@Kane610"]

View File

@ -306,7 +306,7 @@ avea==1.4
avri-api==0.1.7
# homeassistant.components.axis
axis==31
axis==32
# homeassistant.components.azure_event_hub
azure-eventhub==5.1.0

View File

@ -150,7 +150,7 @@ av==8.0.2
avri-api==0.1.7
# homeassistant.components.axis
axis==31
axis==32
# homeassistant.components.homekit
base36==0.1.1

View File

@ -7,6 +7,7 @@ import axis as axislib
from axis.api_discovery import URL as API_DISCOVERY_URL
from axis.basic_device_info import URL as BASIC_DEVICE_INFO_URL
from axis.event_stream import OPERATION_INITIALIZED
from axis.light_control import URL as LIGHT_CONTROL_URL
from axis.mqtt import URL_CLIENT as MQTT_CLIENT_URL
from axis.param_cgi import (
BRAND as BRAND_URL,
@ -82,7 +83,6 @@ API_DISCOVERY_PORT_MANAGEMENT = {
"name": "IO Port Management",
}
BASIC_DEVICE_INFO_RESPONSE = {
"apiVersion": "1.1",
"data": {
@ -95,6 +95,27 @@ BASIC_DEVICE_INFO_RESPONSE = {
},
}
LIGHT_CONTROL_RESPONSE = {
"apiVersion": "1.1",
"method": "getLightInformation",
"data": {
"items": [
{
"lightID": "led0",
"lightType": "IR",
"enabled": True,
"synchronizeDayNightMode": True,
"lightState": False,
"automaticIntensityMode": False,
"automaticAngleOfIlluminationMode": False,
"nrOfLEDs": 1,
"error": False,
"errorInfo": "",
}
]
},
}
MQTT_CLIENT_RESPONSE = {
"apiVersion": "1.0",
"context": "some context",
@ -167,6 +188,8 @@ def vapix_session_request(session, url, **kwargs):
return json.dumps(API_DISCOVERY_RESPONSE)
if BASIC_DEVICE_INFO_URL in url:
return json.dumps(BASIC_DEVICE_INFO_RESPONSE)
if LIGHT_CONTROL_URL in url:
return json.dumps(LIGHT_CONTROL_RESPONSE)
if MQTT_CLIENT_URL in url:
return json.dumps(MQTT_CLIENT_RESPONSE)
if PORT_MANAGEMENT_URL in url:
@ -217,10 +240,11 @@ async def test_device_setup(hass):
entry = device.config_entry
assert len(forward_entry_setup.mock_calls) == 3
assert len(forward_entry_setup.mock_calls) == 4
assert forward_entry_setup.mock_calls[0][1] == (entry, "binary_sensor")
assert forward_entry_setup.mock_calls[1][1] == (entry, "camera")
assert forward_entry_setup.mock_calls[2][1] == (entry, "switch")
assert forward_entry_setup.mock_calls[2][1] == (entry, "light")
assert forward_entry_setup.mock_calls[3][1] == (entry, "switch")
assert device.host == ENTRY_CONFIG[CONF_HOST]
assert device.model == ENTRY_CONFIG[CONF_MODEL]

View File

@ -0,0 +1,150 @@
"""Axis light platform tests."""
from copy import deepcopy
from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
from homeassistant.setup import async_setup_component
from .test_device import API_DISCOVERY_RESPONSE, NAME, setup_axis_integration
from tests.async_mock import patch
API_DISCOVERY_LIGHT_CONTROL = {
"id": "light-control",
"version": "1.1",
"name": "Light Control",
}
EVENT_ON = {
"operation": "Initialized",
"topic": "tns1:Device/tnsaxis:Light/Status",
"source": "id",
"source_idx": "0",
"type": "state",
"value": "ON",
}
EVENT_OFF = {
"operation": "Initialized",
"topic": "tns1:Device/tnsaxis:Light/Status",
"source": "id",
"source_idx": "0",
"type": "state",
"value": "OFF",
}
async def test_platform_manually_configured(hass):
"""Test that nothing happens when platform is manually configured."""
assert await async_setup_component(
hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": AXIS_DOMAIN}}
)
assert AXIS_DOMAIN not in hass.data
async def test_no_lights(hass):
"""Test that no light events in Axis results in no light entities."""
await setup_axis_integration(hass)
assert not hass.states.async_entity_ids(LIGHT_DOMAIN)
async def test_lights(hass):
"""Test that lights are loaded properly."""
api_discovery = deepcopy(API_DISCOVERY_RESPONSE)
api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL)
with patch.dict(API_DISCOVERY_RESPONSE, api_discovery):
device = await setup_axis_integration(hass)
# Add light
with patch(
"axis.light_control.LightControl.get_current_intensity",
return_value={"data": {"intensity": 100}},
), patch(
"axis.light_control.LightControl.get_valid_intensity",
return_value={"data": {"ranges": [{"high": 150}]}},
):
device.api.event.process_event(EVENT_ON)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1
light_0 = hass.states.get(f"light.{NAME}_ir_light_0")
assert light_0.state == "on"
assert light_0.name == f"{NAME} IR Light 0"
# Turn on, set brightness, light already on
with patch(
"axis.light_control.LightControl.activate_light"
) as mock_activate, patch(
"axis.light_control.LightControl.set_manual_intensity"
) as mock_set_intensity, patch(
"axis.light_control.LightControl.get_current_intensity",
return_value={"data": {"intensity": 100}},
):
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": f"light.{NAME}_ir_light_0", ATTR_BRIGHTNESS: 50},
blocking=True,
)
mock_activate.not_called()
mock_set_intensity.assert_called_once_with("led0", 29)
# Turn off
with patch(
"axis.light_control.LightControl.deactivate_light"
) as mock_deactivate, patch(
"axis.light_control.LightControl.get_current_intensity",
return_value={"data": {"intensity": 100}},
):
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{"entity_id": f"light.{NAME}_ir_light_0"},
blocking=True,
)
mock_deactivate.assert_called_once()
# Event turn off light
device.api.event.process_event(EVENT_OFF)
await hass.async_block_till_done()
light_0 = hass.states.get(f"light.{NAME}_ir_light_0")
assert light_0.state == "off"
# Turn on, set brightness
with patch(
"axis.light_control.LightControl.activate_light"
) as mock_activate, patch(
"axis.light_control.LightControl.set_manual_intensity"
) as mock_set_intensity, patch(
"axis.light_control.LightControl.get_current_intensity",
return_value={"data": {"intensity": 100}},
):
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": f"light.{NAME}_ir_light_0"},
blocking=True,
)
mock_activate.assert_called_once()
mock_set_intensity.assert_not_called()
# Turn off, light already off
with patch(
"axis.light_control.LightControl.deactivate_light"
) as mock_deactivate, patch(
"axis.light_control.LightControl.get_current_intensity",
return_value={"data": {"intensity": 100}},
):
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{"entity_id": f"light.{NAME}_ir_light_0"},
blocking=True,
)
mock_deactivate.assert_not_called()