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 (
|
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] = []
|
||||||
|
@ -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 = {
|
||||||
|
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 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."""
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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}"
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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":
|
||||||
|
@ -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