Add config flow for efergy (#56890)
This commit is contained in:
parent
3825f80a2d
commit
c4eeebd7a7
27 changed files with 851 additions and 184 deletions
|
@ -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"],
|
||||||
|
}
|
||||||
|
|
85
homeassistant/components/efergy/config_flow.py
Normal file
85
homeassistant/components/efergy/config_flow.py
Normal 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
|
13
homeassistant/components/efergy/const.py
Normal file
13
homeassistant/components/efergy/const.py
Normal 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)
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
21
homeassistant/components/efergy/strings.json
Normal file
21
homeassistant/components/efergy/strings.json
Normal 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%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
homeassistant/components/efergy/translations/en.json
Normal file
21
homeassistant/components/efergy/translations/en.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,6 +70,7 @@ FLOWS = [
|
||||||
"eafm",
|
"eafm",
|
||||||
"ecobee",
|
"ecobee",
|
||||||
"econet",
|
"econet",
|
||||||
|
"efergy",
|
||||||
"elgato",
|
"elgato",
|
||||||
"elkm1",
|
"elkm1",
|
||||||
"emonitor",
|
"emonitor",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
134
tests/components/efergy/test_config_flow.py
Normal file
134
tests/components/efergy/test_config_flow.py
Normal 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
|
61
tests/components/efergy/test_init.py
Normal file
61
tests/components/efergy/test_init.py
Normal 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"
|
|
@ -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"
|
||||||
|
|
5
tests/fixtures/efergy/monthly_cost.json
vendored
Normal file
5
tests/fixtures/efergy/monthly_cost.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"sum": "147.56",
|
||||||
|
"duration": 2537340,
|
||||||
|
"units": "GBP"
|
||||||
|
}
|
5
tests/fixtures/efergy/monthly_energy.json
vendored
Normal file
5
tests/fixtures/efergy/monthly_energy.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"sum": "1069.88",
|
||||||
|
"duration": 2537340,
|
||||||
|
"units": "kWh"
|
||||||
|
}
|
31
tests/fixtures/efergy/status.json
vendored
Normal file
31
tests/fixtures/efergy/status.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
tests/fixtures/efergy/weekly_cost.json
vendored
Normal file
5
tests/fixtures/efergy/weekly_cost.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"sum": "36.89",
|
||||||
|
"duration": 377280,
|
||||||
|
"units": "GBP"
|
||||||
|
}
|
5
tests/fixtures/efergy/weekly_energy.json
vendored
Normal file
5
tests/fixtures/efergy/weekly_energy.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"sum": "267.47",
|
||||||
|
"duration": 377280,
|
||||||
|
"units": "kWh"
|
||||||
|
}
|
5
tests/fixtures/efergy/yearly_cost.json
vendored
Normal file
5
tests/fixtures/efergy/yearly_cost.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"sum": "1844.50",
|
||||||
|
"duration": 23532540,
|
||||||
|
"units": "GBP"
|
||||||
|
}
|
5
tests/fixtures/efergy/yearly_energy.json
vendored
Normal file
5
tests/fixtures/efergy/yearly_energy.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"sum": "13373.50",
|
||||||
|
"duration": 23532540,
|
||||||
|
"units": "kWh"
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue