mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Add WattTime integration (#56149)
This commit is contained in:
parent
9059ee6604
commit
aabc8cd2d5
@ -1178,6 +1178,8 @@ omit =
|
||||
homeassistant/components/waterfurnace/*
|
||||
homeassistant/components/watson_iot/*
|
||||
homeassistant/components/watson_tts/tts.py
|
||||
homeassistant/components/watttime/__init__.py
|
||||
homeassistant/components/watttime/sensor.py
|
||||
homeassistant/components/waze_travel_time/__init__.py
|
||||
homeassistant/components/waze_travel_time/helpers.py
|
||||
homeassistant/components/waze_travel_time/sensor.py
|
||||
|
@ -574,6 +574,7 @@ homeassistant/components/wake_on_lan/* @ntilley905
|
||||
homeassistant/components/wallbox/* @hesselonline
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/watttime/* @bachya
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/webostv/* @bendavid @thecode
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
|
78
homeassistant/components/watttime/__init__.py
Normal file
78
homeassistant/components/watttime/__init__.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""The WattTime integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from aiowatttime import Client
|
||||
from aiowatttime.emissions import RealTimeEmissionsResponseType
|
||||
from aiowatttime.errors import WattTimeError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DATA_COORDINATOR, DOMAIN, LOGGER
|
||||
|
||||
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
PLATFORMS: list[str] = ["sensor"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up WattTime from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}})
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
try:
|
||||
client = await Client.async_login(
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
session=session,
|
||||
logger=LOGGER,
|
||||
)
|
||||
except WattTimeError as err:
|
||||
LOGGER.error("Error while authenticating with WattTime: %s", err)
|
||||
return False
|
||||
|
||||
async def async_update_data() -> RealTimeEmissionsResponseType:
|
||||
"""Get the latest realtime emissions data."""
|
||||
try:
|
||||
return await client.emissions.async_get_realtime_emissions(
|
||||
entry.data[CONF_LATITUDE], entry.data[CONF_LONGITUDE]
|
||||
)
|
||||
except WattTimeError as err:
|
||||
raise UpdateFailed(
|
||||
f"Error while requesting data from WattTime: {err}"
|
||||
) from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
LOGGER,
|
||||
name=entry.title,
|
||||
update_interval=DEFAULT_UPDATE_INTERVAL,
|
||||
update_method=async_update_data,
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
165
homeassistant/components/watttime/config_flow.py
Normal file
165
homeassistant/components/watttime/config_flow.py
Normal file
@ -0,0 +1,165 @@
|
||||
"""Config flow for WattTime integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiowatttime import Client
|
||||
from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
from .const import (
|
||||
CONF_BALANCING_AUTHORITY,
|
||||
CONF_BALANCING_AUTHORITY_ABBREV,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
CONF_LOCATION_TYPE = "location_type"
|
||||
|
||||
LOCATION_TYPE_COORDINATES = "Specify coordinates"
|
||||
LOCATION_TYPE_HOME = "Use home location"
|
||||
|
||||
STEP_COORDINATES_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_LATITUDE): cv.latitude,
|
||||
vol.Required(CONF_LONGITUDE): cv.longitude,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_LOCATION_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_LOCATION_TYPE): vol.In(
|
||||
[LOCATION_TYPE_HOME, LOCATION_TYPE_COORDINATES]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for WattTime."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
self._client: Client | None = None
|
||||
self._password: str | None = None
|
||||
self._username: str | None = None
|
||||
|
||||
async def async_step_coordinates(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the coordinates step."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="coordinates", data_schema=STEP_COORDINATES_DATA_SCHEMA
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self._client
|
||||
|
||||
unique_id = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}"
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
try:
|
||||
grid_region = await self._client.emissions.async_get_grid_region(
|
||||
user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE]
|
||||
)
|
||||
except CoordinatesNotFoundError:
|
||||
return self.async_show_form(
|
||||
step_id="coordinates",
|
||||
data_schema=STEP_COORDINATES_DATA_SCHEMA,
|
||||
errors={CONF_LATITUDE: "unknown_coordinates"},
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
LOGGER.exception("Unexpected exception while getting region: %s", err)
|
||||
return self.async_show_form(
|
||||
step_id="coordinates",
|
||||
data_schema=STEP_COORDINATES_DATA_SCHEMA,
|
||||
errors={"base": "unknown"},
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=unique_id,
|
||||
data={
|
||||
CONF_USERNAME: self._username,
|
||||
CONF_PASSWORD: self._password,
|
||||
CONF_LATITUDE: user_input[CONF_LATITUDE],
|
||||
CONF_LONGITUDE: user_input[CONF_LONGITUDE],
|
||||
CONF_BALANCING_AUTHORITY: grid_region["name"],
|
||||
CONF_BALANCING_AUTHORITY_ABBREV: grid_region["abbrev"],
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_location(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the "pick a location" step."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="location", data_schema=STEP_LOCATION_DATA_SCHEMA
|
||||
)
|
||||
|
||||
if user_input[CONF_LOCATION_TYPE] == LOCATION_TYPE_COORDINATES:
|
||||
return self.async_show_form(
|
||||
step_id="coordinates", data_schema=STEP_COORDINATES_DATA_SCHEMA
|
||||
)
|
||||
return await self.async_step_coordinates(
|
||||
{
|
||||
CONF_LATITUDE: self.hass.config.latitude,
|
||||
CONF_LONGITUDE: self.hass.config.longitude,
|
||||
}
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
|
||||
try:
|
||||
self._client = await Client.async_login(
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
session=session,
|
||||
)
|
||||
except InvalidCredentialsError:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors={CONF_USERNAME: "invalid_auth"},
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
LOGGER.exception("Unexpected exception while logging in: %s", err)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors={"base": "unknown"},
|
||||
)
|
||||
|
||||
self._username = user_input[CONF_USERNAME]
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
return await self.async_step_location()
|
11
homeassistant/components/watttime/const.py
Normal file
11
homeassistant/components/watttime/const.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Constants for the WattTime integration."""
|
||||
import logging
|
||||
|
||||
DOMAIN = "watttime"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
CONF_BALANCING_AUTHORITY = "balancing_authority"
|
||||
CONF_BALANCING_AUTHORITY_ABBREV = "balancing_authority_abbreviation"
|
||||
|
||||
DATA_COORDINATOR = "coordinator"
|
17
homeassistant/components/watttime/manifest.json
Normal file
17
homeassistant/components/watttime/manifest.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"domain": "watttime",
|
||||
"name": "WattTime",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/watttime",
|
||||
"requirements": [
|
||||
"aiowatttime==0.1.1"
|
||||
],
|
||||
"ssdp": [],
|
||||
"zeroconf": [],
|
||||
"homekit": {},
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@bachya"
|
||||
],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
134
homeassistant/components/watttime/sensor.py
Normal file
134
homeassistant/components/watttime/sensor.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""Support for WattTime sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
MASS_POUNDS,
|
||||
PERCENTAGE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_BALANCING_AUTHORITY,
|
||||
CONF_BALANCING_AUTHORITY_ABBREV,
|
||||
DATA_COORDINATOR,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
ATTR_BALANCING_AUTHORITY = "balancing_authority"
|
||||
|
||||
DEFAULT_ATTRIBUTION = "Pickup data provided by WattTime"
|
||||
|
||||
SENSOR_TYPE_REALTIME_EMISSIONS_MOER = "realtime_emissions_moer"
|
||||
SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT = "realtime_emissions_percent"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RealtimeEmissionsSensorDescriptionMixin:
|
||||
"""Define an entity description mixin for realtime emissions sensors."""
|
||||
|
||||
data_key: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class RealtimeEmissionsSensorEntityDescription(
|
||||
SensorEntityDescription, RealtimeEmissionsSensorDescriptionMixin
|
||||
):
|
||||
"""Describe a realtime emissions sensor."""
|
||||
|
||||
|
||||
REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS = (
|
||||
RealtimeEmissionsSensorEntityDescription(
|
||||
key=SENSOR_TYPE_REALTIME_EMISSIONS_MOER,
|
||||
name="Marginal Operating Emissions Rate",
|
||||
icon="mdi:blur",
|
||||
native_unit_of_measurement=f"{MASS_POUNDS} CO2/MWh",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
data_key="moer",
|
||||
),
|
||||
RealtimeEmissionsSensorEntityDescription(
|
||||
key=SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT,
|
||||
name="Relative Marginal Emissions Intensity",
|
||||
icon="mdi:blur",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
data_key="percent",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up WattTime sensors based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id]
|
||||
async_add_entities(
|
||||
[
|
||||
RealtimeEmissionsSensor(coordinator, description)
|
||||
for description in REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS
|
||||
if description.data_key in coordinator.data
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Define a realtime emissions sensor."""
|
||||
|
||||
entity_description: RealtimeEmissionsSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
description: RealtimeEmissionsSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert coordinator.config_entry
|
||||
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION,
|
||||
ATTR_BALANCING_AUTHORITY: coordinator.config_entry.data[
|
||||
CONF_BALANCING_AUTHORITY
|
||||
],
|
||||
ATTR_LATITUDE: coordinator.config_entry.data[ATTR_LATITUDE],
|
||||
ATTR_LONGITUDE: coordinator.config_entry.data[ATTR_LONGITUDE],
|
||||
}
|
||||
self._attr_name = f"{description.name} ({coordinator.config_entry.data[CONF_BALANCING_AUTHORITY_ABBREV]})"
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Respond to a DataUpdateCoordinator update."""
|
||||
self.update_from_latest_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
self.update_from_latest_data()
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
self._attr_native_value = self.coordinator.data[
|
||||
self.entity_description.data_key
|
||||
]
|
34
homeassistant/components/watttime/strings.json
Normal file
34
homeassistant/components/watttime/strings.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"coordinates": {
|
||||
"description": "Input the latitude and longitude to monitor:",
|
||||
"data": {
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"description": "Pick a location to monitor:",
|
||||
"data": {
|
||||
"location_type": "[%key:common::config_flow::data::location%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"description": "Input your username and password:",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"unknown_coordinates": "No data for latitude/longitude"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
34
homeassistant/components/watttime/translations/en.json
Normal file
34
homeassistant/components/watttime/translations/en.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_coordinates": "No data for latitude/longitude"
|
||||
},
|
||||
"step": {
|
||||
"coordinates": {
|
||||
"data": {
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
},
|
||||
"description": "Input the latitude and longitude to monitor:"
|
||||
},
|
||||
"location": {
|
||||
"data": {
|
||||
"location_type": "Location"
|
||||
},
|
||||
"description": "Pick a location to monitor:"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
},
|
||||
"description": "Input your username and password:"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -303,6 +303,7 @@ FLOWS = [
|
||||
"vizio",
|
||||
"volumio",
|
||||
"wallbox",
|
||||
"watttime",
|
||||
"waze_travel_time",
|
||||
"wemo",
|
||||
"whirlpool",
|
||||
|
@ -254,6 +254,9 @@ aiotractive==0.5.2
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==26
|
||||
|
||||
# homeassistant.components.watttime
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.yandex_transport
|
||||
aioymaps==1.1.0
|
||||
|
||||
|
@ -178,6 +178,9 @@ aiotractive==0.5.2
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==26
|
||||
|
||||
# homeassistant.components.watttime
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.yandex_transport
|
||||
aioymaps==1.1.0
|
||||
|
||||
|
1
tests/components/watttime/__init__.py
Normal file
1
tests/components/watttime/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the WattTime integration."""
|
263
tests/components/watttime/test_config_flow.py
Normal file
263
tests/components/watttime/test_config_flow.py
Normal file
@ -0,0 +1,263 @@
|
||||
"""Test the WattTime config flow."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.watttime.config_flow import (
|
||||
CONF_LOCATION_TYPE,
|
||||
LOCATION_TYPE_COORDINATES,
|
||||
LOCATION_TYPE_HOME,
|
||||
)
|
||||
from homeassistant.components.watttime.const import (
|
||||
CONF_BALANCING_AUTHORITY,
|
||||
CONF_BALANCING_AUTHORITY_ABBREV,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import (
|
||||
RESULT_TYPE_ABORT,
|
||||
RESULT_TYPE_CREATE_ENTRY,
|
||||
RESULT_TYPE_FORM,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def client_fixture(get_grid_region):
|
||||
"""Define a fixture for an aiowatttime client."""
|
||||
client = AsyncMock(return_value=None)
|
||||
client.emissions.async_get_grid_region = get_grid_region
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture(name="client_login")
|
||||
def client_login_fixture(client):
|
||||
"""Define a fixture for patching the aiowatttime coroutine to get a client."""
|
||||
with patch("homeassistant.components.watttime.config_flow.Client.async_login") as m:
|
||||
m.return_value = client
|
||||
yield m
|
||||
|
||||
|
||||
@pytest.fixture(name="get_grid_region")
|
||||
def get_grid_region_fixture():
|
||||
"""Define a fixture for getting grid region data."""
|
||||
return AsyncMock(return_value={"abbrev": "AUTH_1", "id": 1, "name": "Authority 1"})
|
||||
|
||||
|
||||
async def test_duplicate_error(hass: HomeAssistant, client_login):
|
||||
"""Test that errors are shown when duplicate entries are added."""
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="32.87336, -117.22743",
|
||||
data={
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "password",
|
||||
CONF_LATITUDE: 32.87336,
|
||||
CONF_LONGITUDE: -117.22743,
|
||||
},
|
||||
).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_USERNAME: "user", CONF_PASSWORD: "password"},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION_TYPE: LOCATION_TYPE_HOME},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_show_form_coordinates(hass: HomeAssistant) -> None:
|
||||
"""Test showing the form to input custom latitude/longitude."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_USERNAME: "user", CONF_PASSWORD: "password"},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION_TYPE: LOCATION_TYPE_COORDINATES},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "coordinates"
|
||||
assert result["errors"] is None
|
||||
|
||||
|
||||
async def test_show_form_user(hass: HomeAssistant) -> None:
|
||||
"""Test showing the form to select the authentication type."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"get_grid_region", [AsyncMock(side_effect=CoordinatesNotFoundError)]
|
||||
)
|
||||
async def test_step_coordinates_unknown_coordinates(
|
||||
hass: HomeAssistant, client_login
|
||||
) -> None:
|
||||
"""Test that providing coordinates with no data is handled."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_USERNAME: "user", CONF_PASSWORD: "password"},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION_TYPE: LOCATION_TYPE_COORDINATES},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LATITUDE: "0", CONF_LONGITUDE: "0"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"latitude": "unknown_coordinates"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("get_grid_region", [AsyncMock(side_effect=Exception)])
|
||||
async def test_step_coordinates_unknown_error(
|
||||
hass: HomeAssistant, client_login
|
||||
) -> None:
|
||||
"""Test that providing coordinates with no data is handled."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_USERNAME: "user", CONF_PASSWORD: "password"},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION_TYPE: LOCATION_TYPE_HOME},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_step_login_coordinates(hass: HomeAssistant, client_login) -> None:
|
||||
"""Test a full login flow (inputting custom coordinates)."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
with patch(
|
||||
"homeassistant.components.watttime.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_USERNAME: "user", CONF_PASSWORD: "password"},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION_TYPE: LOCATION_TYPE_COORDINATES},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "51.528308, -0.3817765"
|
||||
assert result["data"] == {
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "password",
|
||||
CONF_LATITUDE: 51.528308,
|
||||
CONF_LONGITUDE: -0.3817765,
|
||||
CONF_BALANCING_AUTHORITY: "Authority 1",
|
||||
CONF_BALANCING_AUTHORITY_ABBREV: "AUTH_1",
|
||||
}
|
||||
|
||||
|
||||
async def test_step_user_home(hass: HomeAssistant, client_login) -> None:
|
||||
"""Test a full login flow (selecting the home location)."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
with patch(
|
||||
"homeassistant.components.watttime.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_USERNAME: "user", CONF_PASSWORD: "password"},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION_TYPE: LOCATION_TYPE_HOME},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "32.87336, -117.22743"
|
||||
assert result["data"] == {
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "password",
|
||||
CONF_LATITUDE: 32.87336,
|
||||
CONF_LONGITUDE: -117.22743,
|
||||
CONF_BALANCING_AUTHORITY: "Authority 1",
|
||||
CONF_BALANCING_AUTHORITY_ABBREV: "AUTH_1",
|
||||
}
|
||||
|
||||
|
||||
async def test_step_user_invalid_credentials(hass: HomeAssistant) -> None:
|
||||
"""Test that invalid credentials are handled."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
with patch(
|
||||
"homeassistant.components.watttime.config_flow.Client.async_login",
|
||||
AsyncMock(side_effect=InvalidCredentialsError),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_USERNAME: "user", CONF_PASSWORD: "password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"username": "invalid_auth"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("get_grid_region", [AsyncMock(side_effect=Exception)])
|
||||
async def test_step_user_unknown_error(hass: HomeAssistant, client_login) -> None:
|
||||
"""Test that an unknown error during the login step is handled."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
with patch(
|
||||
"homeassistant.components.watttime.config_flow.Client.async_login",
|
||||
AsyncMock(side_effect=Exception),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_USERNAME: "user", CONF_PASSWORD: "password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
Loading…
x
Reference in New Issue
Block a user