Co2signal configflow (#53193)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Daniel Hjelseth Høyer 2021-07-20 06:30:00 +02:00 committed by GitHub
parent 562aa74c77
commit f0b28c90bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 740 additions and 41 deletions

View File

@ -152,7 +152,6 @@ omit =
homeassistant/components/clicksend/notify.py
homeassistant/components/clicksend_tts/notify.py
homeassistant/components/cmus/media_player.py
homeassistant/components/co2signal/*
homeassistant/components/coinbase/sensor.py
homeassistant/components/comed_hourly_pricing/sensor.py
homeassistant/components/comfoconnect/fan.py

View File

@ -1 +1,20 @@
"""The co2signal component."""
"""The CO2 Signal integration."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN # noqa: F401
PLATFORMS = ["sensor"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up CO2 Signal from a config entry."""
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,249 @@
"""Config flow for Co2signal integration."""
from __future__ import annotations
import logging
from typing import Any
import CO2Signal
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_TOKEN
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from .const import CONF_COUNTRY_CODE, DOMAIN
from .util import get_extra_name
_LOGGER = logging.getLogger(__name__)
TYPE_USE_HOME = "Use home location"
TYPE_SPECIFY_COORDINATES = "Specify coordinates"
TYPE_SPECIFY_COUNTRY = "Specify country code"
def _get_entry_type(config: dict) -> str:
"""Get entry type from the configuration."""
if CONF_LATITUDE in config:
return TYPE_SPECIFY_COORDINATES
if CONF_COUNTRY_CODE in config:
return TYPE_SPECIFY_COUNTRY
return TYPE_USE_HOME
def _validate_info(hass, config: dict) -> dict:
"""Validate the passed in info."""
if CONF_COUNTRY_CODE in config:
latitude = None
longitude = None
else:
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
try:
data = CO2Signal.get_latest(
config[CONF_API_KEY],
config.get(CONF_COUNTRY_CODE),
latitude,
longitude,
wait=False,
)
except ValueError as err:
err_str = str(err)
if "Invalid authentication credentials" in err_str:
raise InvalidAuth from err
if "API rate limit exceeded." in err_str:
raise APIRatelimitExceeded from err
_LOGGER.exception("Unexpected exception")
raise UnknownError from err
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
raise UnknownError from err
else:
if data.get("status") != "ok":
_LOGGER.exception("Unexpected response: %s", data)
raise UnknownError
return data
class CO2Error(HomeAssistantError):
"""Base error."""
class InvalidAuth(CO2Error):
"""Raised when invalid authentication credentials are provided."""
class APIRatelimitExceeded(CO2Error):
"""Raised when the API rate limit is exceeded."""
class UnknownError(CO2Error):
"""Raised when an unknown error occurs."""
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Co2signal."""
VERSION = 1
_data: dict | None
async def async_step_import(self, import_info):
"""Set the config entry up from yaml."""
data = {CONF_API_KEY: import_info[CONF_TOKEN]}
if CONF_COUNTRY_CODE in import_info:
data[CONF_COUNTRY_CODE] = import_info[CONF_COUNTRY_CODE]
new_entry_type = TYPE_SPECIFY_COUNTRY
elif (
CONF_LATITUDE in import_info
and import_info[CONF_LATITUDE] != self.hass.config.latitude
and import_info[CONF_LONGITUDE] != self.hass.config.longitude
):
data[CONF_LATITUDE] = import_info[CONF_LATITUDE]
data[CONF_LONGITUDE] = import_info[CONF_LONGITUDE]
new_entry_type = TYPE_SPECIFY_COORDINATES
else:
new_entry_type = TYPE_USE_HOME
for entry in self._async_current_entries(include_ignore=True):
if entry.source == config_entries.SOURCE_IGNORE:
continue
if (cur_entry_type := _get_entry_type(entry.data)) != new_entry_type:
continue
if cur_entry_type == TYPE_USE_HOME and new_entry_type == TYPE_USE_HOME:
return self.async_abort(reason="already_configured")
if (
cur_entry_type == TYPE_SPECIFY_COUNTRY
and data[CONF_COUNTRY_CODE] == entry.data[CONF_COUNTRY_CODE]
):
return self.async_abort(reason="already_configured")
if (
cur_entry_type == TYPE_SPECIFY_COORDINATES
and data[CONF_LATITUDE] == entry.data[CONF_LATITUDE]
and data[CONF_LONGITUDE] == entry.data[CONF_LONGITUDE]
):
return self.async_abort(reason="already_configured")
try:
await self.hass.async_add_executor_job(_validate_info, self.hass, data)
except CO2Error:
return self.async_abort(reason="unknown")
return self.async_create_entry(
title=get_extra_name(self.hass, data) or "CO2 Signal", data=data
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
data_schema = vol.Schema(
{
vol.Required("location", default=TYPE_USE_HOME): vol.In(
(
TYPE_USE_HOME,
TYPE_SPECIFY_COORDINATES,
TYPE_SPECIFY_COUNTRY,
)
),
vol.Required(CONF_API_KEY): cv.string,
}
)
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=data_schema,
)
data = {CONF_API_KEY: user_input[CONF_API_KEY]}
if user_input["location"] == TYPE_SPECIFY_COORDINATES:
self._data = data
return await self.async_step_coordinates()
if user_input["location"] == TYPE_SPECIFY_COUNTRY:
self._data = data
return await self.async_step_country()
return await self._validate_and_create("user", data_schema, data)
async def async_step_coordinates(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Validate coordinates."""
data_schema = vol.Schema(
{
vol.Required(
CONF_LATITUDE,
): cv.latitude,
vol.Required(
CONF_LONGITUDE,
): cv.longitude,
}
)
if user_input is None:
return self.async_show_form(step_id="coordinates", data_schema=data_schema)
assert self._data is not None
return await self._validate_and_create(
"coordinates", data_schema, {**self._data, **user_input}
)
async def async_step_country(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Validate country."""
data_schema = vol.Schema(
{
vol.Required(CONF_COUNTRY_CODE): cv.string,
}
)
if user_input is None:
return self.async_show_form(step_id="country", data_schema=data_schema)
assert self._data is not None
return await self._validate_and_create(
"country", data_schema, {**self._data, **user_input}
)
async def _validate_and_create(
self, step_id: str, data_schema: vol.Schema, data: dict
) -> FlowResult:
"""Validate data and show form if it is invalid."""
errors: dict[str, str] = {}
try:
await self.hass.async_add_executor_job(_validate_info, self.hass, data)
except InvalidAuth:
errors["base"] = "invalid_auth"
except APIRatelimitExceeded:
errors["base"] = "api_ratelimit"
except UnknownError:
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=get_extra_name(self.hass, data) or "CO2 Signal",
data=data,
)
return self.async_show_form(
step_id=step_id,
data_schema=data_schema,
errors=errors,
)

View File

@ -0,0 +1,11 @@
"""Constants for the Co2signal integration."""
DOMAIN = "co2signal"
CONF_COUNTRY_CODE = "country_code"
ATTRIBUTION = "Data provided by CO2signal"
MSG_LOCATION = (
"Please use either coordinates or the country code. "
"For the coordinates, "
"you need to use both latitude and longitude."
)

View File

@ -2,7 +2,10 @@
"domain": "co2signal",
"name": "CO2 Signal",
"documentation": "https://www.home-assistant.io/integrations/co2signal",
"requirements": ["co2signal==0.4.2"],
"requirements": [
"co2signal==0.4.2"
],
"codeowners": [],
"iot_class": "cloud_polling"
}
"iot_class": "cloud_polling",
"config_flow": true
}

View File

@ -5,9 +5,14 @@ import logging
import CO2Signal
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_NAME,
CONF_API_KEY,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_TOKEN,
@ -15,18 +20,12 @@ from homeassistant.const import (
)
import homeassistant.helpers.config_validation as cv
CONF_COUNTRY_CODE = "country_code"
from .const import ATTRIBUTION, CONF_COUNTRY_CODE, DOMAIN, MSG_LOCATION
from .util import get_extra_name
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=3)
ATTRIBUTION = "Data provided by CO2signal"
MSG_LOCATION = (
"Please use either coordinates or the country code. "
"For the coordinates, "
"you need to use both latitude and longitude."
)
CO2_INTENSITY_UNIT = f"CO2eq/{ENERGY_KILO_WATT_HOUR}"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@ -38,16 +37,31 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the CO2signal sensor."""
token = config[CONF_TOKEN]
lat = config.get(CONF_LATITUDE, hass.config.latitude)
lon = config.get(CONF_LONGITUDE, hass.config.longitude)
country_code = config.get(CONF_COUNTRY_CODE)
await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=config,
)
_LOGGER.debug("Setting up the sensor using the %s", country_code)
add_entities([CO2Sensor(token, country_code, lat, lon)], True)
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the CO2signal sensor."""
name = "CO2 intensity"
if extra_name := get_extra_name(hass, entry.data):
name += f" - {extra_name}"
async_add_entities(
[
CO2Sensor(
name,
entry.data,
entry_id=entry.entry_id,
)
],
True,
)
class CO2Sensor(SensorEntity):
@ -56,33 +70,37 @@ class CO2Sensor(SensorEntity):
_attr_icon = "mdi:molecule-co2"
_attr_unit_of_measurement = CO2_INTENSITY_UNIT
def __init__(self, token, country_code, lat, lon):
def __init__(self, name, config, entry_id):
"""Initialize the sensor."""
self._token = token
self._country_code = country_code
self._latitude = lat
self._longitude = lon
if country_code is not None:
device_name = country_code
else:
device_name = f"{round(self._latitude, 2)}/{round(self._longitude, 2)}"
self._attr_name = f"CO2 intensity - {device_name}"
self._config = config
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._attr_device_info = {
ATTR_IDENTIFIERS: {(DOMAIN, entry_id)},
ATTR_NAME: "CO2 signal",
ATTR_MANUFACTURER: "Tmrow.com",
"entry_type": "service",
}
self._attr_unique_id = f"{entry_id}_co2intensity"
def update(self):
"""Get the latest data and updates the states."""
_LOGGER.debug("Update data for %s", self.name)
if self._country_code is not None:
data = CO2Signal.get_latest_carbon_intensity(
self._token, country_code=self._country_code
)
if CONF_COUNTRY_CODE in self._config:
kwargs = {"country_code": self._config[CONF_COUNTRY_CODE]}
elif CONF_LATITUDE in self._config:
kwargs = {
"latitude": self._config[CONF_LATITUDE],
"longitude": self._config[CONF_LONGITUDE],
}
else:
data = CO2Signal.get_latest_carbon_intensity(
self._token, latitude=self._latitude, longitude=self._longitude
)
kwargs = {
"latitude": self.hass.config.latitude,
"longitude": self.hass.config.longitude,
}
self._attr_state = round(data, 2)
self._attr_state = round(
CO2Signal.get_latest_carbon_intensity(self._config[CONF_API_KEY], **kwargs),
2,
)

View File

@ -0,0 +1,34 @@
{
"config": {
"step": {
"user": {
"data": {
"location": "Get data for",
"api_key": "[%key:common::config_flow::data::access_token%]"
},
"description": "Visit https://co2signal.com/ to request a token."
},
"coordinates": {
"data": {
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
}
},
"country": {
"data": {
"country_code": "Country code"
}
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"api_ratelimit": "API Ratelimit exceeded"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"api_ratelimit": "API Ratelimit exceeded"
}
}
}

View File

@ -0,0 +1,34 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured",
"api_ratelimit": "API Ratelimit exceeded",
"unknown": "Unexpected error"
},
"error": {
"api_ratelimit": "API Ratelimit exceeded",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"coordinates": {
"data": {
"latitude": "Latitude",
"longitude": "Longitude"
}
},
"country": {
"data": {
"country_code": "Country code"
}
},
"user": {
"data": {
"api_key": "Access Token",
"location": "Get data for"
},
"description": "Visit https://co2signal.com/ to request a token."
}
}
}
}

View File

@ -0,0 +1,18 @@
"""Utils for CO2 signal."""
from __future__ import annotations
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from .const import CONF_COUNTRY_CODE
def get_extra_name(hass: HomeAssistant, config: dict) -> str | None:
"""Return the extra name describing the location if not home."""
if CONF_COUNTRY_CODE in config:
return config[CONF_COUNTRY_CODE]
if CONF_LATITUDE in config:
return f"{round(config[CONF_LATITUDE], 2)}, {round(config[CONF_LONGITUDE], 2)}"
return None

View File

@ -45,6 +45,7 @@ FLOWS = [
"cert_expiry",
"climacell",
"cloudflare",
"co2signal",
"coinbase",
"control4",
"coolmaster",

View File

@ -249,6 +249,9 @@ buienradar==1.0.4
# homeassistant.components.caldav
caldav==0.7.1
# homeassistant.components.co2signal
co2signal==0.4.2
# homeassistant.components.coinbase
coinbase==2.1.0

View File

@ -0,0 +1,11 @@
"""Tests for the CO2 Signal integration."""
VALID_PAYLOAD = {
"status": "ok",
"countryCode": "FR",
"data": {
"carbonIntensity": 45.98623190095805,
"fossilFuelPercentage": 5.461182741937103,
},
"units": {"carbonIntensity": "gCO2eq/kWh"},
}

View File

@ -0,0 +1,299 @@
"""Test the CO2 Signal config flow."""
from unittest.mock import patch
import pytest
from homeassistant import config_entries, setup
from homeassistant.components.co2signal import DOMAIN, config_flow
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
from homeassistant.setup import async_setup_component
from . import VALID_PAYLOAD
from tests.common import MockConfigEntry
async def test_form_home(hass: HomeAssistant) -> None:
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
return_value=VALID_PAYLOAD,
), patch(
"homeassistant.components.co2signal.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"location": config_flow.TYPE_USE_HOME,
"api_key": "api_key",
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "CO2 Signal"
assert result2["data"] == {
"api_key": "api_key",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_coordinates(hass: HomeAssistant) -> None:
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"location": config_flow.TYPE_SPECIFY_COORDINATES,
"api_key": "api_key",
},
)
assert result2["type"] == RESULT_TYPE_FORM
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
return_value=VALID_PAYLOAD,
), patch(
"homeassistant.components.co2signal.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
"latitude": 12.3,
"longitude": 45.6,
},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "12.3, 45.6"
assert result3["data"] == {
"latitude": 12.3,
"longitude": 45.6,
"api_key": "api_key",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_country(hass: HomeAssistant) -> None:
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"location": config_flow.TYPE_SPECIFY_COUNTRY,
"api_key": "api_key",
},
)
assert result2["type"] == RESULT_TYPE_FORM
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
return_value=VALID_PAYLOAD,
), patch(
"homeassistant.components.co2signal.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
"country_code": "fr",
},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "fr"
assert result3["data"] == {
"country_code": "fr",
"api_key": "api_key",
}
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
"err_str,err_code",
[
("Invalid authentication credentials", "invalid_auth"),
("API rate limit exceeded.", "api_ratelimit"),
("Something else", "unknown"),
],
)
async def test_form_error_handling(hass: HomeAssistant, err_str, err_code) -> None:
"""Test we handle expected errors."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
side_effect=ValueError(err_str),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"location": config_flow.TYPE_USE_HOME,
"api_key": "api_key",
},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": err_code}
async def test_form_error_unexpected_error(hass: HomeAssistant) -> None:
"""Test we handle unexpected error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
side_effect=Exception("Boom"),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"location": config_flow.TYPE_USE_HOME,
"api_key": "api_key",
},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "unknown"}
async def test_form_error_unexpected_data(hass: HomeAssistant) -> None:
"""Test we handle unexpected data."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
return_value={"status": "error"},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"location": config_flow.TYPE_USE_HOME,
"api_key": "api_key",
},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "unknown"}
async def test_import(hass: HomeAssistant) -> None:
"""Test we import correctly."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
return_value=VALID_PAYLOAD,
):
assert await async_setup_component(
hass, "sensor", {"sensor": {"platform": "co2signal", "token": "1234"}}
)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries("co2signal")) == 1
state = hass.states.get("sensor.co2_intensity")
assert state is not None
assert state.state == "45.99"
assert state.name == "CO2 intensity"
async def test_import_abort_existing_home(hass: HomeAssistant) -> None:
"""Test we abort if home entry found."""
await setup.async_setup_component(hass, "persistent_notification", {})
MockConfigEntry(domain="co2signal", data={"api_key": "abcd"}).add_to_hass(hass)
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
return_value=VALID_PAYLOAD,
):
assert await async_setup_component(
hass, "sensor", {"sensor": {"platform": "co2signal", "token": "1234"}}
)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries("co2signal")) == 1
async def test_import_abort_existing_country(hass: HomeAssistant) -> None:
"""Test we abort if existing country found."""
await setup.async_setup_component(hass, "persistent_notification", {})
MockConfigEntry(
domain="co2signal", data={"api_key": "abcd", "country_code": "nl"}
).add_to_hass(hass)
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
return_value=VALID_PAYLOAD,
):
assert await async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "co2signal",
"token": "1234",
"country_code": "nl",
}
},
)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries("co2signal")) == 1
async def test_import_abort_existing_coordinates(hass: HomeAssistant) -> None:
"""Test we abort if existing coordinates found."""
await setup.async_setup_component(hass, "persistent_notification", {})
MockConfigEntry(
domain="co2signal", data={"api_key": "abcd", "latitude": 1, "longitude": 2}
).add_to_hass(hass)
with patch(
"homeassistant.components.co2signal.config_flow.CO2Signal.get_latest",
return_value=VALID_PAYLOAD,
):
assert await async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "co2signal",
"token": "1234",
"latitude": 1,
"longitude": 2,
}
},
)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries("co2signal")) == 1