Add coordinator to vesync (#134087)

This commit is contained in:
Indu Prakash 2025-01-03 04:33:16 -06:00 committed by GitHub
parent fd12ae2ccd
commit add401ffcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 433 additions and 46 deletions

View File

@ -13,6 +13,7 @@ from .common import async_process_devices
from .const import ( from .const import (
DOMAIN, DOMAIN,
SERVICE_UPDATE_DEVS, SERVICE_UPDATE_DEVS,
VS_COORDINATOR,
VS_DISCOVERY, VS_DISCOVERY,
VS_FANS, VS_FANS,
VS_LIGHTS, VS_LIGHTS,
@ -20,6 +21,7 @@ from .const import (
VS_SENSORS, VS_SENSORS,
VS_SWITCHES, VS_SWITCHES,
) )
from .coordinator import VeSyncDataCoordinator
PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] 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] = {}
hass.data[DOMAIN][VS_MANAGER] = manager 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] = [] switches = hass.data[DOMAIN][VS_SWITCHES] = []
fans = hass.data[DOMAIN][VS_FANS] = [] fans = hass.data[DOMAIN][VS_FANS] = []
lights = hass.data[DOMAIN][VS_LIGHTS] = [] lights = hass.data[DOMAIN][VS_LIGHTS] = []

View File

@ -4,10 +4,25 @@ DOMAIN = "vesync"
VS_DISCOVERY = "vesync_discovery_{}" VS_DISCOVERY = "vesync_discovery_{}"
SERVICE_UPDATE_DEVS = "update_devices" 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_SWITCHES = "switches"
VS_FANS = "fans" VS_FANS = "fans"
VS_LIGHTS = "lights" VS_LIGHTS = "lights"
VS_SENSORS = "sensors" VS_SENSORS = "sensors"
VS_COORDINATOR = "coordinator"
VS_MANAGER = "manager" VS_MANAGER = "manager"
DEV_TYPE_TO_HA = { DEV_TYPE_TO_HA = {

View 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()

View File

@ -5,18 +5,23 @@ from typing import Any
from pyvesync.vesyncbasedevice import VeSyncBaseDevice from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from homeassistant.helpers.device_registry import DeviceInfo 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 .const import DOMAIN
from .coordinator import VeSyncDataCoordinator
class VeSyncBaseEntity(Entity): class VeSyncBaseEntity(CoordinatorEntity[VeSyncDataCoordinator]):
"""Base class for VeSync Entity Representations.""" """Base class for VeSync Entity Representations."""
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__(self, device: VeSyncBaseDevice) -> None: def __init__(
self, device: VeSyncBaseDevice, coordinator: VeSyncDataCoordinator
) -> None:
"""Initialize the VeSync device.""" """Initialize the VeSync device."""
super().__init__(coordinator)
self.device = device self.device = device
self._attr_unique_id = self.base_unique_id self._attr_unique_id = self.base_unique_id
@ -46,10 +51,6 @@ class VeSyncBaseEntity(Entity):
sw_version=self.device.current_firm_version, sw_version=self.device.current_firm_version,
) )
def update(self) -> None:
"""Update vesync device."""
self.device.update()
class VeSyncDevice(VeSyncBaseEntity, ToggleEntity): class VeSyncDevice(VeSyncBaseEntity, ToggleEntity):
"""Base class for VeSync Device Representations.""" """Base class for VeSync Device Representations."""

View File

