Add diagnostics to VeSync (#86350)

* Add diagnostics to VeSync

* Create unit tests for diagnostics and init

* Improved diagnostic test coverage

* Peer review fixes

* Fixed Peer Review comments

* Updated based on Peer Review

* Additional diagnostic redactions

* Removed account_id from diagnostic output
This commit is contained in:
Chuck Deal 2023-02-28 22:12:48 -05:00 committed by GitHub
parent 5b49648846
commit 09d0128601
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 905 additions and 1 deletions

View file

@ -1368,7 +1368,6 @@ omit =
homeassistant/components/verisure/sensor.py
homeassistant/components/verisure/switch.py
homeassistant/components/versasense/*
homeassistant/components/vesync/__init__.py
homeassistant/components/vesync/common.py
homeassistant/components/vesync/fan.py
homeassistant/components/vesync/light.py

View file

@ -0,0 +1,119 @@
"""Diagnostics support for VeSync."""
from __future__ import annotations
from typing import Any
from pyvesync import VeSync
from homeassistant.components.diagnostics import REDACTED
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry
from .common import VeSyncBaseDevice
from .const import DOMAIN, VS_MANAGER
KEYS_TO_REDACT = {"manager", "uuid", "mac_id"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
manager: VeSync = hass.data[DOMAIN][VS_MANAGER]
data = {
DOMAIN: {
"bulb_count": len(manager.bulbs),
"fan_count": len(manager.fans),
"outlets_count": len(manager.outlets),
"switch_count": len(manager.switches),
"timezone": manager.time_zone,
},
"devices": {
"bulbs": [_redact_device_values(device) for device in manager.bulbs],
"fans": [_redact_device_values(device) for device in manager.fans],
"outlets": [_redact_device_values(device) for device in manager.outlets],
"switches": [_redact_device_values(device) for device in manager.switches],
},
}
return data
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device entry."""
manager: VeSync = hass.data[DOMAIN][VS_MANAGER]
device_dict = _build_device_dict(manager)
vesync_device_id = next(iden[1] for iden in device.identifiers if iden[0] == DOMAIN)
# Base device information, without sensitive information.
data = _redact_device_values(device_dict[vesync_device_id])
data["home_assistant"] = {
"name": device.name,
"name_by_user": device.name_by_user,
"disabled": device.disabled,
"disabled_by": device.disabled_by,
"entities": [],
}
# Gather information how this VeSync device is represented in Home Assistant
entity_registry = er.async_get(hass)
hass_entities = er.async_entries_for_device(
entity_registry,
device_id=device.id,
include_disabled_entities=True,
)
for entity_entry in hass_entities:
state = hass.states.get(entity_entry.entity_id)
state_dict = None
if state:
state_dict = dict(state.as_dict())
# The context doesn't provide useful information in this case.
state_dict.pop("context", None)
data["home_assistant"]["entities"].append(
{
"domain": entity_entry.domain,
"entity_id": entity_entry.entity_id,
"entity_category": entity_entry.entity_category,
"device_class": entity_entry.device_class,
"original_device_class": entity_entry.original_device_class,
"name": entity_entry.name,
"original_name": entity_entry.original_name,
"icon": entity_entry.icon,
"original_icon": entity_entry.original_icon,
"unit_of_measurement": entity_entry.unit_of_measurement,
"state": state_dict,
"disabled": entity_entry.disabled,
"disabled_by": entity_entry.disabled_by,
}
)
return data
def _build_device_dict(manager: VeSync) -> dict:
"""Build a dictionary of ALL VeSync devices."""
device_dict = {x.cid: x for x in manager.switches}
device_dict.update({x.cid: x for x in manager.fans})
device_dict.update({x.cid: x for x in manager.outlets})
device_dict.update({x.cid: x for x in manager.bulbs})
return device_dict
def _redact_device_values(device: VeSyncBaseDevice) -> dict:
"""Rebuild and redact values of a VeSync device."""
data = {}
for key, item in device.__dict__.items():
if key not in KEYS_TO_REDACT:
data[key] = item
else:
data[key] = REDACTED
return data

View file

@ -0,0 +1,72 @@
"""Common methods used across tests for VeSync."""
import json
from tests.common import load_fixture
def call_api_side_effect__no_devices(*args, **kwargs):
"""Build a side_effects method for the Helpers.call_api method."""
if args[0] == "/cloud/v1/user/login" and args[1] == "post":
return json.loads(load_fixture("vesync_api_call__login.json", "vesync")), 200
elif args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post":
return (
json.loads(
load_fixture("vesync_api_call__devices__no_devices.json", "vesync")
),
200,
)
else:
raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}")
def call_api_side_effect__single_humidifier(*args, **kwargs):
"""Build a side_effects method for the Helpers.call_api method."""
if args[0] == "/cloud/v1/user/login" and args[1] == "post":
return json.loads(load_fixture("vesync_api_call__login.json", "vesync")), 200
elif args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post":
return (
json.loads(
load_fixture(
"vesync_api_call__devices__single_humidifier.json", "vesync"
)
),
200,
)
elif args[0] == "/cloud/v2/deviceManaged/bypassV2" and kwargs["method"] == "post":
return (
json.loads(
load_fixture(
"vesync_api_call__device_details__single_humidifier.json", "vesync"
)
),
200,
)
else:
raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}")
def call_api_side_effect__single_fan(*args, **kwargs):
"""Build a side_effects method for the Helpers.call_api method."""
if args[0] == "/cloud/v1/user/login" and args[1] == "post":
return json.loads(load_fixture("vesync_api_call__login.json", "vesync")), 200
elif args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post":
return (
json.loads(
load_fixture("vesync_api_call__devices__single_fan.json", "vesync")
),
200,
)
elif (
args[0] == "/131airPurifier/v1/device/deviceDetail"
and kwargs["method"] == "post"
):
return (
json.loads(
load_fixture(
"vesync_api_call__device_details__single_fan.json", "vesync"
)
),
200,
)
else:
raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}")

View file

@ -0,0 +1,104 @@
"""Configuration for VeSync tests."""
from __future__ import annotations
from unittest.mock import Mock, patch
import pytest
from pyvesync import VeSync
from pyvesync.vesyncbulb import VeSyncBulb
from pyvesync.vesyncfan import VeSyncAirBypass
from pyvesync.vesyncoutlet import VeSyncOutlet
from pyvesync.vesyncswitch import VeSyncSwitch
from homeassistant.components.vesync import DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from tests.common import MockConfigEntry
@pytest.fixture(name="config_entry")
def config_entry_fixture(hass: HomeAssistant, config) -> ConfigEntry:
"""Create a mock VeSync config entry."""
entry = MockConfigEntry(
title="VeSync",
domain=DOMAIN,
data=config[DOMAIN],
)
entry.add_to_hass(hass)
return entry
@pytest.fixture(name="config")
def config_fixture() -> ConfigType:
"""Create hass config fixture."""
return {DOMAIN: {CONF_USERNAME: "user", CONF_PASSWORD: "pass"}}
@pytest.fixture(name="manager")
def manager_fixture() -> VeSync:
"""Create a mock VeSync manager fixture."""
outlets = []
switches = []
fans = []
bulbs = []
mock_vesync = Mock(VeSync)
mock_vesync.login = Mock(return_value=True)
mock_vesync.update = Mock()
mock_vesync.outlets = outlets
mock_vesync.switches = switches
mock_vesync.fans = fans
mock_vesync.bulbs = bulbs
mock_vesync._dev_list = {
"fans": fans,
"outlets": outlets,
"switches": switches,
"bulbs": bulbs,
}
mock_vesync.account_id = "account_id"
mock_vesync.time_zone = "America/New_York"
mock = Mock(return_value=mock_vesync)
with patch("homeassistant.components.vesync.VeSync", new=mock):
yield mock_vesync
@pytest.fixture(name="fan")
def fan_fixture():
"""Create a mock VeSync fan fixture."""
mock_fixture = Mock(VeSyncAirBypass)
return mock_fixture
@pytest.fixture(name="bulb")
def bulb_fixture():
"""Create a mock VeSync bulb fixture."""
mock_fixture = Mock(VeSyncBulb)
return mock_fixture
@pytest.fixture(name="switch")
def switch_fixture():
"""Create a mock VeSync switch fixture."""
mock_fixture = Mock(VeSyncSwitch)
mock_fixture.is_dimmable = Mock(return_value=False)
return mock_fixture
@pytest.fixture(name="dimmable_switch")
def dimmable_switch_fixture():
"""Create a mock VeSync switch fixture."""
mock_fixture = Mock(VeSyncSwitch)
mock_fixture.is_dimmable = Mock(return_value=True)
return mock_fixture
@pytest.fixture(name="outlet")
def outlet_fixture():
"""Create a mock VeSync outlet fixture."""
mock_fixture = Mock(VeSyncOutlet)
return mock_fixture

View file

@ -0,0 +1,15 @@
{
"traceId": "0000000000",
"code": 0,
"msg": "request success",
"module": null,
"stacktrace": null,
"result": {
"traceId": "0000000000",
"code": 0,
"result": {
"enabled": false,
"mode": "humidity"
}
}
}

View file

@ -0,0 +1,27 @@
{
"traceId": "0000000000",
"code": 0,
"msg": "request success",
"module": null,
"stacktrace": null,
"result": {
"traceId": "0000000000",
"code": 0,
"result": {
"enabled": false,
"mist_virtual_level": 9,
"mist_level": 3,
"mode": "humidity",
"water_lacks": false,
"water_tank_lifted": false,
"humidity": 35,
"humidity_high": false,
"display": false,
"warm_enabled": false,
"warm_level": 0,
"automatic_stop_reach_target": true,
"configuration": { "auto_target_humidity": 60, "display": true },
"extension": { "schedule_count": 0, "timer_remain": 0 }
}
}
}

View file

@ -0,0 +1,11 @@
{
"traceId": "0000000000",
"code": 0,
"msg": "request success",
"result": {
"total": 1,
"pageSize": 100,
"pageNo": 1,
"list": []
}
}

View file

@ -0,0 +1,37 @@
{
"traceId": "0000000000",
"code": 0,
"msg": "request success",
"result": {
"total": 1,
"pageSize": 100,
"pageNo": 1,
"list": [
{
"deviceRegion": "US",
"isOwner": true,
"authKey": null,
"deviceName": "Fan",
"deviceImg": "",
"cid": "abcdefghabcdefghabcdefghabcdefgh",
"deviceStatus": "off",
"connectionStatus": "online",
"connectionType": "WiFi+BTOnboarding+BTNotify",
"deviceType": "LV-PUR131S",
"type": "wifi-air",
"uuid": "00000000-1111-2222-3333-444444444444",
"configModule": "WFON_AHM_LV-PUR131S_US",
"macID": "00:00:00:00:00:00",
"mode": null,
"speed": null,
"currentFirmVersion": null,
"subDeviceNo": null,
"subDeviceType": null,
"deviceFirstSetupTime": "Jan 24, 2022 12:09:01 AM",
"subDeviceList": null,
"extension": null,
"deviceProp": null
}
]
}
}

View file

@ -0,0 +1,37 @@
{
"traceId": "0000000000",
"code": 0,
"msg": "request success",
"result": {
"total": 1,
"pageSize": 100,
"pageNo": 1,
"list": [
{
"deviceRegion": "US",
"isOwner": true,
"authKey": null,
"deviceName": "Humidifier",
"deviceImg": "https://image.vesync.com/defaultImages/LV_600S_Series/icon_lv600s_humidifier_160.png",
"cid": "abcdefghabcdefghabcdefghabcdefgh",
"deviceStatus": "off",
"connectionStatus": "online",
"connectionType": "WiFi+BTOnboarding+BTNotify",
"deviceType": "LUH-A602S-WUS",
"type": "wifi-air",
"uuid": "00000000-1111-2222-3333-444444444444",
"configModule": "WFON_AHM_LUH-A602S-WUS_US",
"macID": "00:00:00:00:00:00",
"mode": null,
"speed": null,
"currentFirmVersion": null,
"subDeviceNo": null,
"subDeviceType": null,
"deviceFirstSetupTime": "Jan 24, 2022 12:09:01 AM",
"subDeviceList": null,
"extension": null,
"deviceProp": null
}
]
}
}

View file

@ -0,0 +1,9 @@
{
"traceId": "0000000000",
"code": 0,
"msg": "request success",
"result": {
"accountID": "9999999",
"token": "TOKEN"
}
}

View file

@ -0,0 +1,272 @@
# serializer version: 1
# name: test_async_get_config_entry_diagnostics__no_devices
dict({
'devices': dict({
'bulbs': list([
]),
'fans': list([
]),
'outlets': list([
]),
'switches': list([
]),
}),
'vesync': dict({
'bulb_count': 0,
'fan_count': 0,
'outlets_count': 0,
'switch_count': 0,
'timezone': 'US/Pacific',
}),
})
# ---
# name: test_async_get_config_entry_diagnostics__single_humidifier
dict({
'devices': dict({
'bulbs': list([
]),
'fans': list([
dict({
'_api_modes': list([
'getHumidifierStatus',
'setAutomaticStop',
'setSwitch',
'setNightLightBrightness',
'setVirtualLevel',
'setTargetHumidity',
'setHumidityMode',
'setDisplay',
'setLevel',
]),
'cid': 'abcdefghabcdefghabcdefghabcdefgh',
'config': dict({
'auto_target_humidity': 60,
'automatic_stop': True,
'display': True,
}),
'config_dict': dict({
'features': list([
'warm_mist',
'nightlight',
]),
'mist_levels': list([
1,
2,
3,
4,
5,
6,
7,
8,
9,
]),
'mist_modes': list([
'humidity',
'sleep',
'manual',
]),
'models': list([
'LUH-A602S-WUSR',
'LUH-A602S-WUS',
'LUH-A602S-WEUR',
'LUH-A602S-WEU',
'LUH-A602S-WJP',
]),
'module': 'VeSyncHumid200300S',
'warm_mist_levels': list([
0,
1,
2,
3,
]),
}),
'config_module': 'WFON_AHM_LUH-A602S-WUS_US',
'connection_status': 'online',
'connection_type': 'WiFi+BTOnboarding+BTNotify',
'current_firm_version': None,
'details': dict({
'automatic_stop_reach_target': True,
'display': False,
'humidity': 35,
'humidity_high': False,
'mist_level': 3,
'mist_virtual_level': 9,
'mode': 'humidity',
'night_light_brightness': 0,
'warm_mist_enabled': False,
'warm_mist_level': 0,
'water_lacks': False,
'water_tank_lifted': False,
}),
'device_image': 'https://image.vesync.com/defaultImages/LV_600S_Series/icon_lv600s_humidifier_160.png',
'device_name': 'Humidifier',
'device_status': 'off',
'device_type': 'LUH-A602S-WUS',
'enabled': False,
'extension': None,
'features': list([
'warm_mist',
'nightlight',
]),
'mac_id': '**REDACTED**',
'manager': '**REDACTED**',
'mist_levels': list([
1,
2,
3,
4,
5,
6,
7,
8,
9,
]),
'mist_modes': list([
'humidity',
'sleep',
'manual',
]),
'mode': None,
'night_light': True,
'speed': None,
'sub_device_no': None,
'type': 'wifi-air',
'uuid': '**REDACTED**',
'warm_mist_feature': True,
'warm_mist_levels': list([
0,
1,
2,
3,
]),
}),
]),
'outlets': list([
]),
'switches': list([
]),
}),
'vesync': dict({
'bulb_count': 0,
'fan_count': 1,
'outlets_count': 0,
'switch_count': 0,
'timezone': 'US/Pacific',
}),
})
# ---
# name: test_async_get_device_diagnostics__single_fan
dict({
'cid': 'abcdefghabcdefghabcdefghabcdefgh',
'config': dict({
}),
'config_module': 'WFON_AHM_LV-PUR131S_US',
'connection_status': 'unknown',
'connection_type': 'WiFi+BTOnboarding+BTNotify',
'current_firm_version': None,
'details': dict({
'active_time': 0,
'air_quality': 'unknown',
'filter_life': dict({
}),
'level': 0,
'screen_status': 'unknown',
}),
'device_image': '',
'device_name': 'Fan',
'device_status': 'unknown',
'device_type': 'LV-PUR131S',
'extension': None,
'home_assistant': dict({
'disabled': False,
'disabled_by': None,
'entities': list([
dict({
'device_class': None,
'disabled': False,
'disabled_by': None,
'domain': 'fan',
'entity_category': None,
'entity_id': 'fan.fan',
'icon': None,
'name': None,
'original_device_class': None,
'original_icon': None,
'original_name': 'Fan',
'state': dict({
'attributes': dict({
'friendly_name': 'Fan',
'preset_modes': list([
'auto',
'sleep',
]),
'supported_features': 1,
}),
'entity_id': 'fan.fan',
'last_changed': str,
'last_updated': str,
'state': 'unavailable',
}),
'unit_of_measurement': None,
}),
dict({
'device_class': None,
'disabled': False,
'disabled_by': None,
'domain': 'sensor',
'entity_category': 'diagnostic',
'entity_id': 'sensor.fan_filter_life',
'icon': None,
'name': None,
'original_device_class': None,
'original_icon': None,
'original_name': 'Fan Filter Life',
'state': dict({
'attributes': dict({
'friendly_name': 'Fan Filter Life',
'state_class': 'measurement',
'unit_of_measurement': '%',
}),
'entity_id': 'sensor.fan_filter_life',
'last_changed': str,
'last_updated': str,
'state': 'unavailable',
}),
'unit_of_measurement': '%',
}),
dict({
'device_class': None,
'disabled': False,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.fan_air_quality',
'icon': None,
'name': None,
'original_device_class': None,
'original_icon': None,
'original_name': 'Fan Air Quality',
'state': dict({
'attributes': dict({
'friendly_name': 'Fan Air Quality',
}),
'entity_id': 'sensor.fan_air_quality',
'last_changed': str,
'last_updated': str,
'state': 'unavailable',
}),
'unit_of_measurement': None,
}),
]),
'name': 'Fan',
'name_by_user': None,
}),
'mac_id': '**REDACTED**',
'manager': '**REDACTED**',
'mode': None,
'speed': None,
'sub_device_no': None,
'type': 'wifi-air',
'uuid': '**REDACTED**',
})
# ---

View file

@ -0,0 +1,99 @@
"""Tests for the diagnostics data provided by the VeSync integration."""
from unittest.mock import patch
from pyvesync.helpers import Helpers
from syrupy import SnapshotAssertion
from syrupy.matchers import path_type
from homeassistant.components.vesync.const import DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_setup_component
from .common import (
call_api_side_effect__no_devices,
call_api_side_effect__single_fan,
call_api_side_effect__single_humidifier,
)
from tests.components.diagnostics import (
get_diagnostics_for_config_entry,
get_diagnostics_for_device,
)
from tests.typing import ClientSessionGenerator
async def test_async_get_config_entry_diagnostics__no_devices(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
config_entry: ConfigEntry,
config: ConfigType,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics for config entry."""
with patch.object(Helpers, "call_api") as call_api:
call_api.side_effect = call_api_side_effect__no_devices
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
assert isinstance(diag, dict)
assert diag == snapshot
async def test_async_get_config_entry_diagnostics__single_humidifier(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
config_entry: ConfigEntry,
config: ConfigType,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics for config entry."""
with patch.object(Helpers, "call_api") as call_api:
call_api.side_effect = call_api_side_effect__single_humidifier
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
assert isinstance(diag, dict)
assert diag == snapshot
async def test_async_get_device_diagnostics__single_fan(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
config_entry: ConfigEntry,
config: ConfigType,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics for config entry."""
with patch.object(Helpers, "call_api") as call_api:
call_api.side_effect = call_api_side_effect__single_fan
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
device_registry = dr.async_get(hass)
device = device_registry.async_get_device(
identifiers={(DOMAIN, "abcdefghabcdefghabcdefghabcdefgh")},
)
assert device is not None
diag = await get_diagnostics_for_device(hass, hass_client, config_entry, device)
assert isinstance(diag, dict)
assert diag == snapshot(
matcher=path_type(
{
"home_assistant.entities.0.state.last_changed": (str,),
"home_assistant.entities.0.state.last_updated": (str,),
"home_assistant.entities.1.state.last_changed": (str,),
"home_assistant.entities.1.state.last_updated": (str,),
"home_assistant.entities.2.state.last_changed": (str,),
"home_assistant.entities.2.state.last_updated": (str,),
}
)
)

View file

@ -0,0 +1,103 @@
"""Tests for the init module."""
from unittest.mock import Mock, patch
import pytest
from pyvesync import VeSync
from homeassistant.components.vesync import async_setup_entry
from homeassistant.components.vesync.const import (
DOMAIN,
VS_FANS,
VS_LIGHTS,
VS_MANAGER,
VS_SENSORS,
VS_SWITCHES,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
async def test_async_setup_entry__not_login(
hass: HomeAssistant,
config_entry: ConfigEntry,
manager: VeSync,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test setup does not create config entry when not logged in."""
manager.login = Mock(return_value=False)
with patch.object(
hass.config_entries, "async_forward_entry_setups"
) as setups_mock, patch.object(
hass.config_entries, "async_forward_entry_setup"
) as setup_mock, patch(
"homeassistant.components.vesync.async_process_devices"
) as process_mock, patch.object(
hass.services, "async_register"
) as register_mock:
assert not await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()
assert setups_mock.call_count == 0
assert setup_mock.call_count == 0
assert process_mock.call_count == 0
assert register_mock.call_count == 0
assert manager.login.call_count == 1
assert DOMAIN not in hass.data
assert "Unable to login to the VeSync server" in caplog.text
async def test_async_setup_entry__no_devices(
hass: HomeAssistant, config_entry: ConfigEntry, manager: VeSync
) -> None:
"""Test setup connects to vesync and creates empty config when no devices."""
with patch.object(
hass.config_entries, "async_forward_entry_setups"
) as setups_mock, patch.object(
hass.config_entries, "async_forward_entry_setup"
) as setup_mock:
assert await async_setup_entry(hass, config_entry)
# Assert platforms loaded
await hass.async_block_till_done()
assert setups_mock.call_count == 1
assert setups_mock.call_args.args[0] == config_entry
assert setups_mock.call_args.args[1] == []
assert setup_mock.call_count == 0
assert manager.login.call_count == 1
assert hass.data[DOMAIN][VS_MANAGER] == manager
assert not hass.data[DOMAIN][VS_SWITCHES]
assert not hass.data[DOMAIN][VS_FANS]
assert not hass.data[DOMAIN][VS_LIGHTS]
assert not hass.data[DOMAIN][VS_SENSORS]
async def test_async_setup_entry__loads_fans(
hass: HomeAssistant, config_entry: ConfigEntry, manager: VeSync, fan
) -> None:
"""Test setup connects to vesync and loads fan platform."""
fans = [fan]
manager.fans = fans
manager._dev_list = {
"fans": fans,
}
with patch.object(
hass.config_entries, "async_forward_entry_setups"
) as setups_mock, patch.object(
hass.config_entries, "async_forward_entry_setup"
) as setup_mock:
assert await async_setup_entry(hass, config_entry)
# Assert platforms loaded
await hass.async_block_till_done()
assert setups_mock.call_count == 1
assert setups_mock.call_args.args[0] == config_entry
assert setups_mock.call_args.args[1] == [Platform.FAN, Platform.SENSOR]
assert setup_mock.call_count == 0
assert manager.login.call_count == 1
assert hass.data[DOMAIN][VS_MANAGER] == manager
assert not hass.data[DOMAIN][VS_SWITCHES]
assert hass.data[DOMAIN][VS_FANS] == [fan]
assert not hass.data[DOMAIN][VS_LIGHTS]
assert hass.data[DOMAIN][VS_SENSORS] == [fan]