Add config flow for efergy (#56890)

This commit is contained in:
Robert Hillis 2021-10-11 04:07:31 -04:00 committed by GitHub
parent 3825f80a2d
commit c4eeebd7a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 851 additions and 184 deletions

View file

@ -1 +1,83 @@
"""The efergy component."""
"""The Efergy integration."""
from __future__ import annotations
from pyefergy import Efergy, exceptions
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_NAME,
ATTR_SW_VERSION,
CONF_API_KEY,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import ATTRIBUTION, DATA_KEY_API, DEFAULT_NAME, DOMAIN
PLATFORMS = [SENSOR_DOMAIN]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Efergy from a config entry."""
api = Efergy(
entry.data[CONF_API_KEY],
session=async_get_clientsession(hass),
utc_offset=hass.config.time_zone,
currency=hass.config.currency,
)
try:
await api.async_status(get_sids=True)
except (exceptions.ConnectError, exceptions.DataError) as ex:
raise ConfigEntryNotReady(f"Failed to connect to device: {ex}") from ex
except exceptions.InvalidAuth as ex:
raise ConfigEntryAuthFailed(
"API Key is no longer valid. Please reauthenticate"
) from ex
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api}
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class EfergyEntity(Entity):
"""Representation of a Efergy entity."""
def __init__(
self,
api: Efergy,
server_unique_id: str,
) -> None:
"""Initialize an Efergy entity."""
self.api = api
self._server_unique_id = server_unique_id
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
@property
def device_info(self) -> DeviceInfo:
"""Return the device information of the entity."""
return {
"connections": {(dr.CONNECTION_NETWORK_MAC, self.api.info["mac"])},
ATTR_IDENTIFIERS: {(DOMAIN, self._server_unique_id)},
ATTR_MANUFACTURER: DEFAULT_NAME,
ATTR_NAME: DEFAULT_NAME,
ATTR_MODEL: self.api.info["type"],
ATTR_SW_VERSION: self.api.info["version"],
}

View file

@ -0,0 +1,85 @@
"""Config flow for Efergy integration."""
from __future__ import annotations
import logging
from typing import Any
from pyefergy import Efergy, exceptions
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import CONF_APPTOKEN, DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Efergy."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initiated by the user."""
errors = {}
if user_input is not None:
api_key = user_input[CONF_API_KEY]
self._async_abort_entries_match({CONF_API_KEY: api_key})
hid, error = await self._async_try_connect(api_key)
if error is None:
entry = await self.async_set_unique_id(hid)
if entry:
self.hass.config_entries.async_update_entry(entry, data=user_input)
await self.hass.config_entries.async_reload(entry.entry_id)
return self.async_abort(reason="reauth_successful")
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=DEFAULT_NAME,
data={CONF_API_KEY: api_key},
)
errors["base"] = error
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_API_KEY): str,
}
),
errors=errors,
)
async def async_step_import(self, import_config: ConfigType):
"""Import a config entry from configuration.yaml."""
for entry in self._async_current_entries():
if entry.data[CONF_API_KEY] == import_config[CONF_APPTOKEN]:
_part = import_config[CONF_APPTOKEN][0:4]
_msg = f"Efergy yaml config with partial key {_part} has been imported. Please remove it"
_LOGGER.warning(_msg)
return self.async_abort(reason="already_configured")
return await self.async_step_user({CONF_API_KEY: import_config[CONF_APPTOKEN]})
async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult:
"""Handle a reauthorization flow request."""
return await self.async_step_user()
async def _async_try_connect(self, api_key: str) -> tuple[str | None, str | None]:
"""Try connecting to Efergy servers."""
api = Efergy(api_key, session=async_get_clientsession(self.hass))
try:
await api.async_status()
except exceptions.ConnectError:
return None, "cannot_connect"
except exceptions.InvalidAuth:
return None, "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
return None, "unknown"
return api.info["hid"], None

View file

@ -0,0 +1,13 @@
"""Constants for the Efergy integration."""
from datetime import timedelta
ATTRIBUTION = "Data provided by Efergy"
CONF_APPTOKEN = "app_token"
CONF_CURRENT_VALUES = "current_values"
DATA_KEY_API = "api"
DEFAULT_NAME = "Efergy"
DOMAIN = "efergy"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)