@ -6,6 +6,8 @@ import logging
import math import math
from typing import Any from typing import Any
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback 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 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 from .entity import VeSyncDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -56,25 +66,31 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the VeSync fan platform.""" """Set up the VeSync fan platform."""
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback @callback
def discover(devices): def discover(devices):
"""Add new devices to platform.""" """Add new devices to platform."""
_setup_entities(devices, async_add_entities) _setup_entities(devices, async_add_entities, coordinator)
config_entry.async_on_unload( config_entry.async_on_unload(
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_FANS), discover) 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 @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.""" """Check if device is online and add entity."""
entities = [] entities = []
for dev in devices: for dev in devices:
if DEV_TYPE_TO_HA.get(SKU_TO_BASE_DEVICE.get(dev.device_type)) == "fan": if DEV_TYPE_TO_HA.get(SKU_TO_BASE_DEVICE.get(dev.device_type, "")) == "fan":
entities.append(VeSyncFanHA(dev)) entities.append(VeSyncFanHA(dev, coordinator))
else: else:
_LOGGER.warning( _LOGGER.warning(
"%s - Unknown device type - %s", dev.device_name, dev.device_type "%s - Unknown device type - %s", dev.device_name, dev.device_type
@ -96,9 +112,9 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
_attr_name = None _attr_name = None
_attr_translation_key = "vesync" _attr_translation_key = "vesync"
def __init__(self, fan) -> None: def __init__(self, fan, coordinator: VeSyncDataCoordinator) -> None:
"""Initialize the VeSync fan device.""" """Initialize the VeSync fan device."""
super().__init__(fan) super().__init__(fan, coordinator)
self.smartfan = fan self.smartfan = fan
@property @property

View File

@ -3,6 +3,8 @@
import logging import logging
from typing import Any from typing import Any
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN, 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.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import color as color_util 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 from .entity import VeSyncDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,27 +33,33 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up lights.""" """Set up lights."""
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback @callback
def discover(devices): def discover(devices):
"""Add new devices to platform.""" """Add new devices to platform."""
_setup_entities(devices, async_add_entities) _setup_entities(devices, async_add_entities, coordinator)
config_entry.async_on_unload( config_entry.async_on_unload(
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_LIGHTS), discover) 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 @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.""" """Check if device is online and add entity."""
entities = [] entities: list[VeSyncBaseLight] = []
for dev in devices: for dev in devices:
if DEV_TYPE_TO_HA.get(dev.device_type) in ("walldimmer", "bulb-dimmable"): 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",): elif DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white",):
entities.append(VeSyncTunableWhiteLightHA(dev)) entities.append(VeSyncTunableWhiteLightHA(dev, coordinator))
else: else:
_LOGGER.debug( _LOGGER.debug(
"%s - Unknown device type - %s", dev.device_name, dev.device_type "%s - Unknown device type - %s", dev.device_name, dev.device_type

View File

@ -6,6 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from pyvesync.vesyncfan import VeSyncAirBypass from pyvesync.vesyncfan import VeSyncAirBypass
from pyvesync.vesyncoutlet import VeSyncOutlet from pyvesync.vesyncoutlet import VeSyncOutlet
from pyvesync.vesyncswitch import VeSyncSwitch 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType 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 from .entity import VeSyncBaseEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -187,24 +196,31 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up switches.""" """Set up switches."""
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback @callback
def discover(devices): def discover(devices):
"""Add new devices to platform.""" """Add new devices to platform."""
_setup_entities(devices, async_add_entities) _setup_entities(devices, async_add_entities, coordinator)
config_entry.async_on_unload( config_entry.async_on_unload(
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_SENSORS), discover) 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 @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.""" """Check if device is online and add entity."""
async_add_entities( async_add_entities(
( (
VeSyncSensorEntity(dev, description) VeSyncSensorEntity(dev, description, coordinator)
for dev in devices for dev in devices
for description in SENSORS for description in SENSORS
if description.exists_fn(dev) if description.exists_fn(dev)
@ -222,9 +238,10 @@ class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity):
self, self,
device: VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch, device: VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch,
description: VeSyncSensorEntityDescription, description: VeSyncSensorEntityDescription,
coordinator,
) -> None: ) -> None:
"""Initialize the VeSync outlet device.""" """Initialize the VeSync outlet device."""
super().__init__(device) super().__init__(device, coordinator)
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{super().unique_id}-{description.key}" self._attr_unique_id = f"{super().unique_id}-{description.key}"

View File

@ -3,13 +3,16 @@
import logging import logging
from typing import Any from typing import Any
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 from .entity import VeSyncDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -22,27 +25,33 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up switches.""" """Set up switches."""
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback @callback
def discover(devices): def discover(devices):
"""Add new devices to platform.""" """Add new devices to platform."""
_setup_entities(devices, async_add_entities) _setup_entities(devices, async_add_entities, coordinator)
config_entry.async_on_unload( config_entry.async_on_unload(
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_SWITCHES), discover) 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 @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.""" """Check if device is online and add entity."""
entities = [] entities: list[VeSyncBaseSwitch] = []
for dev in devices: for dev in devices:
if DEV_TYPE_TO_HA.get(dev.device_type) == "outlet": 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": elif DEV_TYPE_TO_HA.get(dev.device_type) == "switch":
entities.append(VeSyncLightSwitch(dev)) entities.append(VeSyncLightSwitch(dev, coordinator))
else: else:
_LOGGER.warning( _LOGGER.warning(
"%s - Unknown device type - %s", dev.device_name, dev.device_type "%s - Unknown device type - %s", dev.device_name, dev.device_type
@ -65,21 +74,16 @@ class VeSyncBaseSwitch(VeSyncDevice, SwitchEntity):
class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity):
"""Representation of a VeSync switch.""" """Representation of a VeSync switch."""
def __init__(self, plug): def __init__(self, plug, coordinator: VeSyncDataCoordinator) -> None:
"""Initialize the VeSync switch device.""" """Initialize the VeSync switch device."""
super().__init__(plug) super().__init__(plug, coordinator)
self.smartplug = plug self.smartplug = plug
def update(self) -> None:
"""Update outlet details and energy usage."""
self.smartplug.update()
self.smartplug.update_energy()
class VeSyncLightSwitch(VeSyncBaseSwitch, SwitchEntity): class VeSyncLightSwitch(VeSyncBaseSwitch, SwitchEntity):
"""Handle representation of VeSync Light Switch.""" """Handle representation of VeSync Light Switch."""
def __init__(self, switch): def __init__(self, switch, coordinator: VeSyncDataCoordinator) -> None:
"""Initialize Light Switch device class.""" """Initialize Light Switch device class."""
super().__init__(switch) super().__init__(switch, coordinator)
self.switch = switch self.switch = switch

View File

@ -1,10 +1,12 @@
"""Common methods used across tests for VeSync.""" """Common methods used across tests for VeSync."""
import json import json
from typing import Any
import requests_mock import requests_mock
from homeassistant.components.vesync.const import DOMAIN from homeassistant.components.vesync.const import DOMAIN
from homeassistant.util.json import JsonObjectType
from tests.common import load_fixture, load_json_object_fixture 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") ("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json")
], ],
"Air Purifier 400s": [ "Air Purifier 400s": [
("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json") ("post", "/cloud/v2/deviceManaged/bypassV2", "air-purifier-400s-detail.json")
], ],
"Air Purifier 600s": [ "Air Purifier 600s": [
("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json") ("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json")
@ -37,7 +39,10 @@ DEVICE_FIXTURES: dict[str, list[tuple[str, str, str]]] = {
"Temperature Light": [ "Temperature Light": [
("post", "/cloud/v1/deviceManaged/bypass", "device-detail.json") ("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": [ "Wall Switch": [
("post", "/inwallswitch/v1/device/devicedetail", "device-detail.json") ("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): def call_api_side_effect__no_devices(*args, **kwargs):
"""Build a side_effects method for the Helpers.call_api method.""" """Build a side_effects method for the Helpers.call_api method."""
if args[0] == "/cloud/v1/user/login" and args[1] == "post": if args[0] == "/cloud/v1/user/login" and args[1] == "post":

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,7 @@
{
"energyConsumptionOfToday": 1,
"costPerKWH": 0.15,
"maxEnergy": 6,
"totalEnergy": 0,
"currency": "$"
}

View 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"