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 (
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] = []

View File

@ -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 = {

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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":

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"