View file

@ -1,8 +1,9 @@
{
"domain": "efergy",
"name": "Efergy",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/efergy",
"requirements": ["pyefergy==0.0.3"],
"requirements": ["pyefergy==0.1.2"],
"codeowners": ["@tkdrob"],
"iot_class": "cloud_polling"
}

View file

@ -2,15 +2,18 @@
from __future__ import annotations
import logging
from re import sub
from pyefergy import Efergy, exceptions
import voluptuous as vol
from homeassistant.components.efergy import EfergyEntity
from homeassistant.components.sensor import (
PLATFORM_SCHEMA,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_CURRENCY,
CONF_MONITORED_VARIABLES,
@ -22,72 +25,103 @@ from homeassistant.const import (
POWER_WATT,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
CONF_APPTOKEN = "app_token"
CONF_UTC_OFFSET = "utc_offset"
CONF_PERIOD = "period"
CONF_INSTANT = "instant_readings"
CONF_AMOUNT = "amount"
CONF_BUDGET = "budget"
CONF_COST = "cost"
CONF_CURRENT_VALUES = "current_values"
DEFAULT_PERIOD = "year"
DEFAULT_UTC_OFFSET = "0"
from .const import CONF_APPTOKEN, CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES: dict[str, SensorEntityDescription] = {
CONF_INSTANT: SensorEntityDescription(
key=CONF_INSTANT,
name="Energy Usage",
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="instant_readings",
name="Power Usage",
device_class=DEVICE_CLASS_POWER,
native_unit_of_measurement=POWER_WATT,
),
CONF_AMOUNT: SensorEntityDescription(
key=CONF_AMOUNT,
name="Energy Consumed",
SensorEntityDescription(
key="energy_day",
name="Daily Consumption",
device_class=DEVICE_CLASS_ENERGY,
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energy_week",
name="Weekly Consumption",
device_class=DEVICE_CLASS_ENERGY,
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="energy_month",
name="Monthly Consumption",
device_class=DEVICE_CLASS_ENERGY,
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
),
CONF_BUDGET: SensorEntityDescription(
key=CONF_BUDGET,
name="Energy Budget",
SensorEntityDescription(
key="energy_year",
name="Yearly Consumption",
device_class=DEVICE_CLASS_ENERGY,
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
entity_registry_enabled_default=False,
),
CONF_COST: SensorEntityDescription(
key=CONF_COST,
name="Energy Cost",
SensorEntityDescription(
key="budget",
name="Energy Budget",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="cost_day",
name="Daily Energy Cost",
device_class=DEVICE_CLASS_MONETARY,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="cost_week",
name="Weekly Energy Cost",
device_class=DEVICE_CLASS_MONETARY,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="cost_month",
name="Monthly Energy Cost",
device_class=DEVICE_CLASS_MONETARY,
),
CONF_CURRENT_VALUES: SensorEntityDescription(
SensorEntityDescription(
key="cost_year",
name="Yearly Energy Cost",
device_class=DEVICE_CLASS_MONETARY,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=CONF_CURRENT_VALUES,
name="Per-Device Usage",
name="Power Usage",
device_class=DEVICE_CLASS_POWER,
native_unit_of_measurement=POWER_WATT,
),
}
)
TYPES_SCHEMA = vol.In(
["current_values", "instant_readings", "amount", "budget", "cost"]
)
TYPES_SCHEMA = vol.In(SENSOR_TYPES)
SENSORS_SCHEMA = vol.Schema(
{
vol.Required(CONF_TYPE): TYPES_SCHEMA,
vol.Optional(CONF_CURRENCY, default=""): cv.string,
vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.string,
vol.Optional("period", default="year"): cv.string,
}
)
# Deprecated in Home Assistant 2021.11
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_APPTOKEN): cv.string,
vol.Optional(CONF_UTC_OFFSET, default=DEFAULT_UTC_OFFSET): cv.string,
vol.Optional("utc_offset", default="0"): cv.string,
vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA],
}
)
@ -99,62 +133,69 @@ async def async_setup_platform(
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType = None,
) -> None:
"""Set up the Efergy sensor."""
api = Efergy(
config[CONF_APPTOKEN],
async_get_clientsession(hass),
utc_offset=config[CONF_UTC_OFFSET],
"""Set up the Efergy sensor from yaml."""
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
)
dev = []
try:
sensors = await api.get_sids()
except (exceptions.DataError, exceptions.ConnectTimeout) as ex:
raise PlatformNotReady("Error getting data from Efergy:") from ex
for variable in config[CONF_MONITORED_VARIABLES]:
if variable[CONF_TYPE] == CONF_CURRENT_VALUES:
for sensor in sensors:
dev.append(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: entity_platform.AddEntitiesCallback,
) -> None:
"""Set up Efergy sensors."""
api: Efergy = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]
sensors = []
for description in SENSOR_TYPES:
if description.key != CONF_CURRENT_VALUES:
sensors.append(
EfergySensor(
api,
description,
entry.entry_id,
period=sub("^energy_|^cost_", "", description.key),
currency=hass.config.currency,
)
)
else:
description.entity_registry_enabled_default = len(api.info["sids"]) > 1
for sid in api.info["sids"]:
sensors.append(
EfergySensor(
api,
variable[CONF_PERIOD],
variable[CONF_CURRENCY],
SENSOR_TYPES[variable[CONF_TYPE]],
sid=sensor["sid"],
description,
entry.entry_id,
sid=sid,
)
)
dev.append(
EfergySensor(
api,
variable[CONF_PERIOD],
variable[CONF_CURRENCY],
SENSOR_TYPES[variable[CONF_TYPE]],
)
)
add_entities(dev, True)
async_add_entities(sensors, True)
class EfergySensor(SensorEntity):
class EfergySensor(EfergyEntity, SensorEntity):
"""Implementation of an Efergy sensor."""
def __init__(
self,
api: Efergy,
period: str,
currency: str,
description: SensorEntityDescription,
sid: str = None,
server_unique_id: str,
period: str = None,
currency: str = None,
sid: str = "",
) -> None:
"""Initialize the sensor."""
super().__init__(api, server_unique_id)
self.entity_description = description
if description.key == CONF_CURRENT_VALUES:
self._attr_name = f"{description.name}_{sid}"
self._attr_unique_id = f"{server_unique_id}/{description.key}_{sid}"
if "cost" in description.key:
self._attr_native_unit_of_measurement = currency
self.sid = sid
self.api = api
self.period = period
if sid:
self._attr_name = f"efergy_{sid}"
if description.key == CONF_COST:
self._attr_native_unit_of_measurement = f"{currency}/{period}"
async def async_update(self) -> None:
"""Get the Efergy monitor data from the web service."""
@ -162,11 +203,11 @@ class EfergySensor(SensorEntity):
self._attr_native_value = await self.api.async_get_reading(
self.entity_description.key, period=self.period, sid=self.sid
)
except (exceptions.DataError, exceptions.ConnectTimeout) as ex:
except (exceptions.DataError, exceptions.ConnectError) as ex:
if self._attr_available:
self._attr_available = False
_LOGGER.error("Error getting data from Efergy: %s", ex)
_LOGGER.error("Error getting data: %s", ex)
return
if not self._attr_available:
self._attr_available = True
_LOGGER.info("Connection to Efergy has resumed")
_LOGGER.info("Connection has resumed")

