Follow Axis library changes and improve tests (#44126)

This commit is contained in:
Robert Svensson 2021-01-13 14:03:54 +01:00 committed by GitHub
parent ff3a1f2050
commit 6325bc8bfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 149 additions and 78 deletions

View File

@ -7,10 +7,12 @@ from axis.event_stream import (
CLASS_LIGHT, CLASS_LIGHT,
CLASS_MOTION, CLASS_MOTION,
CLASS_OUTPUT, CLASS_OUTPUT,
CLASS_PTZ,
CLASS_SOUND, CLASS_SOUND,
FenceGuard, FenceGuard,
LoiteringGuard, LoiteringGuard,
MotionGuard, MotionGuard,
ObjectAnalytics,
Vmd4, Vmd4,
) )
@ -46,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add binary sensor from Axis device.""" """Add binary sensor from Axis device."""
event = device.api.event[event_id] event = device.api.event[event_id]
if event.CLASS != CLASS_OUTPUT and not ( if event.CLASS not in (CLASS_OUTPUT, CLASS_PTZ) and not (
event.CLASS == CLASS_LIGHT and event.TYPE == "Light" event.CLASS == CLASS_LIGHT and event.TYPE == "Light"
): ):
async_add_entities([AxisBinarySensor(event, device)]) async_add_entities([AxisBinarySensor(event, device)])
@ -101,7 +103,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity):
"""Return the name of the event.""" """Return the name of the event."""
if ( if (
self.event.CLASS == CLASS_INPUT self.event.CLASS == CLASS_INPUT
and self.event.id and self.event.id in self.device.api.vapix.ports
and self.device.api.vapix.ports[self.event.id].name and self.device.api.vapix.ports[self.event.id].name
): ):
return ( return (
@ -114,6 +116,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity):
(FenceGuard, self.device.api.vapix.fence_guard), (FenceGuard, self.device.api.vapix.fence_guard),
(LoiteringGuard, self.device.api.vapix.loitering_guard), (LoiteringGuard, self.device.api.vapix.loitering_guard),
(MotionGuard, self.device.api.vapix.motion_guard), (MotionGuard, self.device.api.vapix.motion_guard),
(ObjectAnalytics, self.device.api.vapix.object_analytics),
(Vmd4, self.device.api.vapix.vmd4), (Vmd4, self.device.api.vapix.vmd4),
): ):
if ( if (

View File

@ -25,6 +25,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.setup import async_when_setup from homeassistant.setup import async_when_setup
from .const import ( from .const import (
@ -177,7 +178,7 @@ class AxisNetworkDevice:
self.disconnect_from_stream() self.disconnect_from_stream()
event = mqtt_json_to_event(message.payload) event = mqtt_json_to_event(message.payload)
self.api.event.process_event(event) self.api.event.update([event])
# Setup and teardown methods # Setup and teardown methods
@ -195,8 +196,10 @@ class AxisNetworkDevice:
except CannotConnect as err: except CannotConnect as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
except Exception: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
LOGGER.error("Unknown error connecting with Axis device on %s", self.host) LOGGER.error(
"Unknown error connecting with Axis device (%s): %s", self.host, err
)
return False return False
self.fw_version = self.api.vapix.firmware_version self.fw_version = self.api.vapix.firmware_version
@ -239,12 +242,10 @@ class AxisNetworkDevice:
async def shutdown(self, event): async def shutdown(self, event):
"""Stop the event stream.""" """Stop the event stream."""
self.disconnect_from_stream() self.disconnect_from_stream()
await self.api.vapix.close()
async def async_reset(self): async def async_reset(self):
"""Reset this device to default state.""" """Reset this device to default state."""
self.disconnect_from_stream() self.disconnect_from_stream()
await self.api.vapix.close()
unload_ok = all( unload_ok = all(
await asyncio.gather( await asyncio.gather(
@ -267,9 +268,10 @@ class AxisNetworkDevice:
async def get_device(hass, host, port, username, password): async def get_device(hass, host, port, username, password):
"""Create a Axis device.""" """Create a Axis device."""
session = get_async_client(hass, verify_ssl=False)
device = axis.AxisDevice( device = axis.AxisDevice(
Configuration(host, port=port, username=username, password=password) Configuration(session, host, port=port, username=username, password=password)
) )
try: try:
@ -280,15 +282,12 @@ async def get_device(hass, host, port, username, password):
except axis.Unauthorized as err: except axis.Unauthorized as err:
LOGGER.warning("Connected to device at %s but not registered.", host) LOGGER.warning("Connected to device at %s but not registered.", host)
await device.vapix.close()
raise AuthenticationRequired from err raise AuthenticationRequired from err
except (asyncio.TimeoutError, axis.RequestError) as err: except (asyncio.TimeoutError, axis.RequestError) as err:
LOGGER.error("Error connecting to the Axis device at %s", host) LOGGER.error("Error connecting to the Axis device at %s", host)
await device.vapix.close()
raise CannotConnect from err raise CannotConnect from err
except axis.AxisException as err: except axis.AxisException as err:
LOGGER.exception("Unknown Axis communication error occurred") LOGGER.exception("Unknown Axis communication error occurred")
await device.vapix.close()
raise AuthenticationRequired from err raise AuthenticationRequired from err

View File

@ -3,7 +3,7 @@
"name": "Axis", "name": "Axis",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/axis", "documentation": "https://www.home-assistant.io/integrations/axis",
"requirements": ["axis==41"], "requirements": ["axis==42"],
"zeroconf": [ "zeroconf": [
{ "type": "_axis-video._tcp.local.", "macaddress": "00408C*" }, { "type": "_axis-video._tcp.local.", "macaddress": "00408C*" },
{ "type": "_axis-video._tcp.local.", "macaddress": "ACCC8E*" }, { "type": "_axis-video._tcp.local.", "macaddress": "ACCC8E*" },

View File

@ -306,7 +306,7 @@ av==8.0.2
# avion==0.10 # avion==0.10
# homeassistant.components.axis # homeassistant.components.axis
axis==41 axis==42
# homeassistant.components.azure_event_hub # homeassistant.components.azure_event_hub
azure-eventhub==5.1.0 azure-eventhub==5.1.0

View File

@ -177,7 +177,7 @@ auroranoaa==0.0.2
av==8.0.2 av==8.0.2
# homeassistant.components.axis # homeassistant.components.axis
axis==41 axis==42
# homeassistant.components.azure_event_hub # homeassistant.components.azure_event_hub
azure-eventhub==5.1.0 azure-eventhub==5.1.0

View File

@ -19,6 +19,14 @@ EVENTS = [
"type": "state", "type": "state",
"value": "0", "value": "0",
}, },
{
"operation": "Initialized",
"topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1",
"source": "PresetToken",
"source_idx": "0",
"type": "on_preset",
"value": "1",
},
{ {
"operation": "Initialized", "operation": "Initialized",
"topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1",
@ -54,8 +62,7 @@ async def test_binary_sensors(hass):
config_entry = await setup_axis_integration(hass) config_entry = await setup_axis_integration(hass)
device = hass.data[AXIS_DOMAIN][config_entry.unique_id] device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
for event in EVENTS: device.api.event.update(EVENTS)
device.api.event.process_event(event)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2

View File

@ -1,6 +1,8 @@
"""Test Axis config flow.""" """Test Axis config flow."""
from unittest.mock import patch from unittest.mock import patch
import respx
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.axis import config_flow from homeassistant.components.axis import config_flow
from homeassistant.components.axis.const import ( from homeassistant.components.axis.const import (
@ -25,7 +27,13 @@ from homeassistant.data_entry_flow import (
RESULT_TYPE_FORM, RESULT_TYPE_FORM,
) )
from .test_device import MAC, MODEL, NAME, setup_axis_integration, vapix_request from .test_device import (
MAC,
MODEL,
NAME,
mock_default_vapix_requests,
setup_axis_integration,
)
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -41,7 +49,8 @@ async def test_flow_manual_configuration(hass):
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
with patch("axis.vapix.Vapix.request", new=vapix_request): with respx.mock:
mock_default_vapix_requests(respx)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
@ -80,7 +89,8 @@ async def test_manual_configuration_update_configuration(hass):
with patch( with patch(
"homeassistant.components.axis.async_setup_entry", "homeassistant.components.axis.async_setup_entry",
return_value=True, return_value=True,
) as mock_setup_entry, patch("axis.vapix.Vapix.request", new=vapix_request): ) as mock_setup_entry, respx.mock:
mock_default_vapix_requests(respx, "2.3.4.5")
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
@ -109,7 +119,8 @@ async def test_flow_fails_already_configured(hass):
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
with patch("axis.vapix.Vapix.request", new=vapix_request): with respx.mock:
mock_default_vapix_requests(respx)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
@ -196,7 +207,8 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass):
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
with patch("axis.vapix.Vapix.request", new=vapix_request): with respx.mock:
mock_default_vapix_requests(respx)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
@ -238,7 +250,8 @@ async def test_zeroconf_flow(hass):
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
with patch("axis.vapix.Vapix.request", new=vapix_request): with respx.mock:
mock_default_vapix_requests(respx)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
@ -304,7 +317,8 @@ async def test_zeroconf_flow_updated_configuration(hass):
with patch( with patch(
"homeassistant.components.axis.async_setup_entry", "homeassistant.components.axis.async_setup_entry",
return_value=True, return_value=True,
) as mock_setup_entry, patch("axis.vapix.Vapix.request", new=vapix_request): ) as mock_setup_entry, respx.mock:
mock_default_vapix_requests(respx, "2.3.4.5")
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
AXIS_DOMAIN, AXIS_DOMAIN,
data={ data={

View File

@ -1,26 +1,12 @@
"""Test Axis device.""" """Test Axis device."""
from copy import deepcopy from copy import deepcopy
from unittest import mock from unittest import mock
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import Mock, patch
import axis as axislib import axis as axislib
from axis.api_discovery import URL as API_DISCOVERY_URL
from axis.applications import URL_LIST as APPLICATIONS_URL
from axis.applications.vmd4 import URL as VMD4_URL
from axis.basic_device_info import URL as BASIC_DEVICE_INFO_URL
from axis.event_stream import OPERATION_INITIALIZED 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,
INPUT as INPUT_URL,
IOPORT as IOPORT_URL,
OUTPUT as OUTPUT_URL,
PROPERTIES as PROPERTIES_URL,
STREAM_PROFILES as STREAM_PROFILES_URL,
)
from axis.port_management import URL as PORT_MANAGEMENT_URL
import pytest import pytest
import respx
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import axis from homeassistant.components import axis
@ -47,10 +33,12 @@ MAC = "00408C12345"
MODEL = "model" MODEL = "model"
NAME = "name" NAME = "name"
DEFAULT_HOST = "1.2.3.4"
ENTRY_OPTIONS = {CONF_EVENTS: True} ENTRY_OPTIONS = {CONF_EVENTS: True}
ENTRY_CONFIG = { ENTRY_CONFIG = {
CONF_HOST: "1.2.3.4", CONF_HOST: DEFAULT_HOST,
CONF_USERNAME: "root", CONF_USERNAME: "root",
CONF_PASSWORD: "pass", CONF_PASSWORD: "pass",
CONF_PORT: 80, CONF_PORT: 80,
@ -166,6 +154,14 @@ root.Brand.ProdVariant=
root.Brand.WebURL=http://www.axis.com root.Brand.WebURL=http://www.axis.com
""" """
IMAGE_RESPONSE = """root.Image.I0.Enabled=yes
root.Image.I0.Name=View Area 1
root.Image.I0.Source=0
root.Image.I1.Enabled=no
root.Image.I1.Name=View Area 2
root.Image.I1.Source=0
"""
PORTS_RESPONSE = """root.Input.NbrOfInputs=1 PORTS_RESPONSE = """root.Input.NbrOfInputs=1
root.IOPort.I0.Configurable=no root.IOPort.I0.Configurable=no
root.IOPort.I0.Direction=input root.IOPort.I0.Direction=input
@ -188,6 +184,9 @@ root.Properties.Image.Rotation=0,180
root.Properties.System.SerialNumber=00408C12345 root.Properties.System.SerialNumber=00408C12345
""" """
PTZ_RESPONSE = ""
STREAM_PROFILES_RESPONSE = """root.StreamProfile.MaxGroups=26 STREAM_PROFILES_RESPONSE = """root.StreamProfile.MaxGroups=26
root.StreamProfile.S0.Description=profile_1_description root.StreamProfile.S0.Description=profile_1_description
root.StreamProfile.S0.Name=profile_1 root.StreamProfile.S0.Name=profile_1
@ -197,31 +196,85 @@ root.StreamProfile.S1.Name=profile_2
root.StreamProfile.S1.Parameters=videocodec=h265 root.StreamProfile.S1.Parameters=videocodec=h265
""" """
VIEW_AREAS_RESPONSE = {"apiVersion": "1.0", "method": "list", "data": {"viewAreas": []}}
async def vapix_request(self, session, url, **kwargs):
"""Return data based on url.""" def mock_default_vapix_requests(respx: respx, host: str = DEFAULT_HOST) -> None:
if API_DISCOVERY_URL in url: """Mock default Vapix requests responses."""
return API_DISCOVERY_RESPONSE respx.post(f"http://{host}:80/axis-cgi/apidiscovery.cgi").respond(
if APPLICATIONS_URL in url: json=API_DISCOVERY_RESPONSE,
return APPLICATIONS_LIST_RESPONSE )
if BASIC_DEVICE_INFO_URL in url: respx.post(f"http://{host}:80/axis-cgi/basicdeviceinfo.cgi").respond(
return BASIC_DEVICE_INFO_RESPONSE json=BASIC_DEVICE_INFO_RESPONSE,
if LIGHT_CONTROL_URL in url: )
return LIGHT_CONTROL_RESPONSE respx.post(f"http://{host}:80/axis-cgi/io/portmanagement.cgi").respond(
if MQTT_CLIENT_URL in url: json=PORT_MANAGEMENT_RESPONSE,
return MQTT_CLIENT_RESPONSE )
if PORT_MANAGEMENT_URL in url: respx.post(f"http://{host}:80/axis-cgi/lightcontrol.cgi").respond(
return PORT_MANAGEMENT_RESPONSE json=LIGHT_CONTROL_RESPONSE,
if VMD4_URL in url: )
return VMD4_RESPONSE respx.post(f"http://{host}:80/axis-cgi/mqtt/client.cgi").respond(
if BRAND_URL in url: json=MQTT_CLIENT_RESPONSE,
return BRAND_RESPONSE )
if IOPORT_URL in url or INPUT_URL in url or OUTPUT_URL in url: respx.post(f"http://{host}:80/axis-cgi/streamprofile.cgi").respond(
return PORTS_RESPONSE json=STREAM_PROFILES_RESPONSE,
if PROPERTIES_URL in url: )
return PROPERTIES_RESPONSE respx.post(f"http://{host}:80/axis-cgi/viewarea/info.cgi").respond(
if STREAM_PROFILES_URL in url: json=VIEW_AREAS_RESPONSE
return STREAM_PROFILES_RESPONSE )
respx.get(
f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Brand"
).respond(
text=BRAND_RESPONSE,
headers={"Content-Type": "text/plain"},
)
respx.get(
f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Image"
).respond(
text=IMAGE_RESPONSE,
headers={"Content-Type": "text/plain"},
)
respx.get(
f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Input"
).respond(
text=PORTS_RESPONSE,
headers={"Content-Type": "text/plain"},
)
respx.get(
f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.IOPort"
).respond(
text=PORTS_RESPONSE,
headers={"Content-Type": "text/plain"},
)
respx.get(
f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Output"
).respond(
text=PORTS_RESPONSE,
headers={"Content-Type": "text/plain"},
)
respx.get(
f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Properties"
).respond(
text=PROPERTIES_RESPONSE,
headers={"Content-Type": "text/plain"},
)
respx.get(
f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.PTZ"
).respond(
text=PTZ_RESPONSE,
headers={"Content-Type": "text/plain"},
)
respx.get(
f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.StreamProfile"
).respond(
text=STREAM_PROFILES_RESPONSE,
headers={"Content-Type": "text/plain"},
)
respx.post(f"http://{host}:80/axis-cgi/applications/list.cgi").respond(
text=APPLICATIONS_LIST_RESPONSE,
headers={"Content-Type": "text/xml"},
)
respx.post(f"http://{host}:80/local/vmd/control.cgi").respond(json=VMD4_RESPONSE)
async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS): async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS):
@ -235,10 +288,8 @@ async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTION
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with patch("axis.vapix.Vapix.request", new=vapix_request), patch( with patch("axis.rtsp.RTSPClient.start", return_value=True), respx.mock:
"axis.rtsp.RTSPClient.start", mock_default_vapix_requests(respx)
return_value=True,
):
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -317,10 +368,11 @@ async def test_update_address(hass):
device = hass.data[AXIS_DOMAIN][config_entry.unique_id] device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
assert device.api.config.host == "1.2.3.4" assert device.api.config.host == "1.2.3.4"
with patch("axis.vapix.Vapix.request", new=vapix_request), patch( with patch(
"homeassistant.components.axis.async_setup_entry", "homeassistant.components.axis.async_setup_entry",
return_value=True, return_value=True,
) as mock_setup_entry: ) as mock_setup_entry, respx.mock:
mock_default_vapix_requests(respx, "2.3.4.5")
await hass.config_entries.flow.async_init( await hass.config_entries.flow.async_init(
AXIS_DOMAIN, AXIS_DOMAIN,
data={ data={
@ -390,12 +442,10 @@ async def test_shutdown():
axis_device = axis.device.AxisNetworkDevice(hass, entry) axis_device = axis.device.AxisNetworkDevice(hass, entry)
axis_device.api = Mock() axis_device.api = Mock()
axis_device.api.vapix.close = AsyncMock()
await axis_device.shutdown(None) await axis_device.shutdown(None)
assert len(axis_device.api.stream.stop.mock_calls) == 1 assert len(axis_device.api.stream.stop.mock_calls) == 1
assert len(axis_device.api.vapix.close.mock_calls) == 1
async def test_get_device_fails(hass): async def test_get_device_fails(hass):

View File

@ -74,7 +74,7 @@ async def test_lights(hass):
"axis.light_control.LightControl.get_valid_intensity", "axis.light_control.LightControl.get_valid_intensity",
return_value={"data": {"ranges": [{"high": 150}]}}, return_value={"data": {"ranges": [{"high": 150}]}},
): ):
device.api.event.process_event(EVENT_ON) device.api.event.update([EVENT_ON])
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1 assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1
@ -119,7 +119,7 @@ async def test_lights(hass):
mock_deactivate.assert_called_once() mock_deactivate.assert_called_once()
# Event turn off light # Event turn off light
device.api.event.process_event(EVENT_OFF) device.api.event.update([EVENT_OFF])
await hass.async_block_till_done() await hass.async_block_till_done()
light_0 = hass.states.get(entity_id) light_0 = hass.states.get(entity_id)

View File

@ -68,8 +68,7 @@ async def test_switches_with_port_cgi(hass):
device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["0"].close = AsyncMock()
device.api.vapix.ports["1"].name = "" device.api.vapix.ports["1"].name = ""
for event in EVENTS: device.api.event.update(EVENTS)
device.api.event.process_event(event)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
@ -116,8 +115,7 @@ async def test_switches_with_port_management(hass):
device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["0"].close = AsyncMock()
device.api.vapix.ports["1"].name = "" device.api.vapix.ports["1"].name = ""
for event in EVENTS: device.api.event.update(EVENTS)
device.api.event.process_event(event)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2