mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
Add coordinator to vesync (#134087)
This commit is contained in:
parent
fd12ae2ccd
commit
add401ffcf
@ -13,6 +13,7 @@ from .common import async_process_devices
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE_DEVS,
|
||||
VS_COORDINATOR,
|
||||
VS_DISCOVERY,
|
||||
VS_FANS,
|
||||
VS_LIGHTS,
|
||||
@ -20,6 +21,7 @@ from .const import (
|
||||
VS_SENSORS,
|
||||
VS_SWITCHES,
|
||||
)
|
||||
from .coordinator import VeSyncDataCoordinator
|
||||
|
||||
PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
@ -48,6 +50,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
hass.data[DOMAIN] = {}
|
||||
hass.data[DOMAIN][VS_MANAGER] = manager
|
||||
|
||||
coordinator = VeSyncDataCoordinator(hass, manager)
|
||||
|
||||
# Store coordinator at domain level since only single integration instance is permitted.
|
||||
hass.data[DOMAIN][VS_COORDINATOR] = coordinator
|
||||
|
||||
switches = hass.data[DOMAIN][VS_SWITCHES] = []
|
||||
fans = hass.data[DOMAIN][VS_FANS] = []
|
||||
lights = hass.data[DOMAIN][VS_LIGHTS] = []
|
||||
|
@ -4,10 +4,25 @@ DOMAIN = "vesync"
|
||||
VS_DISCOVERY = "vesync_discovery_{}"
|
||||
SERVICE_UPDATE_DEVS = "update_devices"
|
||||
|
||||
UPDATE_INTERVAL = 60
|
||||
"""
|
||||
Update interval for DataCoordinator.
|
||||
|
||||
The vesync daily quota formula is 3200 + 1500 * device_count.
|
||||
|
||||
An interval of 60 seconds amounts 1440 calls/day which
|
||||
would be below the 4700 daily quota. For 2 devices, the
|
||||
total would be 2880.
|
||||
|
||||
Using 30 seconds interval gives 8640 for 3 devices which
|
||||
exceeds the quota of 7700.
|
||||
"""
|
||||
|
||||
VS_SWITCHES = "switches"
|
||||
VS_FANS = "fans"
|
||||
VS_LIGHTS = "lights"
|
||||
VS_SENSORS = "sensors"
|
||||
VS_COORDINATOR = "coordinator"
|
||||
VS_MANAGER = "manager"
|
||||
|
||||
DEV_TYPE_TO_HA = {
|
||||
|
43
homeassistant/components/vesync/coordinator.py
Normal file
43
homeassistant/components/vesync/coordinator.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""Class to manage VeSync data updates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyvesync import VeSync
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import UPDATE_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VeSyncDataCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Class representing data coordinator for VeSync devices."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, manager: VeSync) -> None:
|
||||
"""Initialize."""
|
||||
self._manager = manager
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="VeSyncDataCoordinator",
|
||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint."""
|
||||
|
||||
return await self.hass.async_add_executor_job(self.update_data_all)
|
||||
|
||||
def update_data_all(self) -> None:
|
||||
"""Update all the devices."""
|
||||
|
||||
# Using `update_all_devices` instead of `update` to avoid fetching device list every time.
|
||||
self._manager.update_all_devices()
|
||||
# Vesync updates energy on applicable devices every 6 hours
|
||||
self._manager.update_energy()
|
@ -5,18 +5,23 @@ from typing import Any
|
||||
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, ToggleEntity
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import VeSyncDataCoordinator
|
||||
|
||||
|
||||
class VeSyncBaseEntity(Entity):
|
||||
class VeSyncBaseEntity(CoordinatorEntity[VeSyncDataCoordinator]):
|
||||
"""Base class for VeSync Entity Representations."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, device: VeSyncBaseDevice) -> None:
|
||||
def __init__(
|
||||
self, device: VeSyncBaseDevice, coordinator: VeSyncDataCoordinator
|
||||
) -> None:
|
||||
"""Initialize the VeSync device."""
|
||||
super().__init__(coordinator)
|
||||
self.device = device
|
||||
self._attr_unique_id = self.base_unique_id
|
||||
|
||||
@ -46,10 +51,6 @@ class VeSyncBaseEntity(Entity):
|
||||
sw_version=self.device.current_firm_version,
|
||||
)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update vesync device."""
|
||||
self.device.update()
|
||||
|
||||
|
||||
class VeSyncDevice(VeSyncBaseEntity, ToggleEntity):
|
||||
"""Base class for VeSync Device Representations."""
|
||||
|
@ -6,6 +6,8 @@ import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
|
||||
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@ -17,7 +19,15 @@ from homeassistant.util.percentage import (
|
||||
)
|
||||
from homeassistant.util.scaling import int_states_in_range
|
||||
|
||||
from .const import DEV_TYPE_TO_HA, DOMAIN, SKU_TO_BASE_DEVICE, VS_DISCOVERY, VS_FANS
|
||||
from .const import (
|
||||
DEV_TYPE_TO_HA,
|
||||
DOMAIN,
|
||||
SKU_TO_BASE_DEVICE,
|
||||
VS_COORDINATOR,
|
||||
VS_DISCOVERY,
|
||||
VS_FANS,
|
||||
)
|
||||
from .coordinator import VeSyncDataCoordinator
|
||||
from .entity import VeSyncDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -56,25 +66,31 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the VeSync fan platform."""
|
||||
|
||||
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
|
||||
|
||||
@callback
|
||||
def discover(devices):
|
||||
"""Add new devices to platform."""
|
||||
_setup_entities(devices, async_add_entities)
|
||||
_setup_entities(devices, async_add_entities, coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_FANS), discover)
|
||||
)
|
||||
|
||||
_setup_entities(hass.data[DOMAIN][VS_FANS], async_add_entities)
|
||||
_setup_entities(hass.data[DOMAIN][VS_FANS], async_add_entities, coordinator)
|
||||
|
||||
|
||||
@callback
|
||||
def _setup_entities(devices, async_add_entities):
|
||||
def _setup_entities(
|
||||
devices: list[VeSyncBaseDevice],
|
||||
async_add_entities,
|
||||
coordinator: VeSyncDataCoordinator,
|
||||
):
|
||||
"""Check if device is online and add entity."""
|
||||
entities = []
|
||||
for dev in devices:
|
||||
if DEV_TYPE_TO_HA.get(SKU_TO_BASE_DEVICE.get(dev.device_type)) == "fan":
|
||||
entities.append(VeSyncFanHA(dev))
|
||||
if DEV_TYPE_TO_HA.get(SKU_TO_BASE_DEVICE.get(dev.device_type, "")) == "fan":
|
||||
entities.append(VeSyncFanHA(dev, coordinator))
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"%s - Unknown device type - %s", dev.device_name, dev.device_type
|
||||
@ -96,9 +112,9 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
|
||||
_attr_name = None
|
||||
_attr_translation_key = "vesync"
|
||||
|
||||
def __init__(self, fan) -> None:
|
||||
def __init__(self, fan, coordinator: VeSyncDataCoordinator) -> None:
|
||||
"""Initialize the VeSync fan device."""
|
||||
super().__init__(fan)
|
||||
super().__init__(fan, coordinator)
|
||||
self.smartfan = fan
|
||||
|
||||
@property
|
||||
|
@ -3,6 +3,8 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
@ -15,7 +17,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_LIGHTS
|
||||
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_COORDINATOR, VS_DISCOVERY, VS_LIGHTS
|
||||
from .coordinator import VeSyncDataCoordinator
|
||||
from .entity import VeSyncDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -30,27 +33,33 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up lights."""
|
||||
|
||||
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
|
||||
|
||||
@callback
|
||||
def discover(devices):
|
||||
"""Add new devices to platform."""
|
||||
_setup_entities(devices, async_add_entities)
|
||||
_setup_entities(devices, async_add_entities, coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_LIGHTS), discover)
|
||||
)
|
||||
|
||||
_setup_entities(hass.data[DOMAIN][VS_LIGHTS], async_add_entities)
|
||||
_setup_entities(hass.data[DOMAIN][VS_LIGHTS], async_add_entities, coordinator)
|
||||
|
||||
|
||||
@callback
|
||||
def _setup_entities(devices, async_add_entities):
|
||||
def _setup_entities(
|
||||
devices: list[VeSyncBaseDevice],
|
||||
async_add_entities,
|
||||
coordinator: VeSyncDataCoordinator,
|
||||
):
|
||||
"""Check if device is online and add entity."""
|
||||
entities = []
|
||||
entities: list[VeSyncBaseLight] = []
|
||||
for dev in devices:
|
||||
if DEV_TYPE_TO_HA.get(dev.device_type) in ("walldimmer", "bulb-dimmable"):
|
||||
entities.append(VeSyncDimmableLightHA(dev))
|
||||
entities.append(VeSyncDimmableLightHA(dev, coordinator))
|
||||
elif DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white",):
|
||||
entities.append(VeSyncTunableWhiteLightHA(dev))
|
||||
entities.append(VeSyncTunableWhiteLightHA(dev, coordinator))
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - Unknown device type - %s", dev.device_name, dev.device_type
|
||||
|
@ -6,6 +6,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
|
||||
from pyvesync.vesyncfan import VeSyncAirBypass
|
||||
from pyvesync.vesyncoutlet import VeSyncOutlet
|
||||
from pyvesync.vesyncswitch import VeSyncSwitch
|
||||
@ -30,7 +31,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DEV_TYPE_TO_HA, DOMAIN, SKU_TO_BASE_DEVICE, VS_DISCOVERY, VS_SENSORS
|
||||
from .const import (
|
||||
DEV_TYPE_TO_HA,
|
||||
DOMAIN,
|
||||
SKU_TO_BASE_DEVICE,
|
||||
VS_COORDINATOR,
|
||||
VS_DISCOVERY,
|
||||
VS_SENSORS,
|
||||
)
|
||||
from .coordinator import VeSyncDataCoordinator
|
||||
from .entity import VeSyncBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -187,24 +196,31 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up switches."""
|
||||
|
||||
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
|
||||
|
||||
@callback
|
||||
def discover(devices):
|
||||
"""Add new devices to platform."""
|
||||
_setup_entities(devices, async_add_entities)
|
||||
_setup_entities(devices, async_add_entities, coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_SENSORS), discover)
|
||||
)
|
||||
|
||||
_setup_entities(hass.data[DOMAIN][VS_SENSORS], async_add_entities)
|
||||
_setup_entities(hass.data[DOMAIN][VS_SENSORS], async_add_entities, coordinator)
|
||||
|
||||
|
||||
@callback
|
||||
def _setup_entities(devices, async_add_entities):
|
||||
def _setup_entities(
|
||||
devices: list[VeSyncBaseDevice],
|
||||
async_add_entities,
|
||||
coordinator: VeSyncDataCoordinator,
|
||||
):
|
||||
"""Check if device is online and add entity."""
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
VeSyncSensorEntity(dev, description)
|
||||
VeSyncSensorEntity(dev, description, coordinator)
|
||||
for dev in devices
|
||||
for description in SENSORS
|
||||
if description.exists_fn(dev)
|
||||
@ -222,9 +238,10 @@ class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity):
|
||||
self,
|
||||
device: VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch,
|
||||
description: VeSyncSensorEntityDescription,
|
||||
coordinator,
|
||||
) -> None:
|
||||
"""Initialize the VeSync outlet device."""
|
||||
super().__init__(device)
|
||||
super().__init__(device, coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}-{description.key}"
|
||||
|
||||
|
@ -3,13 +3,16 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_SWITCHES
|
||||
from .const import DEV_TYPE_TO_HA, DOMAIN, VS_COORDINATOR, VS_DISCOVERY, VS_SWITCHES
|
||||
from .coordinator import VeSyncDataCoordinator
|
||||
from .entity import VeSyncDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -22,27 +25,33 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up switches."""
|
||||
|
||||
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
|
||||
|
||||
@callback
|
||||
def discover(devices):
|
||||
"""Add new devices to platform."""
|
||||
_setup_entities(devices, async_add_entities)
|
||||
_setup_entities(devices, async_add_entities, coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_SWITCHES), discover)
|
||||
)
|
||||
|
||||
_setup_entities(hass.data[DOMAIN][VS_SWITCHES], async_add_entities)
|
||||
_setup_entities(hass.data[DOMAIN][VS_SWITCHES], async_add_entities, coordinator)
|
||||
|
||||
|
||||
@callback
|
||||
def _setup_entities(devices, async_add_entities):
|
||||
def _setup_entities(
|
||||
devices: list[VeSyncBaseDevice],
|
||||
async_add_entities,
|
||||
coordinator: VeSyncDataCoordinator,
|
||||
):
|
||||
"""Check if device is online and add entity."""
|
||||
entities = []
|
||||
entities: list[VeSyncBaseSwitch] = []
|
||||
for dev in devices:
|
||||
if DEV_TYPE_TO_HA.get(dev.device_type) == "outlet":
|
||||
entities.append(VeSyncSwitchHA(dev))
|
||||
entities.append(VeSyncSwitchHA(dev, coordinator))
|
||||
elif DEV_TYPE_TO_HA.get(dev.device_type) == "switch":
|
||||
entities.append(VeSyncLightSwitch(dev))
|
||||
entities.append(VeSyncLightSwitch(dev, coordinator))
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"%s - Unknown device type - %s", dev.device_name, dev.device_type
|
||||
@ -65,21 +74,16 @@ class VeSyncBaseSwitch(VeSyncDevice, SwitchEntity):
|
||||
class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity):
|
||||
"""Representation of a VeSync switch."""
|
||||
|
||||
def __init__(self, plug):
|
||||
def __init__(self, plug, coordinator: VeSyncDataCoordinator) -> None:
|
||||
"""Initialize the VeSync switch device."""
|
||||
super().__init__(plug)
|
||||
super().__init__(plug, coordinator)
|
||||
self.smartplug = plug
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update outlet details and energy usage."""
|
||||
self.smartplug.update()
|
||||
self.smartplug.update_energy()
|
||||
|
||||
|
||||
class VeSyncLightSwitch(VeSyncBaseSwitch, SwitchEntity):
|
||||
"""Handle representation of VeSync Light Switch."""
|
||||
|
||||
def __init__(self, switch):
|
||||
def __init__(self, switch, coordinator: VeSyncDataCoordinator) -> None:
|
||||
"""Initialize Light Switch device class."""
|
||||
super().__init__(switch)
|
||||
super().__init__(switch, coordinator)
|
||||
self.switch = switch
|
||||
|
@ -1,10 +1,12 @@
|
||||
"""Common methods used across tests for VeSync."""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.vesync.const import DOMAIN
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
|
||||
from tests.common import load_fixture, load_json_object_fixture
|
||||
|
||||
@ -26,7 +28,7 @@ DEVICE_FIXTURES: dict[str, list[tuple[str, str, str]]] = {
|
||||
("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json")
|
||||
],
|
||||
"Air Purifier 400s": [
|
||||
("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json")
|
||||
("post", "/cloud/v2/deviceManaged/bypassV2", "air-purifier-400s-detail.json")
|
||||
],
|
||||
"Air Purifier 600s": [
|
||||
("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json")
|
||||
@ -37,7 +39,10 @@ DEVICE_FIXTURES: dict[str, list[tuple[str, str, str]]] = {
|
||||
"Temperature Light": [
|
||||
("post", "/cloud/v1/deviceManaged/bypass", "device-detail.json")
|
||||
],
|
||||
"Outlet": [("get", "/v1/device/outlet/detail", "outlet-detail.json")],
|
||||
"Outlet": [
|
||||
("get", "/v1/device/outlet/detail", "outlet-detail.json"),
|
||||
("get", "/v1/device/outlet/energy/week", "outlet-energy-week.json"),
|
||||
],
|
||||
"Wall Switch": [
|
||||
("post", "/inwallswitch/v1/device/devicedetail", "device-detail.json")
|
||||
],
|
||||
@ -71,6 +76,99 @@ def mock_devices_response(
|
||||
)
|
||||
|
||||
|
||||
def mock_multiple_device_responses(
|
||||
requests_mock: requests_mock.Mocker, device_names: list[str]
|
||||
) -> None:
|
||||
"""Build a response for the Helpers.call_api method for multiple devices."""
|
||||
device_list = [
|
||||
device
|
||||
for device in ALL_DEVICES["result"]["list"]
|
||||
if device["deviceName"] in device_names
|
||||
]
|
||||
|
||||
requests_mock.post(
|
||||
"https://smartapi.vesync.com/cloud/v1/deviceManaged/devices",
|
||||
json={"code": 0, "result": {"list": device_list}},
|
||||
)
|
||||
requests_mock.post(
|
||||
"https://smartapi.vesync.com/cloud/v1/user/login",
|
||||
json=load_json_object_fixture("vesync-login.json", DOMAIN),
|
||||
)
|
||||
for device_name in device_names:
|
||||
for fixture in DEVICE_FIXTURES[device_name]:
|
||||
requests_mock.request(
|
||||
fixture[0],
|
||||
f"https://smartapi.vesync.com{fixture[1]}",
|
||||
json=load_json_object_fixture(fixture[2], DOMAIN),
|
||||
)
|
||||
|
||||
|
||||
def mock_air_purifier_400s_update_response(requests_mock: requests_mock.Mocker) -> None:
|
||||
"""Build a response for the Helpers.call_api method for air_purifier_400s with updated data."""
|
||||
|
||||
device_name = "Air Purifier 400s"
|
||||
for fixture in DEVICE_FIXTURES[device_name]:
|
||||
requests_mock.request(
|
||||
fixture[0],
|
||||
f"https://smartapi.vesync.com{fixture[1]}",
|
||||
json=load_json_object_fixture(
|
||||
"air-purifier-400s-detail-updated.json", DOMAIN
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def mock_device_response(
|
||||
requests_mock: requests_mock.Mocker, device_name: str, override: Any
|
||||
) -> None:
|
||||
"""Build a response for the Helpers.call_api method with updated data."""
|
||||
|
||||
def load_and_merge(source: str) -> JsonObjectType:
|
||||
json = load_json_object_fixture(source, DOMAIN)
|
||||
|
||||
if override:
|
||||
json.update(override)
|
||||
|
||||
return json
|
||||
|
||||
fixtures = DEVICE_FIXTURES[device_name]
|
||||
|
||||
# The first item contain basic device details
|
||||
if len(fixtures) > 0:
|
||||
item = fixtures[0]
|
||||
|
||||
requests_mock.request(
|
||||
item[0],
|
||||
f"https://smartapi.vesync.com{item[1]}",
|
||||
json=load_and_merge(item[2]),
|
||||
)
|
||||
|
||||
|
||||
def mock_outlet_energy_response(
|
||||
requests_mock: requests_mock.Mocker, device_name: str, override: Any
|
||||
) -> None:
|
||||
"""Build a response for the Helpers.call_api energy request with updated data."""
|
||||
|
||||
def load_and_merge(source: str) -> JsonObjectType:
|
||||
json = load_json_object_fixture(source, DOMAIN)
|
||||
|
||||
if override:
|
||||
json.update(override)
|
||||
|
||||
return json
|
||||
|
||||
fixtures = DEVICE_FIXTURES[device_name]
|
||||
|
||||
# The 2nd item contain energy details
|
||||
if len(fixtures) > 1:
|
||||
item = fixtures[1]
|
||||
|
||||
requests_mock.request(
|
||||
item[0],
|
||||
f"https://smartapi.vesync.com{item[1]}",
|
||||
json=load_and_merge(item[2]),
|
||||
)
|
||||
|
||||
|
||||
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":
|
||||
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"code": 0,
|
||||
"brightNess": "50",
|
||||
"result": {
|
||||
"light": {
|
||||
"brightness": 50,
|
||||
"colorTempe": 5400
|
||||
},
|
||||
"result": {
|
||||
"brightness": 50,
|
||||
"red": 178.5,
|
||||
"green": 255,
|
||||
"blue": 25.5,
|
||||
"colorMode": "rgb",
|
||||
"humidity": 35,
|
||||
"mist_virtual_level": 6,
|
||||
"mode": "manual",
|
||||
"water_lacks": true,
|
||||
"water_tank_lifted": true,
|
||||
"automatic_stop_reach_target": true,
|
||||
"night_light_brightness": 10,
|
||||
"enabled": true,
|
||||
"filter_life": 99,
|
||||
"level": 1,
|
||||
"display": true,
|
||||
"display_forever": false,
|
||||
"child_lock": false,
|
||||
"night_light": "on",
|
||||
"air_quality": 15,
|
||||
"air_quality_value": 1,
|
||||
"configuration": {
|
||||
"auto_target_humidity": 40,
|
||||
"display": true,
|
||||
"automatic_stop": true
|
||||
}
|
||||
},
|
||||
"code": 0
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"code": 0,
|
||||
"brightNess": "50",
|
||||
"result": {
|
||||
"light": {
|
||||
"brightness": 50,
|
||||
"colorTempe": 5400
|
||||
},
|
||||
"result": {
|
||||
"brightness": 50,
|
||||
"red": 178.5,
|
||||
"green": 255,
|
||||
"blue": 25.5,
|
||||
"colorMode": "rgb",
|
||||
"humidity": 35,
|
||||
"mist_virtual_level": 6,
|
||||
"mode": "manual",
|
||||
"water_lacks": true,
|
||||
"water_tank_lifted": true,
|
||||
"automatic_stop_reach_target": true,
|
||||
"night_light_brightness": 10,
|
||||
"enabled": true,
|
||||
"filter_life": 99,
|
||||
"level": 1,
|
||||
"display": true,
|
||||
"display_forever": false,
|
||||
"child_lock": false,
|
||||
"night_light": "off",
|
||||
"air_quality": 5,
|
||||
"air_quality_value": 1,
|
||||
"configuration": {
|
||||
"auto_target_humidity": 40,
|
||||
"display": true,
|
||||
"automatic_stop": true
|
||||
}
|
||||
},
|
||||
"code": 0
|
||||
}
|
||||
}
|
7
tests/components/vesync/fixtures/outlet-energy-week.json
Normal file
7
tests/components/vesync/fixtures/outlet-energy-week.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"energyConsumptionOfToday": 1,
|
||||
"costPerKWH": 0.15,
|
||||
"maxEnergy": 6,
|
||||
"totalEnergy": 0,
|
||||
"currency": "$"
|
||||
}
|
92
tests/components/vesync/test_platform.py
Normal file
92
tests/components/vesync/test_platform.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""Tests for the coordinator."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.vesync.const import DOMAIN, UPDATE_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .common import (
|
||||
mock_air_purifier_400s_update_response,
|
||||
mock_device_response,
|
||||
mock_multiple_device_responses,
|
||||
mock_outlet_energy_response,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_entity_update(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
requests_mock: requests_mock.Mocker,
|
||||
) -> None:
|
||||
"""Test Vesync coordinator data update.
|
||||
|
||||
This test sets up a single device `Air Purifier 400s` and then updates it via the coordinator.
|
||||
"""
|
||||
|
||||
config_data = {CONF_PASSWORD: "username", CONF_USERNAME: "password"}
|
||||
config_entry = MockConfigEntry(
|
||||
data=config_data,
|
||||
domain=DOMAIN,
|
||||
unique_id="vesync_unique_id_1",
|
||||
entry_id="1",
|
||||
)
|
||||
|
||||
mock_multiple_device_responses(requests_mock, ["Air Purifier 400s", "Outlet"])
|
||||
|
||||
expected_entities = [
|
||||
# From "Air Purifier 400s"
|
||||
"fan.air_purifier_400s",
|
||||
"sensor.air_purifier_400s_filter_lifetime",
|
||||
"sensor.air_purifier_400s_air_quality",
|
||||
"sensor.air_purifier_400s_pm2_5",
|
||||
# From Outlet
|
||||
"switch.outlet",
|
||||
"sensor.outlet_current_power",
|
||||
"sensor.outlet_energy_use_today",
|
||||
"sensor.outlet_energy_use_weekly",
|
||||
"sensor.outlet_energy_use_monthly",
|
||||
"sensor.outlet_energy_use_yearly",
|
||||
"sensor.outlet_current_voltage",
|
||||
]
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
for entity_id in expected_entities:
|
||||
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
|
||||
|
||||
assert hass.states.get("sensor.air_purifier_400s_air_quality").state == "5"
|
||||
assert hass.states.get("sensor.outlet_current_voltage").state == "120.0"
|
||||
assert hass.states.get("sensor.outlet_energy_use_weekly").state == "0"
|
||||
|
||||
# Update the mock responses
|
||||
mock_air_purifier_400s_update_response(requests_mock)
|
||||
mock_outlet_energy_response(requests_mock, "Outlet", {"totalEnergy": 2.2})
|
||||
mock_device_response(requests_mock, "Outlet", {"voltage": 129})
|
||||
|
||||
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(True)
|
||||
|
||||
assert hass.states.get("sensor.air_purifier_400s_air_quality").state == "15"
|
||||
assert hass.states.get("sensor.outlet_current_voltage").state == "129.0"
|
||||
|
||||
# Test energy update
|
||||
# pyvesync only updates energy parameters once every 6 hours.
|
||||
freezer.tick(timedelta(hours=6))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(True)
|
||||
|
||||
assert hass.states.get("sensor.air_purifier_400s_air_quality").state == "15"
|
||||
assert hass.states.get("sensor.outlet_current_voltage").state == "129.0"
|
||||
assert hass.states.get("sensor.outlet_energy_use_weekly").state == "2.2"
|
Loading…
x
Reference in New Issue
Block a user