View file

@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"title": "Efergy",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}

View file

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"api_key": "API Key"
},
"title": "Efergy"
}
}
}
}

View file

@ -70,6 +70,7 @@ FLOWS = [
"eafm",
"ecobee",
"econet",
"efergy",
"elgato",
"elkm1",
"emonitor",

View file

@ -1447,7 +1447,7 @@ pyeconet==0.1.14
pyedimax==0.2.1
# homeassistant.components.efergy
pyefergy==0.0.3
pyefergy==0.1.2
# homeassistant.components.eight_sleep
pyeight==0.1.9

View file

@ -844,7 +844,7 @@ pydispatcher==2.0.5
pyeconet==0.1.14
# homeassistant.components.efergy
pyefergy==0.0.3
pyefergy==0.1.2
# homeassistant.components.everlights
pyeverlights==0.1.0

View file

@ -1 +1,157 @@
"""Tests for the efergy component."""
"""Tests for Efergy integration."""
from unittest.mock import AsyncMock, patch
from pyefergy import Efergy, exceptions
from homeassistant.components.efergy import DOMAIN
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
TOKEN = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT"
MULTI_SENSOR_TOKEN = "9r6QGF7dpZfO3fqPTBl1fyRmjV1cGoLT"
CONF_DATA = {CONF_API_KEY: TOKEN}
HID = "12345678901234567890123456789012"
IMPORT_DATA = {"platform": "efergy", "app_token": TOKEN}
BASE_URL = "https://engage.efergy.com/mobile_proxy/"
def create_entry(hass: HomeAssistant, token: str = TOKEN) -> MockConfigEntry:
"""Create Efergy entry in Home Assistant."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=HID,
data={CONF_API_KEY: token},
)
entry.add_to_hass(hass)
return entry
async def init_integration(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
token: str = TOKEN,
error: bool = False,
) -> MockConfigEntry:
"""Set up the Efergy integration in Home Assistant."""
entry = create_entry(hass, token=token)
await mock_responses(hass, aioclient_mock, token=token, error=error)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
async def mock_responses(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
token: str = TOKEN,
error: bool = False,
):
"""Mock responses from Efergy."""
base_url = "https://engage.efergy.com/mobile_proxy/"
api = Efergy(
token, session=async_get_clientsession(hass), utc_offset=hass.config.time_zone
)
offset = api._utc_offset # pylint: disable=protected-access
if error:
aioclient_mock.get(
f"{base_url}getInstant?token={token}",
exc=exceptions.ConnectError,
)
return
aioclient_mock.get(
f"{base_url}getStatus?token={token}",
text=load_fixture("efergy/status.json"),
)
aioclient_mock.get(
f"{base_url}getInstant?token={token}",
text=load_fixture("efergy/instant.json"),
)
aioclient_mock.get(
f"{base_url}getEnergy?token={token}&offset={offset}&period=day",
text=load_fixture("efergy/daily_energy.json"),
)
aioclient_mock.get(
f"{base_url}getEnergy?token={token}&offset={offset}&period=week",
text=load_fixture("efergy/weekly_energy.json"),
)
aioclient_mock.get(
f"{base_url}getEnergy?token={token}&offset={offset}&period=month",
text=load_fixture("efergy/monthly_energy.json"),
)
aioclient_mock.get(
f"{base_url}getEnergy?token={token}&offset={offset}&period=year",
text=load_fixture("efergy/yearly_energy.json"),
)
aioclient_mock.get(
f"{base_url}getBudget?token={token}",
text=load_fixture("efergy/budget.json"),
)
aioclient_mock.get(
f"{base_url}getCost?token={token}&offset={offset}&period=day",
text=load_fixture("efergy/daily_cost.json"),
)
aioclient_mock.get(
f"{base_url}getCost?token={token}&offset={offset}&period=week",
text=load_fixture("efergy/weekly_cost.json"),
)
aioclient_mock.get(
f"{base_url}getCost?token={token}&offset={offset}&period=month",
text=load_fixture("efergy/monthly_cost.json"),
)
aioclient_mock.get(
f"{base_url}getCost?token={token}&offset={offset}&period=year",
text=load_fixture("efergy/yearly_cost.json"),
)
if token == TOKEN:
aioclient_mock.get(
f"{base_url}getCurrentValuesSummary?token={token}",
text=load_fixture("efergy/current_values_single.json"),
)
else:
aioclient_mock.get(
f"{base_url}getCurrentValuesSummary?token={token}",
text=load_fixture("efergy/current_values_multi.json"),
)
def _patch_efergy():
mocked_efergy = AsyncMock()
mocked_efergy.info = {}
mocked_efergy.info["hid"] = HID
mocked_efergy.info["mac"] = "AA:BB:CC:DD:EE:FF"
mocked_efergy.info["status"] = "on"
mocked_efergy.info["type"] = "EEEHub"
mocked_efergy.info["version"] = "2.3.7"
return patch(
"homeassistant.components.efergy.config_flow.Efergy",
return_value=mocked_efergy,
)
def _patch_efergy_status():
return patch("homeassistant.components.efergy.config_flow.Efergy.async_status")
async def setup_platform(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
platform: str,
token: str = TOKEN,
error: bool = False,
):
"""Set up the platform."""
entry = await init_integration(hass, aioclient_mock, token=token, error=error)
with patch("homeassistant.components.efergy.PLATFORMS", [platform]):
assert await async_setup_component(hass, DOMAIN, {})
return entry

View file

@ -0,0 +1,134 @@
"""Test Efergy config flow."""
from unittest.mock import patch
from pyefergy import exceptions
from homeassistant.components.efergy.const import DEFAULT_NAME, DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER
from homeassistant.const import CONF_API_KEY, CONF_SOURCE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from . import (
CONF_DATA,
HID,
IMPORT_DATA,
_patch_efergy,
_patch_efergy_status,
create_entry,
)
def _patch_setup():
return patch("homeassistant.components.efergy.async_setup_entry")
async def test_flow_user(hass: HomeAssistant):
"""Test user initialized flow."""
with _patch_efergy(), _patch_setup():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_USER},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=CONF_DATA,
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == DEFAULT_NAME
assert result["data"] == CONF_DATA
assert result["result"].unique_id == HID
async def test_flow_user_cannot_connect(hass: HomeAssistant):
"""Test user initialized flow with unreachable service."""
with _patch_efergy_status() as efergymock:
efergymock.side_effect = exceptions.ConnectError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"]["base"] == "cannot_connect"
async def test_flow_user_invalid_auth(hass: HomeAssistant):
"""Test user initialized flow with invalid authentication."""
with _patch_efergy_status() as efergymock:
efergymock.side_effect = exceptions.InvalidAuth
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"]["base"] == "invalid_auth"
async def test_flow_user_unknown(hass: HomeAssistant):
"""Test user initialized flow with unknown error."""
with _patch_efergy_status() as efergymock:
efergymock.side_effect = Exception
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"]["base"] == "unknown"
async def test_flow_import(hass: HomeAssistant):
"""Test import step."""
with _patch_efergy(), _patch_setup():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == DEFAULT_NAME
assert result["data"] == CONF_DATA
assert result["result"].unique_id == HID
async def test_flow_import_already_configured(hass: HomeAssistant):
"""Test import step already configured."""
create_entry(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_flow_reauth(hass: HomeAssistant):
"""Test reauth step."""
entry = create_entry(hass)
with _patch_efergy(), _patch_setup():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
CONF_SOURCE: SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
data=CONF_DATA,
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
new_conf = {CONF_API_KEY: "1234567890"}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=new_conf,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "reauth_successful"
assert entry.data == new_conf

View file

@ -0,0 +1,61 @@
"""Test Efergy integration."""
from pyefergy import exceptions
from homeassistant.components.efergy.const import DEFAULT_NAME, DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import _patch_efergy_status, create_entry, init_integration, setup_platform
from tests.test_util.aiohttp import AiohttpClientMocker
async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
"""Test unload."""
entry = await init_integration(hass, aioclient_mock)
assert entry.state == ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN)
async def test_async_setup_entry_not_ready(hass: HomeAssistant):
"""Test that it throws ConfigEntryNotReady when exception occurs during setup."""
entry = create_entry(hass)
with _patch_efergy_status() as efergymock:
efergymock.side_effect = (exceptions.ConnectError, exceptions.DataError)
await hass.config_entries.async_setup(entry.entry_id)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state == ConfigEntryState.SETUP_RETRY
assert not hass.data.get(DOMAIN)
async def test_async_setup_entry_auth_failed(hass: HomeAssistant):
"""Test that it throws ConfigEntryAuthFailed when authentication fails."""
entry = create_entry(hass)
with _patch_efergy_status() as efergymock:
efergymock.side_effect = exceptions.InvalidAuth
await hass.config_entries.async_setup(entry.entry_id)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state == ConfigEntryState.SETUP_ERROR
assert not hass.data.get(DOMAIN)
async def test_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
"""Test device info."""
entry = await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN)
device_registry = await dr.async_get_registry(hass)
device = device_registry.async_get_device({(DOMAIN, entry.entry_id)})
assert device.connections == {("mac", "ff:ff:ff:ff:ff:ff")}
assert device.identifiers == {(DOMAIN, entry.entry_id)}
assert device.manufacturer == DEFAULT_NAME
assert device.model == "EEEHub"
assert device.name == DEFAULT_NAME
assert device.sw_version == "2.3.7"

View file

@ -1,135 +1,125 @@
"""The tests for Efergy sensor platform."""
import asyncio
from datetime import timedelta
from homeassistant.components.efergy.sensor import SENSOR_TYPES
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_MONETARY,
DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
POWER_WATT,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, load_fixture
from . import MULTI_SENSOR_TOKEN, mock_responses, setup_platform
from tests.common import async_fire_time_changed
from tests.test_util.aiohttp import AiohttpClientMocker
token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT"
multi_sensor_token = "9r6QGF7dpZfO3fqPTBl1fyRmjV1cGoLT"
ONE_SENSOR_CONFIG = {
"platform": "efergy",
"app_token": token,
"utc_offset": "300",
"monitored_variables": [
{"type": "amount", "period": "day"},
{"type": "instant_readings"},
{"type": "budget"},
{"type": "cost", "period": "day", "currency": "$"},
{"type": "current_values"},
],
}
MULTI_SENSOR_CONFIG = {
"platform": "efergy",
"app_token": multi_sensor_token,
"utc_offset": "300",
"monitored_variables": [{"type": "current_values"}],
}
def mock_responses(aioclient_mock: AiohttpClientMocker, error: bool = False):
"""Mock responses for Efergy."""
base_url = "https://engage.efergy.com/mobile_proxy/"
if error:
aioclient_mock.get(
f"{base_url}getCurrentValuesSummary?token={token}", exc=asyncio.TimeoutError
)
return
aioclient_mock.get(
f"{base_url}getInstant?token={token}",
text=load_fixture("efergy/efergy_instant.json"),
)
aioclient_mock.get(
f"{base_url}getEnergy?token={token}&offset=300&period=day",
text=load_fixture("efergy/efergy_energy.json"),
)
aioclient_mock.get(
f"{base_url}getBudget?token={token}",
text=load_fixture("efergy/efergy_budget.json"),
)
aioclient_mock.get(
f"{base_url}getCost?token={token}&offset=300&period=day",
text=load_fixture("efergy/efergy_cost.json"),
)
aioclient_mock.get(
f"{base_url}getCurrentValuesSummary?token={token}",
text=load_fixture("efergy/efergy_current_values_single.json"),
)
aioclient_mock.get(
f"{base_url}getCurrentValuesSummary?token={multi_sensor_token}",
text=load_fixture("efergy/efergy_current_values_multi.json"),
)
async def test_single_sensor_readings(
async def test_sensor_readings(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
):
"""Test for successfully setting up the Efergy platform."""
mock_responses(aioclient_mock)
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
await hass.async_block_till_done()
for description in SENSOR_TYPES:
description.entity_registry_enabled_default = True
entry = await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN)
ent_reg: EntityRegistry = er.async_get(hass)
assert hass.states.get("sensor.energy_consumed").state == "38.21"
assert hass.states.get("sensor.energy_usage").state == "1580"
assert hass.states.get("sensor.energy_budget").state == "ok"
assert hass.states.get("sensor.energy_cost").state == "5.27"
assert hass.states.get("sensor.efergy_728386").state == "1628"
state = hass.states.get("sensor.power_usage")
assert state.state == "1580"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
state = hass.states.get("sensor.energy_budget")
assert state.state == "ok"
assert state.attributes.get(ATTR_DEVICE_CLASS) is None
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
state = hass.states.get("sensor.daily_consumption")
assert state.state == "38.21"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
state = hass.states.get("sensor.weekly_consumption")
assert state.state == "267.47"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
state = hass.states.get("sensor.monthly_consumption")
assert state.state == "1069.88"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
state = hass.states.get("sensor.yearly_consumption")
assert state.state == "13373.50"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
state = hass.states.get("sensor.daily_energy_cost")
assert state.state == "5.27"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR"
state = hass.states.get("sensor.weekly_energy_cost")
assert state.state == "36.89"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR"
state = hass.states.get("sensor.monthly_energy_cost")
assert state.state == "147.56"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR"
state = hass.states.get("sensor.yearly_energy_cost")
assert state.state == "1844.50"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR"
entity = ent_reg.async_get("sensor.power_usage_728386")
assert entity.disabled_by == er.DISABLED_INTEGRATION
ent_reg.async_update_entity(entity.entity_id, **{"disabled_by": None})
await hass.config_entries.async_reload(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.power_usage_728386")
assert state.state == "1628"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
async def test_multi_sensor_readings(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
):
"""Test for multiple sensors in one household."""
mock_responses(aioclient_mock)
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MULTI_SENSOR_CONFIG}
)
await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == "218"
assert hass.states.get("sensor.efergy_0").state == "1808"
assert hass.states.get("sensor.efergy_728387").state == "312"
async def test_failed_getting_sids(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
):
"""Test failed gettings sids."""
mock_responses(aioclient_mock, error=True)
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
assert not hass.states.async_all("sensor")
for description in SENSOR_TYPES:
description.entity_registry_enabled_default = True
await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN, MULTI_SENSOR_TOKEN)
state = hass.states.get("sensor.power_usage_728386")
assert state.state == "218"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
state = hass.states.get("sensor.power_usage_0")
assert state.state == "1808"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
state = hass.states.get("sensor.power_usage_728387")
assert state.state == "312"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
async def test_failed_update_and_reconnection(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
):
"""Test failed update and reconnection."""
mock_responses(aioclient_mock)
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN)
assert hass.states.get("sensor.power_usage").state == "1580"
aioclient_mock.clear_requests()
mock_responses(aioclient_mock, error=True)
next_update = dt_util.utcnow() + timedelta(seconds=3)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == STATE_UNAVAILABLE
aioclient_mock.clear_requests()
mock_responses(aioclient_mock)
await mock_responses(hass, aioclient_mock, error=True)
next_update = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == "1628"
assert hass.states.get("sensor.power_usage").state == STATE_UNAVAILABLE
aioclient_mock.clear_requests()
await mock_responses(hass, aioclient_mock)
next_update = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert hass.states.get("sensor.power_usage").state == "1580"

View file

@ -0,0 +1,5 @@
{
"sum": "147.56",
"duration": 2537340,
"units": "GBP"
}

View file

@ -0,0 +1,5 @@
{
"sum": "1069.88",
"duration": 2537340,
"units": "kWh"
}

31
tests/fixtures/efergy/status.json vendored Normal file
View file

@ -0,0 +1,31 @@
{
"hid":"1234567890abcdef1234567890abcdef",
"listOfMacs":[
{
"listofchannels":[
{
"assoc":1,
"cid":"cid.ffffffffffff",
"reading":null,
"ts":1632961265,
"tsDelta":1,
"tsHuman":"Thu Sep 30 00:00:00 2021",
"type":{
"battery":5,
"falseBattery":0,
"id":null,
"name":"EFCT"
}
}
],
"mac":"ffffffffffff",
"personality":"E1",
"status":"on",
"ts":1632961265,
"tsDelta":1,
"tsHuman":"Thu Sep 30 00:00:00 2021",
"type":"EEEHub",
"version":"2.3.7"
}
]
}

View file

@ -0,0 +1,5 @@
{
"sum": "36.89",
"duration": 377280,
"units": "GBP"
}

View file

@ -0,0 +1,5 @@
{
"sum": "267.47",
"duration": 377280,
"units": "kWh"
}

View file

@ -0,0 +1,5 @@
{
"sum": "1844.50",
"duration": 23532540,
"units": "GBP"
}

View file

@ -0,0 +1,5 @@
{
"sum": "13373.50",
"duration": 23532540,
"units": "kWh"
}