diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 4b9cb7d20cf..eedeac01366 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry): # 0.104 introduced config entry unique id, this makes upgrading possible if config_entry.unique_id is None: hass.config_entries.async_update_entry( - config_entry, unique_id=device.api.vapix.params.system_serialnumber + config_entry, unique_id=device.api.vapix.serial_number ) hass.data[AXIS_DOMAIN][config_entry.unique_id] = device diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 37141d6017a..508bee6aff5 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -61,8 +61,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): password=user_input[CONF_PASSWORD], ) - serial_number = device.vapix.params.system_serialnumber - await self.async_set_unique_id(serial_number) + await self.async_set_unique_id(device.vapix.serial_number) self._abort_if_unique_id_configured( updates={ @@ -76,8 +75,8 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_PORT: user_input[CONF_PORT], CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_MAC: serial_number, - CONF_MODEL: device.vapix.params.prodnbr, + CONF_MAC: device.vapix.serial_number, + CONF_MODEL: device.vapix.product_number, } return await self._create_entry() diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index cc327cb3b65..54e534068a8 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -4,6 +4,7 @@ import asyncio import async_timeout import axis +from axis.configuration import Configuration from axis.event_stream import OPERATION_INITIALIZED from axis.mqtt import mqtt_json_to_event from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED @@ -185,8 +186,8 @@ class AxisNetworkDevice: LOGGER.error("Unknown error connecting with Axis device on %s", self.host) return False - self.fw_version = self.api.vapix.params.firmware_version - self.product_type = self.api.vapix.params.prodtype + self.fw_version = self.api.vapix.firmware_version + self.product_type = self.api.vapix.product_type async def start_platforms(): await asyncio.gather( @@ -254,22 +255,12 @@ async def get_device(hass, host, port, username, password): """Create a Axis device.""" device = axis.AxisDevice( - host=host, port=port, username=username, password=password, web_proto="http", + Configuration(host, port=port, username=username, password=password) ) - device.vapix.initialize_params(preload_data=False) - device.vapix.initialize_ports() - try: with async_timeout.timeout(15): - - for vapix_call in ( - device.vapix.initialize_api_discovery, - device.vapix.params.update_brand, - device.vapix.params.update_properties, - device.vapix.ports.update, - ): - await hass.async_add_executor_job(vapix_call) + await hass.async_add_executor_job(device.vapix.initialize) return device diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index dd33cdbf1ea..417e1781c7c 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==27"], + "requirements": ["axis==28"], "zeroconf": ["_axis-video._tcp.local."], "after_dependencies": ["mqtt"], "codeowners": ["@Kane610"] diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index db91115484f..256a22db114 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -1,7 +1,6 @@ """Support for Axis switches.""" from axis.event_stream import CLASS_OUTPUT -from axis.port_cgi import ACTION_HIGH, ACTION_LOW from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback @@ -39,13 +38,13 @@ class AxisSwitch(AxisEventBase, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn on switch.""" await self.hass.async_add_executor_job( - self.device.api.vapix.ports[self.event.id].action, ACTION_HIGH + self.device.api.vapix.ports[self.event.id].close ) async def async_turn_off(self, **kwargs): """Turn off switch.""" await self.hass.async_add_executor_job( - self.device.api.vapix.ports[self.event.id].action, ACTION_LOW + self.device.api.vapix.ports[self.event.id].open ) @property diff --git a/requirements_all.txt b/requirements_all.txt index 62ed538235a..98759a6a077 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -306,7 +306,7 @@ avea==1.4 avri-api==0.1.7 # homeassistant.components.axis -axis==27 +axis==28 # homeassistant.components.azure_event_hub azure-eventhub==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da7a7f4fe58..44b069ab2ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -147,7 +147,7 @@ async-upnp-client==0.14.13 av==8.0.1 # homeassistant.components.axis -axis==27 +axis==28 # homeassistant.components.homekit base36==0.1.1 diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 7cc0e3e535b..ab3516873fa 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,5 +1,4 @@ """Test Axis config flow.""" -from homeassistant.components import axis from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import CONF_MODEL, DOMAIN as AXIS_DOMAIN from homeassistant.const import ( @@ -11,30 +10,12 @@ from homeassistant.const import ( CONF_USERNAME, ) -from .test_device import MAC, MODEL, NAME, setup_axis_integration +from .test_device import MAC, MODEL, NAME, setup_axis_integration, vapix_session_request -from tests.async_mock import Mock, patch +from tests.async_mock import patch from tests.common import MockConfigEntry -def setup_mock_axis_device(mock_device): - """Prepare mock axis device.""" - - def mock_constructor(host, username, password, port, web_proto): - """Fake the controller constructor.""" - mock_device.host = host - mock_device.username = username - mock_device.password = password - mock_device.port = port - return mock_device - - mock_device.side_effect = mock_constructor - mock_device.vapix.params.system_serialnumber = MAC - mock_device.vapix.params.prodnbr = "prodnbr" - mock_device.vapix.params.prodtype = "prodtype" - mock_device.vapix.params.firmware_version = "firmware_version" - - async def test_flow_manual_configuration(hass): """Test that config flow works.""" result = await hass.config_entries.flow.async_init( @@ -44,10 +25,7 @@ async def test_flow_manual_configuration(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch("axis.AxisDevice") as mock_device: - - setup_mock_axis_device(mock_device) - + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -59,15 +37,15 @@ async def test_flow_manual_configuration(hass): ) assert result["type"] == "create_entry" - assert result["title"] == f"prodnbr - {MAC}" + assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", CONF_PORT: 80, CONF_MAC: MAC, - CONF_MODEL: "prodnbr", - CONF_NAME: "prodnbr 0", + CONF_MODEL: "M1065-LW", + CONF_NAME: "M1065-LW 0", } @@ -82,13 +60,7 @@ async def test_manual_configuration_update_configuration(hass): assert result["type"] == "form" assert result["step_id"] == "user" - mock_device = Mock() - mock_device.vapix.params.system_serialnumber = MAC - - with patch( - "homeassistant.components.axis.config_flow.get_device", - return_value=mock_device, - ): + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -115,13 +87,7 @@ async def test_flow_fails_already_configured(hass): assert result["type"] == "form" assert result["step_id"] == "user" - mock_device = Mock() - mock_device.vapix.params.system_serialnumber = MAC - - with patch( - "homeassistant.components.axis.config_flow.get_device", - return_value=mock_device, - ): + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -191,11 +157,11 @@ async def test_flow_fails_device_unavailable(hass): async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): """Test that create entry can generate a name with other entries.""" entry = MockConfigEntry( - domain=AXIS_DOMAIN, data={CONF_NAME: "prodnbr 0", CONF_MODEL: "prodnbr"}, + domain=AXIS_DOMAIN, data={CONF_NAME: "M1065-LW 0", CONF_MODEL: "M1065-LW"}, ) entry.add_to_hass(hass) entry2 = MockConfigEntry( - domain=AXIS_DOMAIN, data={CONF_NAME: "prodnbr 1", CONF_MODEL: "prodnbr"}, + domain=AXIS_DOMAIN, data={CONF_NAME: "M1065-LW 1", CONF_MODEL: "M1065-LW"}, ) entry2.add_to_hass(hass) @@ -206,10 +172,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch("axis.AxisDevice") as mock_device: - - setup_mock_axis_device(mock_device) - + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -221,41 +184,37 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): ) assert result["type"] == "create_entry" - assert result["title"] == f"prodnbr - {MAC}" + assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", CONF_PORT: 80, CONF_MAC: MAC, - CONF_MODEL: "prodnbr", - CONF_NAME: "prodnbr 2", + CONF_MODEL: "M1065-LW", + CONF_NAME: "M1065-LW 2", } - assert result["data"][CONF_NAME] == "prodnbr 2" + assert result["data"][CONF_NAME] == "M1065-LW 2" async def test_zeroconf_flow(hass): """Test that zeroconf discovery for new devices work.""" - with patch.object(axis.device, "get_device", return_value=Mock()): - result = await hass.config_entries.flow.async_init( - AXIS_DOMAIN, - data={ - CONF_HOST: "1.2.3.4", - CONF_PORT: 80, - "hostname": "name", - "properties": {"macaddress": MAC}, - }, - context={"source": "zeroconf"}, - ) + result = await hass.config_entries.flow.async_init( + AXIS_DOMAIN, + data={ + CONF_HOST: "1.2.3.4", + CONF_PORT: 80, + "hostname": "name", + "properties": {"macaddress": MAC}, + }, + context={"source": "zeroconf"}, + ) assert result["type"] == "form" assert result["step_id"] == "user" - with patch("axis.AxisDevice") as mock_device: - - setup_mock_axis_device(mock_device) - + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -267,18 +226,18 @@ async def test_zeroconf_flow(hass): ) assert result["type"] == "create_entry" - assert result["title"] == f"prodnbr - {MAC}" + assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", CONF_PORT: 80, CONF_MAC: MAC, - CONF_MODEL: "prodnbr", - CONF_NAME: "prodnbr 0", + CONF_MODEL: "M1065-LW", + CONF_NAME: "M1065-LW 0", } - assert result["data"][CONF_NAME] == "prodnbr 0" + assert result["data"][CONF_NAME] == "M1065-LW 0" async def test_zeroconf_flow_already_configured(hass): diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index a61176d84a7..c96d13659d1 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,9 +1,22 @@ """Test Axis device.""" from copy import deepcopy +import json from unittest import mock 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.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 from homeassistant import config_entries @@ -47,7 +60,7 @@ ENTRY_CONFIG = { CONF_NAME: NAME, } -DEFAULT_API_DISCOVERY = { +API_DISCOVERY_RESPONSE = { "method": "getApiList", "apiVersion": "1.0", "data": { @@ -58,7 +71,58 @@ DEFAULT_API_DISCOVERY = { }, } -DEFAULT_BRAND = """root.Brand.Brand=AXIS +API_DISCOVERY_BASIC_DEVICE_INFO = { + "id": "basic-device-info", + "version": "1.1", + "name": "Basic Device Information", +} +API_DISCOVERY_MQTT = {"id": "mqtt-client", "version": "1.0", "name": "MQTT Client API"} +API_DISCOVERY_PORT_MANAGEMENT = { + "id": "io-port-management", + "version": "1.0", + "name": "IO Port Management", +} + + +BASIC_DEVICE_INFO_RESPONSE = { + "apiVersion": "1.1", + "data": { + "propertyList": { + "ProdNbr": "M1065-LW", + "ProdType": "Network Camera", + "SerialNumber": "00408C12345", + "Version": "9.80.1", + } + }, +} + +MQTT_CLIENT_RESPONSE = { + "apiVersion": "1.0", + "context": "some context", + "method": "getClientStatus", + "data": {"status": {"state": "active", "connectionStatus": "Connected"}}, +} + +PORT_MANAGEMENT_RESPONSE = { + "apiVersion": "1.0", + "method": "getPorts", + "data": { + "numberOfPorts": 1, + "items": [ + { + "port": "0", + "configurable": False, + "usage": "", + "name": "PIR sensor", + "direction": "input", + "state": "open", + "normalState": "open", + } + ], + }, +} + +BRAND_RESPONSE = """root.Brand.Brand=AXIS root.Brand.ProdFullName=AXIS M1065-LW Network Camera root.Brand.ProdNbr=M1065-LW root.Brand.ProdShortName=AXIS M1065-LW @@ -67,7 +131,7 @@ root.Brand.ProdVariant= root.Brand.WebURL=http://www.axis.com """ -DEFAULT_PORTS = """root.Input.NbrOfInputs=1 +PORTS_RESPONSE = """root.Input.NbrOfInputs=1 root.IOPort.I0.Configurable=no root.IOPort.I0.Direction=input root.IOPort.I0.Input.Name=PIR sensor @@ -75,7 +139,7 @@ root.IOPort.I0.Input.Trig=closed root.Output.NbrOfOutputs=0 """ -DEFAULT_PROPERTIES = """root.Properties.API.HTTP.Version=3 +PROPERTIES_RESPONSE = """root.Properties.API.HTTP.Version=3 root.Properties.API.Metadata.Metadata=yes root.Properties.API.Metadata.Version=1.0 root.Properties.Firmware.BuildDate=Feb 15 2019 09:42 @@ -89,15 +153,27 @@ root.Properties.System.SerialNumber=00408C12345 """ -async def setup_axis_integration( - hass, - config=ENTRY_CONFIG, - options=ENTRY_OPTIONS, - api_discovery=DEFAULT_API_DISCOVERY, - brand=DEFAULT_BRAND, - ports=DEFAULT_PORTS, - properties=DEFAULT_PROPERTIES, -): +def vapix_session_request(session, url, **kwargs): + """Return data based on url.""" + if API_DISCOVERY_URL in url: + return json.dumps(API_DISCOVERY_RESPONSE) + if BASIC_DEVICE_INFO_URL in url: + return json.dumps(BASIC_DEVICE_INFO_RESPONSE) + if MQTT_CLIENT_URL in url: + return json.dumps(MQTT_CLIENT_RESPONSE) + if PORT_MANAGEMENT_URL in url: + return json.dumps(PORT_MANAGEMENT_RESPONSE) + if BRAND_URL in url: + return BRAND_RESPONSE + if IOPORT_URL in url or INPUT_URL in url or OUTPUT_URL in url: + return PORTS_RESPONSE + if PROPERTIES_URL in url: + return PROPERTIES_RESPONSE + if STREAM_PROFILES_URL in url: + return "" + + +async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS): """Create the Axis device.""" config_entry = MockConfigEntry( domain=AXIS_DOMAIN, @@ -109,25 +185,7 @@ async def setup_axis_integration( ) config_entry.add_to_hass(hass) - def mock_update_api_discovery(self): - self.process_raw(api_discovery) - - def mock_update_brand(self): - self.process_raw(brand) - - def mock_update_ports(self): - self.process_raw(ports) - - def mock_update_properties(self): - self.process_raw(properties) - - with patch( - "axis.api_discovery.ApiDiscovery.update", new=mock_update_api_discovery - ), patch("axis.param_cgi.Brand.update_brand", new=mock_update_brand), patch( - "axis.param_cgi.Ports.update_ports", new=mock_update_ports - ), patch( - "axis.param_cgi.Properties.update_properties", new=mock_update_properties - ), patch( + with patch("axis.vapix.session_request", new=vapix_session_request), patch( "axis.rtsp.RTSPClient.start", return_value=True, ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -144,7 +202,12 @@ async def test_device_setup(hass): ) as forward_entry_setup: device = await setup_axis_integration(hass) - entry = device.config_entry + assert device.api.vapix.firmware_version == "9.10.1" + assert device.api.vapix.product_number == "M1065-LW" + assert device.api.vapix.product_type == "Network Camera" + assert device.api.vapix.serial_number == "00408C12345" + + entry = device.config_entry assert len(forward_entry_setup.mock_calls) == 3 assert forward_entry_setup.mock_calls[0][1] == (entry, "binary_sensor") @@ -157,20 +220,29 @@ async def test_device_setup(hass): assert device.serial == ENTRY_CONFIG[CONF_MAC] +async def test_device_info(hass): + """Verify other path of device information works.""" + api_discovery = deepcopy(API_DISCOVERY_RESPONSE) + api_discovery["data"]["apiList"].append(API_DISCOVERY_BASIC_DEVICE_INFO) + + with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): + device = await setup_axis_integration(hass) + + assert device.api.vapix.firmware_version == "9.80.1" + assert device.api.vapix.product_number == "M1065-LW" + assert device.api.vapix.product_type == "Network Camera" + assert device.api.vapix.serial_number == "00408C12345" + + async def test_device_support_mqtt(hass): """Successful setup.""" - api_discovery = deepcopy(DEFAULT_API_DISCOVERY) - api_discovery["data"]["apiList"].append( - {"id": "mqtt-client", "version": "1.0", "name": "MQTT Client API"} - ) - get_client_status = {"data": {"status": {"state": "active"}}} + api_discovery = deepcopy(API_DISCOVERY_RESPONSE) + api_discovery["data"]["apiList"].append(API_DISCOVERY_MQTT) mock_mqtt = await async_mock_mqtt_component(hass) - with patch( - "axis.mqtt.MqttClient.get_client_status", return_value=get_client_status - ): - await setup_axis_integration(hass, api_discovery=api_discovery) + with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): + await setup_axis_integration(hass) mock_mqtt.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8") @@ -267,7 +339,7 @@ async def test_shutdown(): async def test_get_device_fails(hass): """Device unauthorized yields authentication required error.""" with patch( - "axis.api_discovery.ApiDiscovery.update", side_effect=axislib.Unauthorized + "axis.vapix.session_request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): await axis.device.get_device(hass, host="", port="", username="", password="") @@ -275,7 +347,7 @@ async def test_get_device_fails(hass): async def test_get_device_device_unavailable(hass): """Device unavailable yields cannot connect error.""" with patch( - "axis.api_discovery.ApiDiscovery.update", side_effect=axislib.RequestError + "axis.vapix.session_request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): await axis.device.get_device(hass, host="", port="", username="", password="") @@ -283,6 +355,6 @@ async def test_get_device_device_unavailable(hass): async def test_get_device_unknown_error(hass): """Device yield unknown error.""" with patch( - "axis.api_discovery.ApiDiscovery.update", side_effect=axislib.AxisException + "axis.vapix.session_request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): await axis.device.get_device(hass, host="", port="", username="", password="") diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 98ca5141a81..f00c17784d2 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,14 +1,19 @@ """Axis switch platform tests.""" -from axis.port_cgi import ACTION_HIGH, ACTION_LOW +from copy import deepcopy from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.setup import async_setup_component -from .test_device import NAME, setup_axis_integration +from .test_device import ( + API_DISCOVERY_PORT_MANAGEMENT, + API_DISCOVERY_RESPONSE, + NAME, + setup_axis_integration, +) -from tests.async_mock import Mock, call as mock_call +from tests.async_mock import Mock, patch EVENTS = [ { @@ -46,8 +51,8 @@ async def test_no_switches(hass): assert not hass.states.async_entity_ids(SWITCH_DOMAIN) -async def test_switches(hass): - """Test that switches are loaded properly.""" +async def test_switches_with_port_cgi(hass): + """Test that switches are loaded properly using port.cgi.""" device = await setup_axis_integration(hass) device.api.vapix.ports = {"0": Mock(), "1": Mock()} @@ -68,14 +73,13 @@ async def test_switches(hass): assert relay_1.state == "on" assert relay_1.name == f"{NAME} Relay 1" - device.api.vapix.ports["0"].action = Mock() - await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": f"switch.{NAME}_doorbell"}, blocking=True, ) + device.api.vapix.ports["0"].close.assert_called_once() await hass.services.async_call( SWITCH_DOMAIN, @@ -83,8 +87,47 @@ async def test_switches(hass): {"entity_id": f"switch.{NAME}_doorbell"}, blocking=True, ) + device.api.vapix.ports["0"].open.assert_called_once() - assert device.api.vapix.ports["0"].action.call_args_list == [ - mock_call(ACTION_HIGH), - mock_call(ACTION_LOW), - ] + +async def test_switches_with_port_management(hass): + """Test that switches are loaded properly using port management.""" + api_discovery = deepcopy(API_DISCOVERY_RESPONSE) + api_discovery["data"]["apiList"].append(API_DISCOVERY_PORT_MANAGEMENT) + + with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): + device = await setup_axis_integration(hass) + + device.api.vapix.ports = {"0": Mock(), "1": Mock()} + device.api.vapix.ports["0"].name = "Doorbell" + device.api.vapix.ports["1"].name = "" + + for event in EVENTS: + device.api.event.process_event(event) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 + + relay_0 = hass.states.get(f"switch.{NAME}_doorbell") + assert relay_0.state == "off" + assert relay_0.name == f"{NAME} Doorbell" + + relay_1 = hass.states.get(f"switch.{NAME}_relay_1") + assert relay_1.state == "on" + assert relay_1.name == f"{NAME} Relay 1" + + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_on", + {"entity_id": f"switch.{NAME}_doorbell"}, + blocking=True, + ) + device.api.vapix.ports["0"].close.assert_called_once() + + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_off", + {"entity_id": f"switch.{NAME}_doorbell"}, + blocking=True, + ) + device.api.vapix.ports["0"].open.assert_called_once()