mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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:
parent
e92e26b73a
commit
02e03340df
@ -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(
|
||||
|
@ -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]
|
||||
|
116
homeassistant/components/axis/light.py
Normal file
116
homeassistant/components/axis/light.py
Normal 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
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
150
tests/components/axis/test_light.py
Normal file
150
tests/components/axis/test_light.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user