Migrate emoncms to config flow (#121336)
* Migrate emoncms to config flow * tests coverage 98% * use runtime_data * Remove pyemoncms bump. * Remove not needed yaml parameters add async_update_data to coordinator * Reduce snapshot size * Remove CONF_UNIT_OF_MEASUREMENT * correct path in emoncms_client mock * Remove init connexion check as done by config_entry_first_refresh since async_update_data catches exceptionand raise UpdateFailed * Remove CONF_EXCLUDE_FEEDID from config flow * Update homeassistant/components/emoncms/__init__.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/emoncms/sensor.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/emoncms/strings.json Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Use options in options flow and common strings * Extend the ConfigEntry type * Define the type explicitely * Add data description in strings.json * Update tests/components/emoncms/test_config_flow.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update tests/components/emoncms/test_config_flow.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Add test import same yaml conf + corrections * Add test user flow * Use data_description... * Use snapshot_platform in test_sensor * Transfer all fixtures in conftest * Add async_step_choose_feeds to ask flows to user * Test abortion reason in test_flow_import_failure * Add issue when value_template is i yaml conf * make text more expressive in strings.json * Add issue when no feed imported during migration. * Update tests/components/emoncms/test_config_flow.py * Update tests/components/emoncms/test_config_flow.py --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
470335e27a
commit
8255728f53
15 changed files with 815 additions and 107 deletions
|
@ -1 +1,40 @@
|
|||
"""The emoncms component."""
|
||||
|
||||
from pyemoncms import EmoncmsClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import EmoncmsCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
type EmonCMSConfigEntry = ConfigEntry[EmoncmsCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> bool:
|
||||
"""Load a config entry."""
|
||||
emoncms_client = EmoncmsClient(
|
||||
entry.data[CONF_URL],
|
||||
entry.data[CONF_API_KEY],
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
coordinator = EmoncmsCoordinator(hass, emoncms_client)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
|
210
homeassistant/components/emoncms/config_flow.py
Normal file
210
homeassistant/components/emoncms/config_flow.py
Normal file
|
@ -0,0 +1,210 @@
|
|||
"""Configflow for the emoncms integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyemoncms import EmoncmsClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import selector
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_MESSAGE,
|
||||
CONF_ONLY_INCLUDE_FEEDID,
|
||||
CONF_SUCCESS,
|
||||
DOMAIN,
|
||||
FEED_ID,
|
||||
FEED_NAME,
|
||||
FEED_TAG,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
|
||||
def get_options(feeds: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Build the selector options with the feed list."""
|
||||
return [
|
||||
{
|
||||
"value": feed[FEED_ID],
|
||||
"label": f"{feed[FEED_ID]}|{feed[FEED_TAG]}|{feed[FEED_NAME]}",
|
||||
}
|
||||
for feed in feeds
|
||||
]
|
||||
|
||||
|
||||
def sensor_name(url: str) -> str:
|
||||
"""Return sensor name."""
|
||||
sensorip = url.rsplit("//", maxsplit=1)[-1]
|
||||
return f"emoncms@{sensorip}"
|
||||
|
||||
|
||||
async def get_feed_list(hass: HomeAssistant, url: str, api_key: str) -> dict[str, Any]:
|
||||
"""Check connection to emoncms and return feed list if successful."""
|
||||
emoncms_client = EmoncmsClient(
|
||||
url,
|
||||
api_key,
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
return await emoncms_client.async_request("/feed/list.json")
|
||||
|
||||
|
||||
class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""emoncms integration UI config flow."""
|
||||
|
||||
url: str
|
||||
api_key: str
|
||||
include_only_feeds: list | None = None
|
||||
dropdown: dict = {}
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowWithConfigEntry:
|
||||
"""Get the options flow for this handler."""
|
||||
return EmoncmsOptionsFlow(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Initiate a flow via the UI."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
CONF_URL: user_input[CONF_URL],
|
||||
}
|
||||
)
|
||||
result = await get_feed_list(
|
||||
self.hass, user_input[CONF_URL], user_input[CONF_API_KEY]
|
||||
)
|
||||
if not result[CONF_SUCCESS]:
|
||||
errors["base"] = result[CONF_MESSAGE]
|
||||
else:
|
||||
self.include_only_feeds = user_input.get(CONF_ONLY_INCLUDE_FEEDID)
|
||||
self.url = user_input[CONF_URL]
|
||||
self.api_key = user_input[CONF_API_KEY]
|
||||
options = get_options(result[CONF_MESSAGE])
|
||||
self.dropdown = {
|
||||
"options": options,
|
||||
"mode": "dropdown",
|
||||
"multiple": True,
|
||||
}
|
||||
return await self.async_step_choose_feeds()
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_URL): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
),
|
||||
user_input,
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_choose_feeds(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Choose feeds to import."""
|
||||
errors: dict[str, str] = {}
|
||||
include_only_feeds: list = []
|
||||
if user_input or self.include_only_feeds is not None:
|
||||
if self.include_only_feeds is not None:
|
||||
include_only_feeds = self.include_only_feeds
|
||||
elif user_input:
|
||||
include_only_feeds = user_input[CONF_ONLY_INCLUDE_FEEDID]
|
||||
return self.async_create_entry(
|
||||
title=sensor_name(self.url),
|
||||
data={
|
||||
CONF_URL: self.url,
|
||||
CONF_API_KEY: self.api_key,
|
||||
CONF_ONLY_INCLUDE_FEEDID: include_only_feeds,
|
||||
},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="choose_feeds",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_ONLY_INCLUDE_FEEDID,
|
||||
default=include_only_feeds,
|
||||
): selector({"select": self.dropdown}),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_info: ConfigType) -> ConfigFlowResult:
|
||||
"""Import config from yaml."""
|
||||
url = import_info[CONF_URL]
|
||||
api_key = import_info[CONF_API_KEY]
|
||||
include_only_feeds = None
|
||||
if import_info.get(CONF_ONLY_INCLUDE_FEEDID) is not None:
|
||||
include_only_feeds = list(map(str, import_info[CONF_ONLY_INCLUDE_FEEDID]))
|
||||
config = {
|
||||
CONF_API_KEY: api_key,
|
||||
CONF_ONLY_INCLUDE_FEEDID: include_only_feeds,
|
||||
CONF_URL: url,
|
||||
}
|
||||
LOGGER.debug(config)
|
||||
result = await self.async_step_user(config)
|
||||
if errors := result.get("errors"):
|
||||
return self.async_abort(reason=errors["base"])
|
||||
return result
|
||||
|
||||
|
||||
class EmoncmsOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
"""Emoncms Options flow handler."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
errors: dict[str, str] = {}
|
||||
data = self.options if self.options else self._config_entry.data
|
||||
url = data[CONF_URL]
|
||||
api_key = data[CONF_API_KEY]
|
||||
include_only_feeds = data.get(CONF_ONLY_INCLUDE_FEEDID, [])
|
||||
options: list = include_only_feeds
|
||||
result = await get_feed_list(self.hass, url, api_key)
|
||||
if not result[CONF_SUCCESS]:
|
||||
errors["base"] = result[CONF_MESSAGE]
|
||||
else:
|
||||
options = get_options(result[CONF_MESSAGE])
|
||||
dropdown = {"options": options, "mode": "dropdown", "multiple": True}
|
||||
if user_input:
|
||||
include_only_feeds = user_input[CONF_ONLY_INCLUDE_FEEDID]
|
||||
return self.async_create_entry(
|
||||
title=sensor_name(url),
|
||||
data={
|
||||
CONF_URL: url,
|
||||
CONF_API_KEY: api_key,
|
||||
CONF_ONLY_INCLUDE_FEEDID: include_only_feeds,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_ONLY_INCLUDE_FEEDID, default=include_only_feeds
|
||||
): selector({"select": dropdown}),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
|
@ -7,6 +7,9 @@ CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id"
|
|||
CONF_MESSAGE = "message"
|
||||
CONF_SUCCESS = "success"
|
||||
DOMAIN = "emoncms"
|
||||
FEED_ID = "id"
|
||||
FEED_NAME = "name"
|
||||
FEED_TAG = "tag"
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
|
|
@ -18,14 +18,13 @@ class EmoncmsCoordinator(DataUpdateCoordinator[list[dict[str, Any]] | None]):
|
|||
self,
|
||||
hass: HomeAssistant,
|
||||
emoncms_client: EmoncmsClient,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the emoncms data coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
name="emoncms_coordinator",
|
||||
update_interval=scan_interval,
|
||||
update_interval=timedelta(seconds=60),
|
||||
)
|
||||
self.emoncms_client = emoncms_client
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"domain": "emoncms",
|
||||
"name": "Emoncms",
|
||||
"codeowners": ["@borpin", "@alexandrecuer"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/emoncms",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyemoncms==0.0.7"]
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from pyemoncms import EmoncmsClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
|
@ -14,25 +12,33 @@ from homeassistant.components.sensor import (
|
|||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_ID,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_URL,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
STATE_UNKNOWN,
|
||||
UnitOfPower,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import CONF_EXCLUDE_FEEDID, CONF_ONLY_INCLUDE_FEEDID
|
||||
from .config_flow import sensor_name
|
||||
from .const import (
|
||||
CONF_EXCLUDE_FEEDID,
|
||||
CONF_ONLY_INCLUDE_FEEDID,
|
||||
DOMAIN,
|
||||
FEED_ID,
|
||||
FEED_NAME,
|
||||
FEED_TAG,
|
||||
)
|
||||
from .coordinator import EmoncmsCoordinator
|
||||
|
||||
ATTR_FEEDID = "FeedId"
|
||||
|
@ -42,9 +48,7 @@ ATTR_LASTUPDATETIMESTR = "LastUpdatedStr"
|
|||
ATTR_SIZE = "Size"
|
||||
ATTR_TAG = "Tag"
|
||||
ATTR_USERID = "UserId"
|
||||
|
||||
CONF_SENSOR_NAMES = "sensor_names"
|
||||
|
||||
DECIMALS = 2
|
||||
DEFAULT_UNIT = UnitOfPower.WATT
|
||||
|
||||
|
@ -76,20 +80,73 @@ async def async_setup_platform(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Emoncms sensor."""
|
||||
apikey = config[CONF_API_KEY]
|
||||
url = config[CONF_URL]
|
||||
sensorid = config[CONF_ID]
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
config_unit = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
"""Import config from yaml."""
|
||||
if CONF_VALUE_TEMPLATE in config:
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"remove_{CONF_VALUE_TEMPLATE}_{DOMAIN}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key=f"remove_{CONF_VALUE_TEMPLATE}",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"parameter": CONF_VALUE_TEMPLATE,
|
||||
},
|
||||
)
|
||||
return
|
||||
if CONF_ONLY_INCLUDE_FEEDID not in config:
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"missing_{CONF_ONLY_INCLUDE_FEEDID}_{DOMAIN}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=f"missing_{CONF_ONLY_INCLUDE_FEEDID}",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
if (
|
||||
result.get("type") == FlowResultType.CREATE_ENTRY
|
||||
or result.get("reason") == "already_configured"
|
||||
):
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
breaks_in_ha_version="2025.3.0",
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "emoncms",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the emoncms sensors."""
|
||||
config = entry.options if entry.options else entry.data
|
||||
name = sensor_name(config[CONF_URL])
|
||||
exclude_feeds = config.get(CONF_EXCLUDE_FEEDID)
|
||||
include_only_feeds = config.get(CONF_ONLY_INCLUDE_FEEDID)
|
||||
sensor_names = config.get(CONF_SENSOR_NAMES)
|
||||
scan_interval = config.get(CONF_SCAN_INTERVAL, timedelta(seconds=30))
|
||||
|
||||
emoncms_client = EmoncmsClient(url, apikey, session=async_get_clientsession(hass))
|
||||
coordinator = EmoncmsCoordinator(hass, emoncms_client, scan_interval)
|
||||
await coordinator.async_refresh()
|
||||
if exclude_feeds is None and include_only_feeds is None:
|
||||
return
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
elems = coordinator.data
|
||||
if not elems:
|
||||
return
|
||||
|
@ -97,28 +154,15 @@ async def async_setup_platform(
|
|||
sensors: list[EmonCmsSensor] = []
|
||||
|
||||
for idx, elem in enumerate(elems):
|
||||
if exclude_feeds is not None and int(elem["id"]) in exclude_feeds:
|
||||
if include_only_feeds is not None and elem[FEED_ID] not in include_only_feeds:
|
||||
continue
|
||||
|
||||
if include_only_feeds is not None and int(elem["id"]) not in include_only_feeds:
|
||||
continue
|
||||
|
||||
name = None
|
||||
if sensor_names is not None:
|
||||
name = sensor_names.get(int(elem["id"]), None)
|
||||
|
||||
if unit := elem.get("unit"):
|
||||
unit_of_measurement = unit
|
||||
else:
|
||||
unit_of_measurement = config_unit
|
||||
|
||||
sensors.append(
|
||||
EmonCmsSensor(
|
||||
coordinator,
|
||||
entry.entry_id,
|
||||
elem["unit"],
|
||||
name,
|
||||
value_template,
|
||||
unit_of_measurement,
|
||||
str(sensorid),
|
||||
idx,
|
||||
)
|
||||
)
|
||||
|
@ -131,10 +175,9 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
|
|||
def __init__(
|
||||
self,
|
||||
coordinator: EmoncmsCoordinator,
|
||||
name: str | None,
|
||||
value_template: template.Template | None,
|
||||
entry_id: str,
|
||||
unit_of_measurement: str | None,
|
||||
sensorid: str,
|
||||
name: str,
|
||||
idx: int,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
|
@ -143,20 +186,9 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
|
|||
elem = {}
|
||||
if self.coordinator.data:
|
||||
elem = self.coordinator.data[self.idx]
|
||||
if name is None:
|
||||
# Suppress ID in sensor name if it's 1, since most people won't
|
||||
# have more than one EmonCMS source and it's redundant to show the
|
||||
# ID if there's only one.
|
||||
id_for_name = "" if str(sensorid) == "1" else sensorid
|
||||
# Use the feed name assigned in EmonCMS or fall back to the feed ID
|
||||
feed_name = elem.get("name", f"Feed {elem.get('id')}")
|
||||
self._attr_name = f"EmonCMS{id_for_name} {feed_name}"
|
||||
else:
|
||||
self._attr_name = name
|
||||
self._value_template = value_template
|
||||
self._attr_name = f"{name} {elem[FEED_NAME]}"
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
self._sensorid = sensorid
|
||||
|
||||
self._attr_unique_id = f"{entry_id}-{elem[FEED_ID]}"
|
||||
if unit_of_measurement in ("kWh", "Wh"):
|
||||
self._attr_device_class = SensorDeviceClass.ENERGY
|
||||
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
@ -186,9 +218,9 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
|
|||
def _update_attributes(self, elem: dict[str, Any]) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_FEEDID: elem["id"],
|
||||
ATTR_TAG: elem["tag"],
|
||||
ATTR_FEEDNAME: elem["name"],
|
||||
ATTR_FEEDID: elem[FEED_ID],
|
||||
ATTR_TAG: elem[FEED_TAG],
|
||||
ATTR_FEEDNAME: elem[FEED_NAME],
|
||||
}
|
||||
if elem["value"] is not None:
|
||||
self._attr_extra_state_attributes[ATTR_SIZE] = elem["size"]
|
||||
|
@ -199,13 +231,7 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
|
|||
)
|
||||
|
||||
self._attr_native_value = None
|
||||
if self._value_template is not None:
|
||||
self._attr_native_value = (
|
||||
self._value_template.async_render_with_possible_json_value(
|
||||
elem["value"], STATE_UNKNOWN
|
||||
)
|
||||
)
|
||||
elif elem["value"] is not None:
|
||||
if elem["value"] is not None:
|
||||
self._attr_native_value = round(float(elem["value"]), DECIMALS)
|
||||
|
||||
@callback
|
||||
|
|
40
homeassistant/components/emoncms/strings.json
Normal file
40
homeassistant/components/emoncms/strings.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "Server url starting with the protocol (http or https)",
|
||||
"api_key": "Your 32 bits api key"
|
||||
}
|
||||
},
|
||||
"choose_feeds": {
|
||||
"data": {
|
||||
"include_only_feed_id": "Choose feeds to include"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"include_only_feed_id": "[%key:component::emoncms::config::step::choose_feeds::data::include_only_feed_id%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"remove_value_template": {
|
||||
"title": "The {domain} integration cannot start",
|
||||
"description": "Configuring {domain} using YAML is being removed and the `{parameter}` parameter cannot be imported.\n\nPlease remove `{parameter}` from your `{domain}` yaml configuration and restart Home Assistant\n\nAlternatively, you may entirely remove the `{domain}` configuration from your configuration.yaml, restart Home Assistant, and add the {domain} integration manually."
|
||||
},
|
||||
"missing_include_only_feed_id": {
|
||||
"title": "No feed synchronized with the {domain} sensor",
|
||||
"description": "Configuring {domain} using YAML is being removed.\n\nPlease add manually the feeds you want to synchronize with the `configure` button of the integration."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -157,6 +157,7 @@ FLOWS = {
|
|||
"elkm1",
|
||||
"elmax",
|
||||
"elvia",
|
||||
"emoncms",
|
||||
"emonitor",
|
||||
"emulated_roku",
|
||||
"energenie_power_sockets",
|
||||
|
|
|
@ -1569,7 +1569,7 @@
|
|||
"integrations": {
|
||||
"emoncms": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Emoncms"
|
||||
},
|
||||
|
|
|
@ -1 +1,12 @@
|
|||
"""Tests for the emoncms component."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Set up the integration."""
|
||||
entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
|
|
@ -1,10 +1,23 @@
|
|||
"""Fixtures for emoncms integration tests."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
import copy
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.emoncms.const import CONF_ONLY_INCLUDE_FEEDID, DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_URL,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
UNITS = ["kWh", "Wh", "W", "V", "A", "VA", "°C", "°F", "K", "Hz", "hPa", ""]
|
||||
|
||||
|
||||
|
@ -29,16 +42,102 @@ FEEDS = [get_feed(i + 1, unit=unit) for i, unit in enumerate(UNITS)]
|
|||
|
||||
EMONCMS_FAILURE = {"success": False, "message": "failure"}
|
||||
|
||||
FLOW_RESULT = {
|
||||
CONF_API_KEY: "my_api_key",
|
||||
CONF_ONLY_INCLUDE_FEEDID: [str(i + 1) for i in range(len(UNITS))],
|
||||
CONF_URL: "http://1.1.1.1",
|
||||
}
|
||||
|
||||
SENSOR_NAME = "emoncms@1.1.1.1"
|
||||
|
||||
YAML_BASE = {
|
||||
CONF_PLATFORM: "emoncms",
|
||||
CONF_API_KEY: "my_api_key",
|
||||
CONF_ID: 1,
|
||||
CONF_URL: "http://1.1.1.1",
|
||||
}
|
||||
|
||||
YAML = {
|
||||
**YAML_BASE,
|
||||
CONF_ONLY_INCLUDE_FEEDID: [1],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def emoncms_yaml_config() -> ConfigType:
|
||||
"""Mock emoncms yaml configuration."""
|
||||
return {"sensor": YAML}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def emoncms_yaml_config_with_template() -> ConfigType:
|
||||
"""Mock emoncms yaml conf with template parameter."""
|
||||
return {"sensor": {**YAML, CONF_VALUE_TEMPLATE: "{{ value | float + 1500 }}"}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def emoncms_yaml_config_no_include_only_feed_id() -> ConfigType:
|
||||
"""Mock emoncms yaml configuration without include_only_feed_id parameter."""
|
||||
return {"sensor": YAML_BASE}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_entry() -> MockConfigEntry:
|
||||
"""Mock emoncms config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=SENSOR_NAME,
|
||||
data=FLOW_RESULT,
|
||||
)
|
||||
|
||||
|
||||
FLOW_RESULT_NO_FEED = copy.deepcopy(FLOW_RESULT)
|
||||
FLOW_RESULT_NO_FEED[CONF_ONLY_INCLUDE_FEEDID] = None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_no_feed() -> MockConfigEntry:
|
||||
"""Mock emoncms config entry with no feed selected."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=SENSOR_NAME,
|
||||
data=FLOW_RESULT_NO_FEED,
|
||||
)
|
||||
|
||||
|
||||
FLOW_RESULT_SINGLE_FEED = copy.deepcopy(FLOW_RESULT)
|
||||
FLOW_RESULT_SINGLE_FEED[CONF_ONLY_INCLUDE_FEEDID] = ["1"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_single_feed() -> MockConfigEntry:
|
||||
"""Mock emoncms config entry with a single feed exposed."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=SENSOR_NAME,
|
||||
data=FLOW_RESULT_SINGLE_FEED,
|
||||
entry_id="XXXXXXXX",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.emoncms.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def emoncms_client() -> AsyncGenerator[AsyncMock]:
|
||||
"""Mock pyemoncms success response."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.emoncms.sensor.EmoncmsClient", autospec=True
|
||||
"homeassistant.components.emoncms.EmoncmsClient", autospec=True
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.emoncms.coordinator.EmoncmsClient",
|
||||
"homeassistant.components.emoncms.config_flow.EmoncmsClient",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
|
|
|
@ -1,5 +1,40 @@
|
|||
# serializer version: 1
|
||||
# name: test_coordinator_update[sensor.emoncms_parameter_1]
|
||||
# name: test_coordinator_update[sensor.emoncms_1_1_1_1_parameter_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.emoncms_1_1_1_1_parameter_1',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'emoncms@1.1.1.1 parameter 1',
|
||||
'platform': 'emoncms',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'XXXXXXXX-1',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_coordinator_update[sensor.emoncms_1_1_1_1_parameter_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'FeedId': '1',
|
||||
|
@ -10,12 +45,12 @@
|
|||
'Tag': 'tag',
|
||||
'UserId': '1',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'EmonCMS parameter 1',
|
||||
'friendly_name': 'emoncms@1.1.1.1 parameter 1',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.emoncms_parameter_1',
|
||||
'entity_id': 'sensor.emoncms_1_1_1_1_parameter_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
|
143
tests/components/emoncms/test_config_flow.py
Normal file
143
tests/components/emoncms/test_config_flow.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
"""Test emoncms config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.components.emoncms.const import CONF_ONLY_INCLUDE_FEEDID, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import EMONCMS_FAILURE, FLOW_RESULT_SINGLE_FEED, SENSOR_NAME, YAML
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_flow_import_include_feeds(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""YAML import with included feed - success test."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=YAML,
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == SENSOR_NAME
|
||||
assert result["data"] == FLOW_RESULT_SINGLE_FEED
|
||||
|
||||
|
||||
async def test_flow_import_failure(
|
||||
hass: HomeAssistant,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""YAML import - failure test."""
|
||||
emoncms_client.async_request.return_value = EMONCMS_FAILURE
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=YAML,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == EMONCMS_FAILURE["message"]
|
||||
|
||||
|
||||
async def test_flow_import_already_configured(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we abort import data set when entry is already configured."""
|
||||
config_entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=YAML,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
USER_INPUT = {
|
||||
CONF_URL: "http://1.1.1.1",
|
||||
CONF_API_KEY: "my_api_key",
|
||||
}
|
||||
|
||||
|
||||
async def test_user_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we get the user form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_ONLY_INCLUDE_FEEDID: ["1"]},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == SENSOR_NAME
|
||||
assert result["data"] == {**USER_INPUT, CONF_ONLY_INCLUDE_FEEDID: ["1"]}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
USER_OPTIONS = {
|
||||
CONF_ONLY_INCLUDE_FEEDID: ["1"],
|
||||
}
|
||||
|
||||
CONFIG_ENTRY = {
|
||||
CONF_API_KEY: "my_api_key",
|
||||
CONF_ONLY_INCLUDE_FEEDID: ["1"],
|
||||
CONF_URL: "http://1.1.1.1",
|
||||
}
|
||||
|
||||
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
emoncms_client: AsyncMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Options flow - success test."""
|
||||
await setup_integration(hass, config_entry)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=USER_OPTIONS,
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == CONFIG_ENTRY
|
||||
assert config_entry.options == CONFIG_ENTRY
|
||||
|
||||
|
||||
async def test_options_flow_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
emoncms_client: AsyncMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Options flow - test failure."""
|
||||
emoncms_client.async_request.return_value = EMONCMS_FAILURE
|
||||
await setup_integration(hass, config_entry)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert result["errors"]["base"] == "failure"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
40
tests/components/emoncms/test_init.py
Normal file
40
tests/components/emoncms/test_init.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
"""Test Emoncms component setup process."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import EMONCMS_FAILURE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_load_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test load and unload entry."""
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_failure(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test load failure."""
|
||||
emoncms_client.async_request.return_value = EMONCMS_FAILURE
|
||||
config_entry.add_to_hass(hass)
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
|
@ -1,54 +1,112 @@
|
|||
"""Test emoncms sensor."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.emoncms.const import CONF_ONLY_INCLUDE_FEEDID, DOMAIN
|
||||
from homeassistant.components.emoncms.const import DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_PLATFORM, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import EMONCMS_FAILURE, FEEDS, get_feed
|
||||
from . import setup_integration
|
||||
from .conftest import EMONCMS_FAILURE, get_feed
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
YAML = {
|
||||
CONF_PLATFORM: "emoncms",
|
||||
CONF_API_KEY: "my_api_key",
|
||||
CONF_ID: 1,
|
||||
CONF_URL: "http://1.1.1.1",
|
||||
CONF_ONLY_INCLUDE_FEEDID: [1, 2],
|
||||
"scan_interval": 30,
|
||||
}
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def emoncms_yaml_config() -> ConfigType:
|
||||
"""Mock emoncms configuration from yaml."""
|
||||
return {"sensor": YAML}
|
||||
async def test_deprecated_yaml(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
emoncms_yaml_config: ConfigType,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test an issue is created when we import from yaml config."""
|
||||
|
||||
await async_setup_component(hass, SENSOR_DOMAIN, emoncms_yaml_config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=HOMEASSISTANT_DOMAIN, issue_id=f"deprecated_yaml_{DOMAIN}"
|
||||
)
|
||||
|
||||
|
||||
def get_entity_ids(feeds: list[dict[str, Any]]) -> list[str]:
|
||||
"""Get emoncms entity ids."""
|
||||
return [
|
||||
f"{SENSOR_DOMAIN}.{DOMAIN}_{feed["name"].replace(' ', '_')}" for feed in feeds
|
||||
]
|
||||
async def test_yaml_with_template(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
emoncms_yaml_config_with_template: ConfigType,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test an issue is created when we import a yaml config with a value_template parameter."""
|
||||
|
||||
await async_setup_component(hass, SENSOR_DOMAIN, emoncms_yaml_config_with_template)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=DOMAIN, issue_id=f"remove_value_template_{DOMAIN}"
|
||||
)
|
||||
|
||||
|
||||
def get_feeds(nbs: list[int]) -> list[dict[str, Any]]:
|
||||
"""Get feeds."""
|
||||
return [feed for feed in FEEDS if feed["id"] in str(nbs)]
|
||||
async def test_yaml_no_include_only_feed_id(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
emoncms_yaml_config_no_include_only_feed_id: ConfigType,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test an issue is created when we import a yaml config without a include_only_feed_id parameter."""
|
||||
|
||||
await async_setup_component(
|
||||
hass, SENSOR_DOMAIN, emoncms_yaml_config_no_include_only_feed_id
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=DOMAIN, issue_id=f"missing_include_only_feed_id_{DOMAIN}"
|
||||
)
|
||||
|
||||
|
||||
async def test_no_feed_selected(
|
||||
hass: HomeAssistant,
|
||||
config_no_feed: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test with no feed selected."""
|
||||
await setup_integration(hass, config_no_feed)
|
||||
|
||||
assert config_no_feed.state is ConfigEntryState.LOADED
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_no_feed.entry_id
|
||||
)
|
||||
assert entity_entries == []
|
||||
|
||||
|
||||
async def test_no_feed_broadcast(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test with no feed broadcasted."""
|
||||
emoncms_client.async_request.return_value = {"success": True, "message": []}
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
assert entity_entries == []
|
||||
|
||||
|
||||
async def test_coordinator_update(
|
||||
hass: HomeAssistant,
|
||||
emoncms_yaml_config: ConfigType,
|
||||
config_single_feed: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
emoncms_client: AsyncMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
|
@ -59,12 +117,11 @@ async def test_coordinator_update(
|
|||
"success": True,
|
||||
"message": [get_feed(1, unit="°C")],
|
||||
}
|
||||
await async_setup_component(hass, SENSOR_DOMAIN, emoncms_yaml_config)
|
||||
await hass.async_block_till_done()
|
||||
feeds = get_feeds([1])
|
||||
for entity_id in get_entity_ids(feeds):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state == snapshot(name=entity_id)
|
||||
await setup_integration(hass, config_single_feed)
|
||||
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, snapshot, config_single_feed.entry_id
|
||||
)
|
||||
|
||||
async def skip_time() -> None:
|
||||
freezer.tick(60)
|
||||
|
@ -78,8 +135,12 @@ async def test_coordinator_update(
|
|||
|
||||
await skip_time()
|
||||
|
||||
for entity_id in get_entity_ids(feeds):
|
||||
state = hass.states.get(entity_id)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_single_feed.entry_id
|
||||
)
|
||||
|
||||
for entity_entry in entity_entries:
|
||||
state = hass.states.get(entity_entry.entity_id)
|
||||
assert state.attributes["LastUpdated"] == 1665509670
|
||||
assert state.state == "24.04"
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue