core/tests/components/unifi/test_switch.py

1636 lines
50 KiB
Python

"""UniFi Network switch platform tests."""
from copy import deepcopy
from datetime import timedelta
from typing import Any
from unittest.mock import patch
from aiounifi.models.message import MessageKey
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.components.unifi.const import (
CONF_BLOCK_CLIENT,
CONF_DPI_RESTRICTIONS,
CONF_SITE_ID,
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
DOMAIN as UNIFI_DOMAIN,
)
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import RegistryEntryDisabler
from homeassistant.util import dt as dt_util
from .conftest import (
CONTROLLER_HOST,
ConfigEntryFactoryType,
WebsocketMessageMock,
WebsocketStateManager,
)
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.test_util.aiohttp import AiohttpClientMocker
CLIENT_1 = {
"hostname": "client_1",
"ip": "10.0.0.1",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
"name": "POE Client 1",
"oui": "Producer",
"sw_mac": "10:00:00:00:01:01",
"sw_port": 1,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
}
CLIENT_2 = {
"hostname": "client_2",
"ip": "10.0.0.2",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
"name": "POE Client 2",
"oui": "Producer",
"sw_mac": "10:00:00:00:01:01",
"sw_port": 2,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
}
CLIENT_3 = {
"hostname": "client_3",
"ip": "10.0.0.3",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:03",
"name": "Non-POE Client 3",
"oui": "Producer",
"sw_mac": "10:00:00:00:01:01",
"sw_port": 3,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
}
CLIENT_4 = {
"hostname": "client_4",
"ip": "10.0.0.4",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:04",
"name": "Non-POE Client 4",
"oui": "Producer",
"sw_mac": "10:00:00:00:01:01",
"sw_port": 4,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
}
POE_SWITCH_CLIENTS = [
{
"hostname": "client_1",
"ip": "10.0.0.1",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
"name": "POE Client 1",
"oui": "Producer",
"sw_mac": "10:00:00:00:01:01",
"sw_port": 1,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
},
{
"hostname": "client_2",
"ip": "10.0.0.2",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
"name": "POE Client 2",
"oui": "Producer",
"sw_mac": "10:00:00:00:01:01",
"sw_port": 1,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
},
]
DEVICE_1 = {
"board_rev": 2,
"device_id": "mock-id",
"ip": "10.0.1.1",
"mac": "10:00:00:00:01:01",
"last_seen": 1562600145,
"model": "US16P150",
"name": "mock-name",
"port_overrides": [],
"port_table": [
{
"media": "GE",
"name": "Port 1",
"port_idx": 1,
"poe_caps": 7,
"poe_class": "Class 4",
"poe_enable": True,
"poe_mode": "auto",
"poe_power": "2.56",
"poe_voltage": "53.40",
"portconf_id": "1a1",
"port_poe": True,
"up": True,
},
{
"media": "GE",
"name": "Port 2",
"port_idx": 2,
"poe_caps": 7,
"poe_class": "Class 4",
"poe_enable": True,
"poe_mode": "auto",
"poe_power": "2.56",
"poe_voltage": "53.40",
"portconf_id": "1a2",
"port_poe": True,
"up": True,
},
{
"media": "GE",
"name": "Port 3",
"port_idx": 3,
"poe_caps": 7,
"poe_class": "Unknown",
"poe_enable": False,
"poe_mode": "off",
"poe_power": "0.00",
"poe_voltage": "0.00",
"portconf_id": "1a3",
"port_poe": False,
"up": True,
},
{
"media": "GE",
"name": "Port 4",
"port_idx": 4,
"poe_caps": 7,
"poe_class": "Unknown",
"poe_enable": False,
"poe_mode": "auto",
"poe_power": "0.00",
"poe_voltage": "0.00",
"portconf_id": "1a4",
"port_poe": True,
"up": True,
},
],
"state": 1,
"type": "usw",
"version": "4.0.42.10433",
}
BLOCKED = {
"blocked": True,
"hostname": "block_client_1",
"ip": "10.0.0.1",
"is_guest": False,
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:01:01",
"name": "Block Client 1",
"noted": True,
"oui": "Producer",
}
UNBLOCKED = {
"blocked": False,
"hostname": "block_client_2",
"ip": "10.0.0.2",
"is_guest": False,
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:01:02",
"name": "Block Client 2",
"noted": True,
"oui": "Producer",
}
EVENT_BLOCKED_CLIENT_CONNECTED = {
"user": BLOCKED["mac"],
"radio": "na",
"channel": "44",
"hostname": BLOCKED["hostname"],
"key": "EVT_WU_Connected",
"subsystem": "wlan",
"site_id": "name",
"time": 1587753456179,
"datetime": "2020-04-24T18:37:36Z",
"msg": f'User{[BLOCKED["mac"]]} has connected."',
"_id": "5ea331fa30c49e00f90ddc1a",
}
EVENT_BLOCKED_CLIENT_BLOCKED = {
"user": BLOCKED["mac"],
"hostname": BLOCKED["hostname"],
"key": "EVT_WC_Blocked",
"subsystem": "wlan",
"site_id": "name",
"time": 1587753456179,
"datetime": "2020-04-24T18:37:36Z",
"msg": f'User{[BLOCKED["mac"]]} has been blocked."',
"_id": "5ea331fa30c49e00f90ddc1a",
}
EVENT_BLOCKED_CLIENT_UNBLOCKED = {
"user": BLOCKED["mac"],
"hostname": BLOCKED["hostname"],
"key": "EVT_WC_Unblocked",
"subsystem": "wlan",
"site_id": "name",
"time": 1587753456179,
"datetime": "2020-04-24T18:37:36Z",
"msg": f'User{[BLOCKED["mac"]]} has been unblocked."',
"_id": "5ea331fa30c49e00f90ddc1a",
}
EVENT_CLIENT_2_CONNECTED = {
"user": CLIENT_2["mac"],
"radio": "na",
"channel": "44",
"hostname": CLIENT_2["hostname"],
"key": "EVT_WU_Connected",
"subsystem": "wlan",
"site_id": "name",
"time": 1587753456179,
"datetime": "2020-04-24T18:37:36Z",
"msg": f'User{[CLIENT_2["mac"]]} has connected."',
"_id": "5ea331fa30c49e00f90ddc1a",
}
DPI_GROUPS = [
{
"_id": "5ba29dd8e3c58f026e9d7c4a",
"attr_no_delete": True,
"attr_hidden_id": "Default",
"name": "Default",
"site_id": "name",
},
{
"_id": "5f976f4ae3c58f018ec7dff6",
"name": "Block Media Streaming",
"site_id": "name",
"dpiapp_ids": ["5f976f62e3c58f018ec7e17d"],
},
]
DPI_APPS = [
{
"_id": "5f976f62e3c58f018ec7e17d",
"apps": [],
"blocked": True,
"cats": ["4"],
"enabled": True,
"log": True,
"site_id": "name",
}
]
DPI_GROUP_REMOVED_EVENT = {
"meta": {"rc": "ok", "message": "dpigroup:delete"},
"data": [
{
"_id": "5f976f4ae3c58f018ec7dff6",
"name": "Block Media Streaming",
"site_id": "name",
"dpiapp_ids": [],
}
],
}
DPI_GROUP_CREATED_EVENT = {
"meta": {"rc": "ok", "message": "dpigroup:add"},
"data": [
{
"name": "Block Media Streaming",
"site_id": "name",
"_id": "5f976f4ae3c58f018ec7dff6",
}
],
}
DPI_GROUP_ADDED_APP = {
"meta": {"rc": "ok", "message": "dpigroup:sync"},
"data": [
{
"_id": "5f976f4ae3c58f018ec7dff6",
"name": "Block Media Streaming",
"site_id": "name",
"dpiapp_ids": ["5f976f62e3c58f018ec7e17d"],
}
],
}
DPI_GROUP_REMOVE_APP = {
"meta": {"rc": "ok", "message": "dpigroup:sync"},
"data": [
{
"_id": "5f976f4ae3c58f018ec7dff6",
"name": "Block Media Streaming",
"site_id": "name",
"dpiapp_ids": [],
}
],
}
DPI_APP_DISABLED_EVENT = {
"meta": {"rc": "ok", "message": "dpiapp:sync"},
"data": [
{
"_id": "5f976f62e3c58f018ec7e17d",
"apps": [],
"blocked": False,
"cats": [],
"enabled": False,
"log": False,
"site_id": "name",
}
],
}
OUTLET_UP1 = {
"_id": "600c8356942a6ade50707b56",
"ip": "192.168.0.189",
"mac": "fc:ec:da:76:4f:5f",
"model": "UP1",
"model_in_lts": False,
"model_in_eol": False,
"type": "uap",
"version": "2.2.1.511",
"adopted": True,
"site_id": "545eb1f0e4b0205d14c4e548",
"x_authkey": "345678976545678",
"cfgversion": "4c62f1e663783447",
"syslog_key": "41c4bcefcbc842d6eefb05b8fd9b78faa1841d10a09cebb170ce3e2f474b43b3",
"config_network": {"type": "dhcp"},
"setup_id": "a8730d36-8fdd-44f9-8678-1e89676f36c1",
"x_vwirekey": "2dabb7e23b048c88b60123456789",
"vwire_table": [],
"dot1x_portctrl_enabled": False,
"outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": True}],
"outlet_enabled": True,
"license_state": "registered",
"x_aes_gcm": True,
"inform_url": "http://192.168.0.5:8080/inform",
"inform_ip": "192.168.0.5",
"required_version": "2.1.3",
"anon_id": "d2744a31-1c26-92fe-423d-6b9ba204abc7",
"board_rev": 2,
"manufacturer_id": 72,
"model_incompatible": False,
"antenna_table": [],
"radio_table": [],
"scan_radio_table": [],
"ethernet_table": [],
"port_table": [],
"switch_caps": {},
"has_speaker": False,
"has_eth1": False,
"fw_caps": 0,
"hw_caps": 128,
"wifi_caps": 0,
"sys_error_caps": 0,
"has_fan": False,
"has_temperature": False,
"country_code": 10752,
"outlet_table": [
{
"index": 1,
"has_relay": True,
"has_metering": False,
"relay_state": True,
"name": "Outlet 1",
},
{
"index": 2,
"has_relay": False,
"has_metering": False,
"relay_state": False,
"name": "Outlet 1",
},
],
"element_ap_serial": "44:d9:e7:90:f4:24",
"connected_at": 1641678609,
"provisioned_at": 1642054077,
"led_override": "default",
"led_override_color": "#0000ff",
"led_override_color_brightness": 100,
"outdoor_mode_override": "default",
"lcm_brightness_override": False,
"lcm_idle_timeout_override": False,
"name": "Plug",
"unsupported": False,
"unsupported_reason": 0,
"two_phase_adopt": False,
"serial": "FCECDA764F5F",
"lcm_tracker_enabled": False,
"wlangroup_id_ng": "545eb1f0e4b0205d14c4e555",
"supports_fingerprint_ml": False,
"last_uplink": {
"uplink_mac": "78:45:58:87:93:16",
"uplink_device_name": "U6-Pro",
"type": "wireless",
},
"device_id": "600c8356942a6ade50707b56",
"uplink": {
"uplink_mac": "78:45:58:87:93:16",
"uplink_device_name": "U6-Pro",
"type": "wireless",
"up": True,
"ap_mac": "78:45:58:87:93:16",
"tx_rate": 54000,
"rx_rate": 72200,
"rssi": 60,
"is_11ax": False,
"is_11ac": False,
"is_11n": True,
"is_11b": False,
"radio": "ng",
"essid": "Network Name",
"channel": 11,
"tx_packets": 1586746,
"rx_packets": 362176,
"tx_bytes": 397773,
"rx_bytes": 24423980,
"tx_bytes-r": 0,
"rx_bytes-r": 45,
"uplink_source": "legacy",
},
"state": 1,
"start_disconnected_millis": 1641679166349,
"last_seen": 1642055273,
"next_interval": 40,
"known_cfgversion": "4c62f1e663783447",
"start_connected_millis": 1641679166355,
"upgradable": False,
"adoptable_when_upgraded": False,
"rollupgrade": False,
"uptime": 376083,
"_uptime": 376083,
"locating": False,
"connect_request_ip": "192.168.0.189",
"connect_request_port": "49155",
"sys_stats": {"mem_total": 98304, "mem_used": 87736},
"system-stats": {},
"lldp_table": [],
"displayable_version": "2.2.1",
"connection_network_name": "LAN",
"startup_timestamp": 1641679190,
"scanning": False,
"spectrum_scanning": False,
"meshv3_peer_mac": "",
"element_peer_mac": "",
"satisfaction": 100,
"uplink_bssid": "78:45:58:87:93:17",
"hide_ch_width": "none",
"isolated": False,
"radio_table_stats": [],
"port_stats": [],
"vap_table": [],
"downlink_table": [],
"vwire_vap_table": [],
"bytes-d": 0,
"tx_bytes-d": 0,
"rx_bytes-d": 0,
"bytes-r": 0,
"element_uplink_ap_mac": "78:45:58:87:93:16",
"prev_non_busy_state": 1,
"stat": {
"ap": {
"site_id": "5a32aa4ee4b0412345678910",
"o": "ap",
"oid": "fc:ec:da:76:4f:5f",
"ap": "fc:ec:da:76:4f:5f",
"time": 1641678600000,
"datetime": "2022-01-08T21:50:00Z",
"user-rx_packets": 0.0,
"guest-rx_packets": 0.0,
"rx_packets": 0.0,
"user-rx_bytes": 0.0,
"guest-rx_bytes": 0.0,
"rx_bytes": 0.0,
"user-rx_errors": 0.0,
"guest-rx_errors": 0.0,
"rx_errors": 0.0,
"user-rx_dropped": 0.0,
"guest-rx_dropped": 0.0,
"rx_dropped": 0.0,
"user-rx_crypts": 0.0,
"guest-rx_crypts": 0.0,
"rx_crypts": 0.0,
"user-rx_frags": 0.0,
"guest-rx_frags": 0.0,
"rx_frags": 0.0,
"user-tx_packets": 0.0,
"guest-tx_packets": 0.0,
"tx_packets": 0.0,
"user-tx_bytes": 0.0,
"guest-tx_bytes": 0.0,
"tx_bytes": 0.0,
"user-tx_errors": 0.0,
"guest-tx_errors": 0.0,
"tx_errors": 0.0,
"user-tx_dropped": 0.0,
"guest-tx_dropped": 0.0,
"tx_dropped": 0.0,
"user-tx_retries": 0.0,
"guest-tx_retries": 0.0,
"tx_retries": 0.0,
"user-mac_filter_rejections": 0.0,
"guest-mac_filter_rejections": 0.0,
"mac_filter_rejections": 0.0,
"user-wifi_tx_attempts": 0.0,
"guest-wifi_tx_attempts": 0.0,
"wifi_tx_attempts": 0.0,
"user-wifi_tx_dropped": 0.0,
"guest-wifi_tx_dropped": 0.0,
"wifi_tx_dropped": 0.0,
"bytes": 0.0,
"duration": 376663000.0,
}
},
"tx_bytes": 0,
"rx_bytes": 0,
"bytes": 0,
"vwireEnabled": True,
"uplink_table": [],
"num_sta": 0,
"user-num_sta": 0,
"user-wlan-num_sta": 0,
"guest-num_sta": 0,
"guest-wlan-num_sta": 0,
"x_has_ssh_hostkey": False,
}
PDU_DEVICE_1 = {
"_id": "123456654321abcdef012345",
"required_version": "5.28.0",
"port_table": [],
"license_state": "registered",
"lcm_brightness_override": False,
"type": "usw",
"board_rev": 4,
"hw_caps": 136,
"reboot_duration": 70,
"snmp_contact": "",
"config_network": {"type": "dhcp", "bonding_enabled": False},
"outlet_table": [
{
"index": 1,
"relay_state": True,
"cycle_enabled": False,
"name": "USB Outlet 1",
"outlet_caps": 1,
},
{
"index": 2,
"relay_state": True,
"cycle_enabled": False,
"name": "Outlet 2",
"outlet_caps": 3,
"outlet_voltage": "119.644",
"outlet_current": "0.935",
"outlet_power": "73.827",
"outlet_power_factor": "0.659",
},
],
"model": "USPPDUP",
"manufacturer_id": 4,
"ip": "192.168.1.76",
"fw2_caps": 0,
"jumboframe_enabled": False,
"version": "6.5.59.14777",
"unsupported_reason": 0,
"adoption_completed": True,
"outlet_enabled": True,
"stp_version": "rstp",
"name": "Dummy USP-PDU-Pro",
"fw_caps": 1732968229,
"lcm_brightness": 80,
"internet": True,
"mgmt_network_id": "123456654321abcdef012347",
"gateway_mac": "01:02:03:04:05:06",
"stp_priority": "32768",
"lcm_night_mode_begins": "22:00",
"two_phase_adopt": False,
"connected_at": 1690626493,
"inform_ip": "192.168.1.1",
"cfgversion": "ba8f30a5a17aad64",
"mac": "01:02:03:04:05:ff",
"provisioned_at": 1690989511,
"inform_url": "http://192.168.1.1:8080/inform",
"upgrade_duration": 100,
"ethernet_table": [{"num_port": 1, "name": "eth0", "mac": "01:02:03:04:05:a1"}],
"flowctrl_enabled": False,
"unsupported": False,
"ble_caps": 0,
"sys_error_caps": 0,
"dot1x_portctrl_enabled": False,
"last_uplink": {},
"disconnected_at": 1690626452,
"architecture": "mips",
"x_aes_gcm": True,
"has_fan": False,
"outlet_overrides": [
{
"cycle_enabled": False,
"name": "USB Outlet 1",
"relay_state": True,
"index": 1,
},
{"cycle_enabled": False, "name": "Outlet 2", "relay_state": True, "index": 2},
],
"model_incompatible": False,
"satisfaction": 100,
"model_in_eol": False,
"anomalies": -1,
"has_temperature": False,
"switch_caps": {},
"adopted_by_client": "web",
"snmp_location": "",
"model_in_lts": False,
"kernel_version": "4.14.115",
"serial": "abc123",
"power_source_ctrl_enabled": False,
"lcm_night_mode_ends": "08:00",
"adopted": True,
"hash_id": "abcdef123456",
"device_id": "mock-pdu",
"uplink": {},
"state": 1,
"start_disconnected_millis": 1690626383386,
"credential_caps": 0,
"default": False,
"discovered_via": "l2",
"adopt_ip": "10.0.10.4",
"adopt_url": "http://192.168.1.1:8080/inform",
"last_seen": 1691518814,
"min_inform_interval_seconds": 10,
"upgradable": False,
"adoptable_when_upgraded": False,
"rollupgrade": False,
"known_cfgversion": "abcfde03929",
"uptime": 1193042,
"_uptime": 1193042,
"locating": False,
"start_connected_millis": 1690626493324,
"prev_non_busy_state": 5,
"next_interval": 47,
"sys_stats": {},
"system-stats": {"cpu": "1.4", "mem": "28.9", "uptime": "1193042"},
"ssh_session_table": [],
"lldp_table": [],
"displayable_version": "6.5.59",
"connection_network_id": "123456654321abcdef012349",
"connection_network_name": "Default",
"startup_timestamp": 1690325774,
"is_access_point": False,
"safe_for_autoupgrade": True,
"overheating": False,
"power_source": "0",
"total_max_power": 0,
"outlet_ac_power_budget": "1875.000",
"outlet_ac_power_consumption": "201.683",
"downlink_table": [],
"uplink_depth": 1,
"downlink_lldp_macs": [],
"dhcp_server_table": [],
"connect_request_ip": "10.0.10.4",
"connect_request_port": "57951",
"ipv4_lease_expiration_timestamp_seconds": 1691576686,
"stat": {},
"tx_bytes": 1426780,
"rx_bytes": 1435064,
"bytes": 2861844,
"num_sta": 0,
"user-num_sta": 0,
"guest-num_sta": 0,
"x_has_ssh_hostkey": True,
}
WLAN = {
"_id": "012345678910111213141516",
"bc_filter_enabled": False,
"bc_filter_list": [],
"dtim_mode": "default",
"dtim_na": 1,
"dtim_ng": 1,
"enabled": True,
"group_rekey": 3600,
"mac_filter_enabled": False,
"mac_filter_list": [],
"mac_filter_policy": "allow",
"minrate_na_advertising_rates": False,
"minrate_na_beacon_rate_kbps": 6000,
"minrate_na_data_rate_kbps": 6000,
"minrate_na_enabled": False,
"minrate_na_mgmt_rate_kbps": 6000,
"minrate_ng_advertising_rates": False,
"minrate_ng_beacon_rate_kbps": 1000,
"minrate_ng_data_rate_kbps": 1000,
"minrate_ng_enabled": False,
"minrate_ng_mgmt_rate_kbps": 1000,
"name": "SSID 1",
"no2ghz_oui": False,
"schedule": [],
"security": "wpapsk",
"site_id": "5a32aa4ee4b0412345678910",
"usergroup_id": "012345678910111213141518",
"wep_idx": 1,
"wlangroup_id": "012345678910111213141519",
"wpa_enc": "ccmp",
"wpa_mode": "wpa2",
"x_iapp_key": "01234567891011121314151617181920",
"x_passphrase": "password",
}
PORT_FORWARD_PLEX = {
"_id": "5a32aa4ee4b0412345678911",
"dst_port": "12345",
"enabled": True,
"fwd_port": "23456",
"fwd": "10.0.0.2",
"name": "plex",
"pfwd_interface": "wan",
"proto": "tcp_udp",
"site_id": "5a32aa4ee4b0412345678910",
"src": "any",
}
TRAFFIC_RULE = {
"_id": "6452cd9b859d5b11aa002ea1",
"action": "BLOCK",
"app_category_ids": [],
"app_ids": [],
"bandwidth_limit": {
"download_limit_kbps": 1024,
"enabled": False,
"upload_limit_kbps": 1024,
},
"description": "Test Traffic Rule",
"name": "Test Traffic Rule",
"domains": [],
"enabled": True,
"ip_addresses": [],
"ip_ranges": [],
"matching_target": "INTERNET",
"network_ids": [],
"regions": [],
"schedule": {
"date_end": "2023-05-10",
"date_start": "2023-05-03",
"mode": "ALWAYS",
"repeat_on_days": [],
"time_all_day": False,
"time_range_end": "12:00",
"time_range_start": "09:00",
},
"target_devices": [{"client_mac": CLIENT_1["mac"], "type": "CLIENT"}],
}
@pytest.mark.parametrize(
"config_entry_options", [{CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}]
)
@pytest.mark.parametrize("client_payload", [[BLOCKED]])
@pytest.mark.parametrize("device_payload", [[DEVICE_1, OUTLET_UP1, PDU_DEVICE_1]])
@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS])
@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS])
@pytest.mark.parametrize("port_forward_payload", [[PORT_FORWARD_PLEX]])
@pytest.mark.parametrize(("traffic_rule_payload"), [([TRAFFIC_RULE])])
@pytest.mark.parametrize("wlan_payload", [[WLAN]])
@pytest.mark.parametrize(
"site_payload",
[[{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}]],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_entity_and_device_data(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
config_entry_factory: ConfigEntryFactoryType,
site_payload: dict[str, Any],
snapshot: SnapshotAssertion,
) -> None:
"""Validate entity and device data with and without admin rights."""
with patch("homeassistant.components.unifi.PLATFORMS", [Platform.SWITCH]):
config_entry = await config_entry_factory()
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.parametrize("client_payload", [[CONTROLLER_HOST]])
@pytest.mark.parametrize("device_payload", [[DEVICE_1]])
@pytest.mark.usefixtures("config_entry_setup")
async def test_hub_not_client(hass: HomeAssistant) -> None:
"""Test that the cloud key doesn't become a switch."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
assert hass.states.get("switch.cloud_key") is None
@pytest.mark.parametrize(
"config_entry_options",
[
{
CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]],
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
}
],
)
@pytest.mark.parametrize("clients_all_payload", [[BLOCKED, UNBLOCKED, CLIENT_1]])
@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS])
@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS])
async def test_switches(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
config_entry_setup: MockConfigEntry,
) -> None:
"""Test the update_items function with some clients."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3
# Block and unblock client
aioclient_mock.clear_requests()
aioclient_mock.post(
f"https://{config_entry_setup.data[CONF_HOST]}:1234"
f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/cmd/stamgr",
)
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True
)
assert aioclient_mock.call_count == 1
assert aioclient_mock.mock_calls[0][2] == {
"mac": "00:00:00:00:01:01",
"cmd": "block-sta",
}
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True
)
assert aioclient_mock.call_count == 2
assert aioclient_mock.mock_calls[1][2] == {
"mac": "00:00:00:00:01:01",
"cmd": "unblock-sta",
}
# Enable and disable DPI
aioclient_mock.clear_requests()
aioclient_mock.put(
f"https://{config_entry_setup.data[CONF_HOST]}:1234"
f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/dpiapp/{DPI_APPS[0]['_id']}",
)
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": "switch.block_media_streaming"},
blocking=True,
)
assert aioclient_mock.call_count == 1
assert aioclient_mock.mock_calls[0][2] == {"enabled": False}
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": "switch.block_media_streaming"},
blocking=True,
)
assert aioclient_mock.call_count == 2
assert aioclient_mock.mock_calls[1][2] == {"enabled": True}
@pytest.mark.parametrize(
"config_entry_options", [{CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}]
)
@pytest.mark.parametrize("client_payload", [[UNBLOCKED]])
@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS])
@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS])
@pytest.mark.usefixtures("config_entry_setup")
async def test_remove_switches(
hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock
) -> None:
"""Test the update_items function with some clients."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
assert hass.states.get("switch.block_client_2") is not None
assert hass.states.get("switch.block_media_streaming") is not None
mock_websocket_message(message=MessageKey.CLIENT_REMOVED, data=[UNBLOCKED])
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
assert hass.states.get("switch.block_client_2") is None
assert hass.states.get("switch.block_media_streaming") is not None
mock_websocket_message(data=DPI_GROUP_REMOVED_EVENT)
await hass.async_block_till_done()
assert hass.states.get("switch.block_media_streaming") is None
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
@pytest.mark.parametrize(
"config_entry_options",
[
{
CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]],
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
}
],
)
@pytest.mark.parametrize("client_payload", [[UNBLOCKED]])
@pytest.mark.parametrize("clients_all_payload", [[BLOCKED]])
async def test_block_switches(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
mock_websocket_message: WebsocketMessageMock,
config_entry_setup: MockConfigEntry,
) -> None:
"""Test the update_items function with some clients."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
blocked = hass.states.get("switch.block_client_1")
assert blocked is not None
assert blocked.state == "off"
unblocked = hass.states.get("switch.block_client_2")
assert unblocked is not None
assert unblocked.state == "on"
mock_websocket_message(
message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_UNBLOCKED
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
blocked = hass.states.get("switch.block_client_1")
assert blocked is not None
assert blocked.state == "on"
mock_websocket_message(message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_BLOCKED)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
blocked = hass.states.get("switch.block_client_1")
assert blocked is not None
assert blocked.state == "off"
aioclient_mock.clear_requests()
aioclient_mock.post(
f"https://{config_entry_setup.data[CONF_HOST]}:1234"
f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/cmd/stamgr",
)
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True
)
assert aioclient_mock.call_count == 1
assert aioclient_mock.mock_calls[0][2] == {
"mac": "00:00:00:00:01:01",
"cmd": "block-sta",
}
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True
)
assert aioclient_mock.call_count == 2
assert aioclient_mock.mock_calls[1][2] == {
"mac": "00:00:00:00:01:01",
"cmd": "unblock-sta",
}
@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS])
@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS])
@pytest.mark.usefixtures("config_entry_setup")
async def test_dpi_switches(
hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock
) -> None:
"""Test the update_items function with some clients."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
assert hass.states.get("switch.block_media_streaming").state == STATE_ON
mock_websocket_message(data=DPI_APP_DISABLED_EVENT)
await hass.async_block_till_done()
assert hass.states.get("switch.block_media_streaming").state == STATE_OFF
# Remove app
mock_websocket_message(data=DPI_GROUP_REMOVE_APP)
await hass.async_block_till_done()
assert hass.states.get("switch.block_media_streaming") is None
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS])
@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS])
@pytest.mark.usefixtures("config_entry_setup")
async def test_dpi_switches_add_second_app(
hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock
) -> None:
"""Test the update_items function with some clients."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
assert hass.states.get("switch.block_media_streaming").state == STATE_ON
second_app_event = {
"apps": [524292],
"blocked": False,
"cats": [],
"enabled": False,
"log": False,
"site_id": "name",
"_id": "61783e89c1773a18c0c61f00",
}
mock_websocket_message(message=MessageKey.DPI_APP_ADDED, data=second_app_event)
await hass.async_block_till_done()
assert hass.states.get("switch.block_media_streaming").state == STATE_ON
add_second_app_to_group = {
"_id": "5f976f4ae3c58f018ec7dff6",
"name": "Block Media Streaming",
"site_id": "name",
"dpiapp_ids": ["5f976f62e3c58f018ec7e17d", "61783e89c1773a18c0c61f00"],
}
mock_websocket_message(
message=MessageKey.DPI_GROUP_UPDATED, data=add_second_app_to_group
)
await hass.async_block_till_done()
assert hass.states.get("switch.block_media_streaming").state == STATE_OFF
second_app_event_enabled = {
"apps": [524292],
"blocked": False,
"cats": [],
"enabled": True,
"log": False,
"site_id": "name",
"_id": "61783e89c1773a18c0c61f00",
}
mock_websocket_message(
message=MessageKey.DPI_APP_UPDATED, data=second_app_event_enabled
)
await hass.async_block_till_done()
assert hass.states.get("switch.block_media_streaming").state == STATE_ON
@pytest.mark.parametrize(("traffic_rule_payload"), [([TRAFFIC_RULE])])
async def test_traffic_rules(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
config_entry_setup: MockConfigEntry,
traffic_rule_payload: list[dict[str, Any]],
) -> None:
"""Test control of UniFi traffic rules."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
# Validate state object
assert hass.states.get("switch.unifi_network_test_traffic_rule").state == STATE_ON
traffic_rule = deepcopy(traffic_rule_payload[0])
# Disable traffic rule
aioclient_mock.put(
f"https://{config_entry_setup.data[CONF_HOST]}:1234"
f"/v2/api/site/{config_entry_setup.data[CONF_SITE_ID]}"
f"/trafficrules/{traffic_rule['_id']}",
)
call_count = aioclient_mock.call_count
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": "switch.unifi_network_test_traffic_rule"},
blocking=True,
)
# Updating the value for traffic rules will make another call to retrieve the values
assert aioclient_mock.call_count == call_count + 2
expected_disable_call = deepcopy(traffic_rule)
expected_disable_call["enabled"] = False
assert aioclient_mock.mock_calls[call_count][2] == expected_disable_call
call_count = aioclient_mock.call_count
# Enable traffic rule
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": "switch.unifi_network_test_traffic_rule"},
blocking=True,
)
expected_enable_call = deepcopy(traffic_rule)
expected_enable_call["enabled"] = True
assert aioclient_mock.call_count == call_count + 2
assert aioclient_mock.mock_calls[call_count][2] == expected_enable_call
@pytest.mark.parametrize(
("device_payload", "entity_id", "outlet_index", "expected_switches"),
[
([OUTLET_UP1], "plug_outlet_1", 1, 1),
([PDU_DEVICE_1], "dummy_usp_pdu_pro_usb_outlet_1", 1, 2),
([PDU_DEVICE_1], "dummy_usp_pdu_pro_outlet_2", 2, 2),
],
)
async def test_outlet_switches(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
mock_websocket_message: WebsocketMessageMock,
config_entry_setup: MockConfigEntry,
device_payload: list[dict[str, Any]],
entity_id: str,
outlet_index: int,
expected_switches: int,
) -> None:
"""Test the outlet entities."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == expected_switches
# Validate state object
assert hass.states.get(f"switch.{entity_id}").state == STATE_ON
# Update state object
device_1 = deepcopy(device_payload[0])
device_1["outlet_table"][outlet_index - 1]["relay_state"] = False
mock_websocket_message(message=MessageKey.DEVICE, data=device_1)
await hass.async_block_till_done()
assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF
# Turn off outlet
device_id = device_payload[0]["device_id"]
aioclient_mock.clear_requests()
aioclient_mock.put(
f"https://{config_entry_setup.data[CONF_HOST]}:1234"
f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/device/{device_id}",
)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: f"switch.{entity_id}"},
blocking=True,
)
expected_off_overrides = deepcopy(device_1["outlet_overrides"])
expected_off_overrides[outlet_index - 1]["relay_state"] = False
assert aioclient_mock.call_count == 1
assert aioclient_mock.mock_calls[0][2] == {
"outlet_overrides": expected_off_overrides
}
# Turn on outlet
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: f"switch.{entity_id}"},
blocking=True,
)
expected_on_overrides = deepcopy(device_1["outlet_overrides"])
expected_on_overrides[outlet_index - 1]["relay_state"] = True
assert aioclient_mock.call_count == 2
assert aioclient_mock.mock_calls[1][2] == {
"outlet_overrides": expected_on_overrides
}
# Device gets disabled
device_1["disabled"] = True
mock_websocket_message(message=MessageKey.DEVICE, data=device_1)
await hass.async_block_till_done()
assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE
# Device gets re-enabled
device_1["disabled"] = False
mock_websocket_message(message=MessageKey.DEVICE, data=device_1)
await hass.async_block_till_done()
assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF
@pytest.mark.parametrize(
"config_entry_options",
[
{
CONF_BLOCK_CLIENT: [BLOCKED["mac"]],
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
CONF_DPI_RESTRICTIONS: False,
}
],
)
@pytest.mark.usefixtures("config_entry_setup")
async def test_new_client_discovered_on_block_control(
hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock
) -> None:
"""Test if 2nd update has a new client."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
assert hass.states.get("switch.block_client_1") is None
mock_websocket_message(message=MessageKey.CLIENT, data=BLOCKED)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
assert hass.states.get("switch.block_client_1") is not None
@pytest.mark.parametrize(
"config_entry_options", [{CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}]
)
@pytest.mark.parametrize("clients_all_payload", [[BLOCKED, UNBLOCKED]])
async def test_option_block_clients(
hass: HomeAssistant,
config_entry_setup: MockConfigEntry,
clients_all_payload: list[dict[str, Any]],
) -> None:
"""Test the changes to option reflects accordingly."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
# Add a second switch
hass.config_entries.async_update_entry(
config_entry_setup,
options={
CONF_BLOCK_CLIENT: [
clients_all_payload[0]["mac"],
clients_all_payload[1]["mac"],
]
},
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
# Remove the second switch again
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_BLOCK_CLIENT: [clients_all_payload[0]["mac"]]}
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
# Enable one and remove the other one
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_BLOCK_CLIENT: [clients_all_payload[1]["mac"]]}
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
# Remove one
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_BLOCK_CLIENT: []}
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
@pytest.mark.parametrize(
"config_entry_options",
[{CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}],
)
@pytest.mark.parametrize("client_payload", [[CLIENT_1]])
@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS])
@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS])
async def test_option_remove_switches(
hass: HomeAssistant, config_entry_setup: MockConfigEntry
) -> None:
"""Test removal of DPI switch when options updated."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
# Disable DPI Switches
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_DPI_RESTRICTIONS: False}
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
@pytest.mark.parametrize("device_payload", [[DEVICE_1]])
async def test_poe_port_switches(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
aioclient_mock: AiohttpClientMocker,
config_entry_setup: MockConfigEntry,
mock_websocket_message: WebsocketMessageMock,
device_payload: list[dict[str, Any]],
) -> None:
"""Test PoE port entities work."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
ent_reg_entry = entity_registry.async_get("switch.mock_name_port_1_poe")
assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION
# Enable entity
entity_registry.async_update_entity(
entity_id="switch.mock_name_port_1_poe", disabled_by=None
)
entity_registry.async_update_entity(
entity_id="switch.mock_name_port_2_poe", disabled_by=None
)
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done()
# Validate state object
assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_ON
# Update state object
device_1 = deepcopy(device_payload[0])
device_1["port_table"][0]["poe_mode"] = "off"
mock_websocket_message(message=MessageKey.DEVICE, data=device_1)
await hass.async_block_till_done()
assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF
# Turn off PoE
aioclient_mock.clear_requests()
aioclient_mock.put(
f"https://{config_entry_setup.data[CONF_HOST]}:1234"
f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/device/mock-id",
)
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": "switch.mock_name_port_1_poe"},
blocking=True,
)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5))
await hass.async_block_till_done()
assert aioclient_mock.call_count == 1
assert aioclient_mock.mock_calls[0][2] == {
"port_overrides": [{"poe_mode": "off", "port_idx": 1, "portconf_id": "1a1"}]
}
# Turn on PoE
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": "switch.mock_name_port_1_poe"},
blocking=True,
)
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": "switch.mock_name_port_2_poe"},
blocking=True,
)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5))
await hass.async_block_till_done()
assert aioclient_mock.call_count == 2
assert aioclient_mock.mock_calls[1][2] == {
"port_overrides": [
{"poe_mode": "auto", "port_idx": 1, "portconf_id": "1a1"},
{"poe_mode": "off", "port_idx": 2, "portconf_id": "1a2"},
]
}
# Device gets disabled
device_1["disabled"] = True
mock_websocket_message(message=MessageKey.DEVICE, data=device_1)
await hass.async_block_till_done()
assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_UNAVAILABLE
# Device gets re-enabled
device_1["disabled"] = False
mock_websocket_message(message=MessageKey.DEVICE, data=device_1)
await hass.async_block_till_done()
assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF
@pytest.mark.parametrize("wlan_payload", [[WLAN]])
async def test_wlan_switches(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
config_entry_setup: MockConfigEntry,
mock_websocket_message: WebsocketMessageMock,
wlan_payload: list[dict[str, Any]],
) -> None:
"""Test control of UniFi WLAN availability."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
# Validate state object
assert hass.states.get("switch.ssid_1").state == STATE_ON
# Update state object
wlan = deepcopy(wlan_payload[0])
wlan["enabled"] = False
mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=wlan)
await hass.async_block_till_done()
assert hass.states.get("switch.ssid_1").state == STATE_OFF
# Disable WLAN
aioclient_mock.clear_requests()
aioclient_mock.put(
f"https://{config_entry_setup.data[CONF_HOST]}:1234"
f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/wlanconf/{wlan['_id']}",
)
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": "switch.ssid_1"},
blocking=True,
)
assert aioclient_mock.call_count == 1
assert aioclient_mock.mock_calls[0][2] == {"enabled": False}
# Enable WLAN
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": "switch.ssid_1"},
blocking=True,
)
assert aioclient_mock.call_count == 2
assert aioclient_mock.mock_calls[1][2] == {"enabled": True}
@pytest.mark.parametrize("port_forward_payload", [[PORT_FORWARD_PLEX]])
async def test_port_forwarding_switches(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
config_entry_setup: MockConfigEntry,
mock_websocket_message: WebsocketMessageMock,
port_forward_payload: list[dict[str, Any]],
) -> None:
"""Test control of UniFi port forwarding."""
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
# Validate state object
assert hass.states.get("switch.unifi_network_plex").state == STATE_ON
# Update state object
data = port_forward_payload[0].copy()
data["enabled"] = False
mock_websocket_message(message=MessageKey.PORT_FORWARD_UPDATED, data=data)
await hass.async_block_till_done()
assert hass.states.get("switch.unifi_network_plex").state == STATE_OFF
# Disable port forward
aioclient_mock.clear_requests()
aioclient_mock.put(
f"https://{config_entry_setup.data[CONF_HOST]}:1234"
f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/portforward/{data['_id']}",
)
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": "switch.unifi_network_plex"},
blocking=True,
)
assert aioclient_mock.call_count == 1
data = port_forward_payload[0].copy()
data["enabled"] = False
assert aioclient_mock.mock_calls[0][2] == data
# Enable port forward
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": "switch.unifi_network_plex"},
blocking=True,
)
assert aioclient_mock.call_count == 2
assert aioclient_mock.mock_calls[1][2] == port_forward_payload[0]
# Remove entity on deleted message
mock_websocket_message(
message=MessageKey.PORT_FORWARD_DELETED, data=port_forward_payload[0]
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
@pytest.mark.parametrize(
"device_payload",
[
[
OUTLET_UP1,
{
"board_rev": 3,
"device_id": "mock-id",
"ip": "10.0.0.1",
"last_seen": 1562600145,
"mac": "00:00:00:00:01:01",
"model": "US16P150",
"name": "switch",
"state": 1,
"type": "usw",
"version": "4.0.42.10433",
"port_table": [
{
"media": "GE",
"name": "Port 1",
"port_idx": 1,
"poe_caps": 7,
"poe_class": "Class 4",
"poe_enable": True,
"poe_mode": "auto",
"poe_power": "2.56",
"poe_voltage": "53.40",
"portconf_id": "1a1",
"port_poe": True,
"up": True,
},
],
},
]
],
)
async def test_updating_unique_id(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
config_entry_factory: ConfigEntryFactoryType,
config_entry: MockConfigEntry,
device_payload: list[dict[str, Any]],
) -> None:
"""Verify outlet control and poe control unique ID update works."""
entity_registry.async_get_or_create(
SWITCH_DOMAIN,
UNIFI_DOMAIN,
f'{device_payload[0]["mac"]}-outlet-1',
suggested_object_id="plug_outlet_1",
config_entry=config_entry,
)
entity_registry.async_get_or_create(
SWITCH_DOMAIN,
UNIFI_DOMAIN,
f'{device_payload[1]["mac"]}-poe-1',
suggested_object_id="switch_port_1_poe",
config_entry=config_entry,
)
await config_entry_factory()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
assert hass.states.get("switch.plug_outlet_1")
assert hass.states.get("switch.switch_port_1_poe")
@pytest.mark.parametrize(
"config_entry_options", [{CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}]
)
@pytest.mark.parametrize("client_payload", [[UNBLOCKED]])
@pytest.mark.parametrize("device_payload", [[DEVICE_1, OUTLET_UP1]])
@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS])
@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS])
@pytest.mark.parametrize("port_forward_payload", [[PORT_FORWARD_PLEX]])
@pytest.mark.parametrize(("traffic_rule_payload"), [([TRAFFIC_RULE])])
@pytest.mark.parametrize("wlan_payload", [[WLAN]])
@pytest.mark.usefixtures("config_entry_setup")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_hub_state_change(
hass: HomeAssistant, mock_websocket_state: WebsocketStateManager
) -> None:
"""Verify entities state reflect on hub connection becoming unavailable."""
entity_ids = (
"switch.block_client_2",
"switch.mock_name_port_1_poe",
"switch.plug_outlet_1",
"switch.block_media_streaming",
"switch.unifi_network_plex",
"switch.unifi_network_test_traffic_rule",
"switch.ssid_1",
)
for entity_id in entity_ids:
assert hass.states.get(entity_id).state == STATE_ON
# Controller disconnects
await mock_websocket_state.disconnect()
for entity_id in entity_ids:
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# Controller reconnects
await mock_websocket_state.reconnect()
for entity_id in entity_ids:
assert hass.states.get(entity_id).state == STATE_ON