mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Co2signal configflow (#53193)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
562aa74c77
commit
f0b28c90bf
@ -152,7 +152,6 @@ omit =
|
|||||||
homeassistant/components/clicksend/notify.py
|
homeassistant/components/clicksend/notify.py
|
||||||
homeassistant/components/clicksend_tts/notify.py
|
homeassistant/components/clicksend_tts/notify.py
|
||||||
homeassistant/components/cmus/media_player.py
|
homeassistant/components/cmus/media_player.py
|
||||||
homeassistant/components/co2signal/*
|
|
||||||
homeassistant/components/coinbase/sensor.py
|
homeassistant/components/coinbase/sensor.py
|
||||||
homeassistant/components/comed_hourly_pricing/sensor.py
|
homeassistant/components/comed_hourly_pricing/sensor.py
|
||||||
homeassistant/components/comfoconnect/fan.py
|
homeassistant/components/comfoconnect/fan.py
|
||||||
|
@ -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)
|
||||||
|
249
homeassistant/components/co2signal/config_flow.py
Normal file
249
homeassistant/components/co2signal/config_flow.py
Normal 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,
|
||||||
|
)
|
11
homeassistant/components/co2signal/const.py
Normal file
11
homeassistant/components/co2signal/const.py
Normal 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."
|
||||||
|
)
|
@ -2,7 +2,10 @@
|
|||||||
"domain": "co2signal",
|
"domain": "co2signal",
|
||||||
"name": "CO2 Signal",
|
"name": "CO2 Signal",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/co2signal",
|
"documentation": "https://www.home-assistant.io/integrations/co2signal",
|
||||||
"requirements": ["co2signal==0.4.2"],
|
"requirements": [
|
||||||
|
"co2signal==0.4.2"
|
||||||
|
],
|
||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling",
|
||||||
}
|
"config_flow": true
|
||||||
|
}
|
@ -5,9 +5,14 @@ import logging
|
|||||||
import CO2Signal
|
import CO2Signal
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION,
|
ATTR_ATTRIBUTION,
|
||||||
|
ATTR_IDENTIFIERS,
|
||||||
|
ATTR_MANUFACTURER,
|
||||||
|
ATTR_NAME,
|
||||||
|
CONF_API_KEY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
CONF_LONGITUDE,
|
CONF_LONGITUDE,
|
||||||
CONF_TOKEN,
|
CONF_TOKEN,
|
||||||
@ -15,18 +20,12 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
SCAN_INTERVAL = timedelta(minutes=3)
|
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}"
|
CO2_INTENSITY_UNIT = f"CO2eq/{ENERGY_KILO_WATT_HOUR}"
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
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."""
|
"""Set up the CO2signal sensor."""
|
||||||
token = config[CONF_TOKEN]
|
await hass.config_entries.flow.async_init(
|
||||||
lat = config.get(CONF_LATITUDE, hass.config.latitude)
|
DOMAIN,
|
||||||
lon = config.get(CONF_LONGITUDE, hass.config.longitude)
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
country_code = config.get(CONF_COUNTRY_CODE)
|
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):
|
class CO2Sensor(SensorEntity):
|
||||||
@ -56,33 +70,37 @@ class CO2Sensor(SensorEntity):
|
|||||||
_attr_icon = "mdi:molecule-co2"
|
_attr_icon = "mdi:molecule-co2"
|
||||||
_attr_unit_of_measurement = CO2_INTENSITY_UNIT
|
_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."""
|
"""Initialize the sensor."""
|
||||||
self._token = token
|
self._config = config
|
||||||
self._country_code = country_code
|
self._attr_name = name
|
||||||
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._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
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):
|
def update(self):
|
||||||
"""Get the latest data and updates the states."""
|
"""Get the latest data and updates the states."""
|
||||||
|
|
||||||
_LOGGER.debug("Update data for %s", self.name)
|
_LOGGER.debug("Update data for %s", self.name)
|
||||||
|
|
||||||
if self._country_code is not None:
|
if CONF_COUNTRY_CODE in self._config:
|
||||||
data = CO2Signal.get_latest_carbon_intensity(
|
kwargs = {"country_code": self._config[CONF_COUNTRY_CODE]}
|
||||||
self._token, country_code=self._country_code
|
elif CONF_LATITUDE in self._config:
|
||||||
)
|
kwargs = {
|
||||||
|
"latitude": self._config[CONF_LATITUDE],
|
||||||
|
"longitude": self._config[CONF_LONGITUDE],
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
data = CO2Signal.get_latest_carbon_intensity(
|
kwargs = {
|
||||||
self._token, latitude=self._latitude, longitude=self._longitude
|
"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,
|
||||||
|
)
|
||||||
|
34
homeassistant/components/co2signal/strings.json
Normal file
34
homeassistant/components/co2signal/strings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
homeassistant/components/co2signal/translations/en.json
Normal file
34
homeassistant/components/co2signal/translations/en.json
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
homeassistant/components/co2signal/util.py
Normal file
18
homeassistant/components/co2signal/util.py
Normal 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
|
@ -45,6 +45,7 @@ FLOWS = [
|
|||||||
"cert_expiry",
|
"cert_expiry",
|
||||||
"climacell",
|
"climacell",
|
||||||
"cloudflare",
|
"cloudflare",
|
||||||
|
"co2signal",
|
||||||
"coinbase",
|
"coinbase",
|
||||||
"control4",
|
"control4",
|
||||||
"coolmaster",
|
"coolmaster",
|
||||||
|
@ -249,6 +249,9 @@ buienradar==1.0.4
|
|||||||
# homeassistant.components.caldav
|
# homeassistant.components.caldav
|
||||||
caldav==0.7.1
|
caldav==0.7.1
|
||||||
|
|
||||||
|
# homeassistant.components.co2signal
|
||||||
|
co2signal==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.coinbase
|
# homeassistant.components.coinbase
|
||||||
coinbase==2.1.0
|
coinbase==2.1.0
|
||||||
|
|
||||||
|
11
tests/components/co2signal/__init__.py
Normal file
11
tests/components/co2signal/__init__.py
Normal 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"},
|
||||||
|
}
|
299
tests/components/co2signal/test_config_flow.py
Normal file
299
tests/components/co2signal/test_config_flow.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user