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:
parent
5b49648846
commit
09d0128601
13 changed files with 905 additions and 1 deletions
|
@ -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
|
||||
|
|
119
homeassistant/components/vesync/diagnostics.py
Normal file
119
homeassistant/components/vesync/diagnostics.py
Normal 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
|
72
tests/components/vesync/common.py
Normal file
72
tests/components/vesync/common.py
Normal 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}")
|
104
tests/components/vesync/conftest.py
Normal file
104
tests/components/vesync/conftest.py
Normal 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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"traceId": "0000000000",
|
||||
"code": 0,
|
||||
"msg": "request success",
|
||||
"result": {
|
||||
"total": 1,
|
||||
"pageSize": 100,
|
||||
"pageNo": 1,
|
||||
"list": []
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"traceId": "0000000000",
|
||||
"code": 0,
|
||||
"msg": "request success",
|
||||
"result": {
|
||||
"accountID": "9999999",
|
||||
"token": "TOKEN"
|
||||
}
|
||||
}
|
272
tests/components/vesync/snapshots/test_diagnostics.ambr
Normal file
272
tests/components/vesync/snapshots/test_diagnostics.ambr
Normal 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**',
|
||||
})
|
||||
# ---
|
99
tests/components/vesync/test_diagnostics.py
Normal file
99
tests/components/vesync/test_diagnostics.py
Normal 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,),
|
||||
}
|
||||
)
|
||||
)
|
103
tests/components/vesync/test_init.py
Normal file
103
tests/components/vesync/test_init.py
Normal 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]
|
Loading…
Add table
Reference in a new issue