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", "domain": "efergy",
"name": "Efergy", "name": "Efergy",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/efergy", "documentation": "https://www.home-assistant.io/integrations/efergy",
"requirements": ["pyefergy==0.0.3"], "requirements": ["pyefergy==0.1.2"],
"codeowners": ["@tkdrob"], "codeowners": ["@tkdrob"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View file

@ -2,15 +2,18 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from re import sub
from pyefergy import Efergy, exceptions from pyefergy import Efergy, exceptions
import voluptuous as vol import voluptuous as vol
from homeassistant.components.efergy import EfergyEntity
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_CURRENCY, CONF_CURRENCY,
CONF_MONITORED_VARIABLES, CONF_MONITORED_VARIABLES,
@ -22,72 +25,103 @@ from homeassistant.const import (
POWER_WATT, POWER_WATT,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
CONF_APPTOKEN = "app_token" from .const import CONF_APPTOKEN, CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN
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"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES: dict[str, SensorEntityDescription] = { SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
CONF_INSTANT: SensorEntityDescription( SensorEntityDescription(
key=CONF_INSTANT, key="instant_readings",
name="Energy Usage", name="Power Usage",
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
native_unit_of_measurement=POWER_WATT, native_unit_of_measurement=POWER_WATT,
), ),
CONF_AMOUNT: SensorEntityDescription( SensorEntityDescription(
key=CONF_AMOUNT, key="energy_day",
name="Energy Consumed", 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, device_class=DEVICE_CLASS_ENERGY,
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
), ),
CONF_BUDGET: SensorEntityDescription( SensorEntityDescription(
key=CONF_BUDGET, key="energy_year",
name="Energy Budget", name="Yearly Consumption",
device_class=DEVICE_CLASS_ENERGY,
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
entity_registry_enabled_default=False,
), ),
CONF_COST: SensorEntityDescription( SensorEntityDescription(
key=CONF_COST, key="budget",
name="Energy Cost", 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, 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, key=CONF_CURRENT_VALUES,
name="Per-Device Usage", name="Power Usage",
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
native_unit_of_measurement=POWER_WATT, 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( SENSORS_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_TYPE): TYPES_SCHEMA, vol.Required(CONF_TYPE): TYPES_SCHEMA,
vol.Optional(CONF_CURRENCY, default=""): cv.string, 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( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_APPTOKEN): cv.string, 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], vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA],
} }
) )
@ -99,62 +133,69 @@ async def async_setup_platform(
add_entities: AddEntitiesCallback, add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType = None, discovery_info: DiscoveryInfoType = None,
) -> None: ) -> None:
"""Set up the Efergy sensor.""" """Set up the Efergy sensor from yaml."""
api = Efergy( hass.async_create_task(
config[CONF_APPTOKEN], hass.config_entries.flow.async_init(
async_get_clientsession(hass), DOMAIN, context={"source": SOURCE_IMPORT}, data=config
utc_offset=config[CONF_UTC_OFFSET], )
) )
dev = []
try: async def async_setup_entry(
sensors = await api.get_sids() hass: HomeAssistant,
except (exceptions.DataError, exceptions.ConnectTimeout) as ex: entry: ConfigEntry,
raise PlatformNotReady("Error getting data from Efergy:") from ex async_add_entities: entity_platform.AddEntitiesCallback,
for variable in config[CONF_MONITORED_VARIABLES]: ) -> None:
if variable[CONF_TYPE] == CONF_CURRENT_VALUES: """Set up Efergy sensors."""
for sensor in sensors: api: Efergy = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]
dev.append( 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( EfergySensor(
api, api,
variable[CONF_PERIOD], description,
variable[CONF_CURRENCY], entry.entry_id,
SENSOR_TYPES[variable[CONF_TYPE]], sid=sid,
sid=sensor["sid"],
) )
) )
dev.append( async_add_entities(sensors, True)
EfergySensor(
api,
variable[CONF_PERIOD],
variable[CONF_CURRENCY],
SENSOR_TYPES[variable[CONF_TYPE]],
)
)
add_entities(dev, True)
class EfergySensor(SensorEntity): class EfergySensor(EfergyEntity, SensorEntity):
"""Implementation of an Efergy sensor.""" """Implementation of an Efergy sensor."""
def __init__( def __init__(
self, self,
api: Efergy, api: Efergy,
period: str,
currency: str,
description: SensorEntityDescription, description: SensorEntityDescription,
sid: str = None, server_unique_id: str,
period: str = None,
currency: str = None,
sid: str = "",
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(api, server_unique_id)
self.entity_description = description 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.sid = sid
self.api = api
self.period = period 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: async def async_update(self) -> None:
"""Get the Efergy monitor data from the web service.""" """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._attr_native_value = await self.api.async_get_reading(
self.entity_description.key, period=self.period, sid=self.sid 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: if self._attr_available:
self._attr_available = False self._attr_available = False
_LOGGER.error("Error getting data from Efergy: %s", ex) _LOGGER.error("Error getting data: %s", ex)
return return
if not self._attr_available: if not self._attr_available:
self._attr_available = True 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", "eafm",
"ecobee", "ecobee",
"econet", "econet",
"efergy",
"elgato", "elgato",
"elkm1", "elkm1",
"emonitor", "emonitor",

View file

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

View file

@ -844,7 +844,7 @@ pydispatcher==2.0.5
pyeconet==0.1.14 pyeconet==0.1.14
# homeassistant.components.efergy # homeassistant.components.efergy
pyefergy==0.0.3 pyefergy==0.1.2
# homeassistant.components.everlights # homeassistant.components.everlights
pyeverlights==0.1.0 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.""" """The tests for Efergy sensor platform."""
import asyncio
from datetime import timedelta from datetime import timedelta
from homeassistant.components.efergy.sensor import SENSOR_TYPES
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN 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.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 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 from tests.test_util.aiohttp import AiohttpClientMocker
token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT"
multi_sensor_token = "9r6QGF7dpZfO3fqPTBl1fyRmjV1cGoLT"
ONE_SENSOR_CONFIG = { async def test_sensor_readings(
"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(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
): ):
"""Test for successfully setting up the Efergy platform.""" """Test for successfully setting up the Efergy platform."""
mock_responses(aioclient_mock) for description in SENSOR_TYPES:
assert await async_setup_component( description.entity_registry_enabled_default = True
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} entry = await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN)
) ent_reg: EntityRegistry = er.async_get(hass)
await hass.async_block_till_done()
assert hass.states.get("sensor.energy_consumed").state == "38.21" state = hass.states.get("sensor.power_usage")
assert hass.states.get("sensor.energy_usage").state == "1580" assert state.state == "1580"
assert hass.states.get("sensor.energy_budget").state == "ok" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
assert hass.states.get("sensor.energy_cost").state == "5.27" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
assert hass.states.get("sensor.efergy_728386").state == "1628" 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( async def test_multi_sensor_readings(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
): ):
"""Test for multiple sensors in one household.""" """Test for multiple sensors in one household."""
mock_responses(aioclient_mock) for description in SENSOR_TYPES:
assert await async_setup_component( description.entity_registry_enabled_default = True
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MULTI_SENSOR_CONFIG} await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN, MULTI_SENSOR_TOKEN)
) state = hass.states.get("sensor.power_usage_728386")
await hass.async_block_till_done() assert state.state == "218"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
assert hass.states.get("sensor.efergy_728386").state == "218" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
assert hass.states.get("sensor.efergy_0").state == "1808" state = hass.states.get("sensor.power_usage_0")
assert hass.states.get("sensor.efergy_728387").state == "312" assert state.state == "1808"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
async def test_failed_getting_sids( state = hass.states.get("sensor.power_usage_728387")
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker assert state.state == "312"
): assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
"""Test failed gettings sids.""" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
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")
async def test_failed_update_and_reconnection( async def test_failed_update_and_reconnection(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
): ):
"""Test failed update and reconnection.""" """Test failed update and reconnection."""
mock_responses(aioclient_mock) await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN)
assert await async_setup_component( assert hass.states.get("sensor.power_usage").state == "1580"
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
aioclient_mock.clear_requests() aioclient_mock.clear_requests()
mock_responses(aioclient_mock, error=True) await mock_responses(hass, 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)
next_update = dt_util.utcnow() + timedelta(seconds=30) next_update = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
await hass.async_block_till_done() 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"
}