From fbdd4459990acce97225a3672044b66e01eaf4c7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 25 Oct 2021 15:52:14 -0600 Subject: [PATCH] Add WattTime config option for showing the monitored location on the map (#57129) * Add WattTime configuration option for showing the monitored location on the map * Update tests * Explicitly pass entry * Tests --- homeassistant/components/watttime/__init__.py | 7 +++ .../components/watttime/config_flow.py | 35 ++++++++++++++ homeassistant/components/watttime/const.py | 1 + homeassistant/components/watttime/sensor.py | 46 +++++++++++++------ .../components/watttime/strings.json | 10 ++++ .../components/watttime/translations/en.json | 10 ++++ tests/components/watttime/test_config_flow.py | 42 +++++++++++++---- 7 files changed, 127 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index a4e1acb4b7e..779fc0791b6 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -69,6 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + return True @@ -79,3 +81,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Handle an options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index a00ba4c8c86..415697670b0 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -8,6 +8,7 @@ from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, OptionsFlow from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -22,6 +23,7 @@ from homeassistant.helpers.typing import ConfigType from .const import ( CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, + CONF_SHOW_ON_MAP, DOMAIN, LOGGER, ) @@ -118,6 +120,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data[CONF_PASSWORD] = password return await self.async_step_location() + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Define the config flow to handle options.""" + return WattTimeOptionsFlowHandler(config_entry) + async def async_step_coordinates( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -222,3 +230,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "user", STEP_USER_DATA_SCHEMA, ) + + +class WattTimeOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a WattTime options flow.""" + + def __init__(self, entry: ConfigEntry) -> None: + """Initialize.""" + self.entry = entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_SHOW_ON_MAP, + default=self.entry.options.get(CONF_SHOW_ON_MAP, True), + ): bool + } + ), + ) diff --git a/homeassistant/components/watttime/const.py b/homeassistant/components/watttime/const.py index 680505c8d43..07ea0e47167 100644 --- a/homeassistant/components/watttime/const.py +++ b/homeassistant/components/watttime/const.py @@ -7,5 +7,6 @@ LOGGER = logging.getLogger(__package__) CONF_BALANCING_AUTHORITY = "balancing_authority" CONF_BALANCING_AUTHORITY_ABBREV = "balancing_authority_abbreviation" +CONF_SHOW_ON_MAP = "show_on_map" DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 50389de35c6..b1ebc262134 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -1,7 +1,8 @@ """Support for WattTime sensors.""" from __future__ import annotations -from typing import TYPE_CHECKING, cast +from collections.abc import Mapping +from typing import Any, cast from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, @@ -27,6 +28,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, + CONF_SHOW_ON_MAP, DATA_COORDINATOR, DOMAIN, ) @@ -64,7 +66,7 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] async_add_entities( [ - RealtimeEmissionsSensor(coordinator, description) + RealtimeEmissionsSensor(coordinator, entry, description) for description in REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS if description.key in coordinator.data ] @@ -77,26 +79,40 @@ class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): def __init__( self, coordinator: DataUpdateCoordinator, + entry: ConfigEntry, description: SensorEntityDescription, ) -> 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._attr_name = ( + f"{description.name} ({entry.data[CONF_BALANCING_AUTHORITY_ABBREV]})" + ) + self._attr_unique_id = f"{entry.entry_id}_{description.key}" + self._entry = entry self.entity_description = description + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + attrs = { + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, + ATTR_BALANCING_AUTHORITY: self._entry.data[CONF_BALANCING_AUTHORITY], + } + + # Displaying the geography on the map relies upon putting the latitude/longitude + # in the entity attributes with "latitude" and "longitude" as the keys. + # Conversely, we can hide the location on the map by using other keys, like + # "lati" and "long". + if self._entry.options.get(CONF_SHOW_ON_MAP) is not False: + attrs[ATTR_LATITUDE] = self._entry.data[ATTR_LATITUDE] + attrs[ATTR_LONGITUDE] = self._entry.data[ATTR_LONGITUDE] + else: + attrs["lati"] = self._entry.data[ATTR_LATITUDE] + attrs["long"] = self._entry.data[ATTR_LONGITUDE] + + return attrs + @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" diff --git a/homeassistant/components/watttime/strings.json b/homeassistant/components/watttime/strings.json index 594848afce1..1650856a669 100644 --- a/homeassistant/components/watttime/strings.json +++ b/homeassistant/components/watttime/strings.json @@ -38,5 +38,15 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "options": { + "step": { + "init": { + "title": "Configure WattTime", + "data": { + "show_on_map": "Show monitored location on the map" + } + } + } } } diff --git a/homeassistant/components/watttime/translations/en.json b/homeassistant/components/watttime/translations/en.json index 49b264851f2..a261615ea81 100644 --- a/homeassistant/components/watttime/translations/en.json +++ b/homeassistant/components/watttime/translations/en.json @@ -38,5 +38,15 @@ "description": "Input your username and password:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Show monitored location on the map" + }, + "title": "Configure WattTime" + } + } } } \ No newline at end of file diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index 672f294c099..de6e16a400a 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.watttime.config_flow import ( CONF_LOCATION_TYPE, LOCATION_TYPE_COORDINATES, @@ -13,6 +13,7 @@ from homeassistant.components.watttime.config_flow import ( from homeassistant.components.watttime.const import ( CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, + CONF_SHOW_ON_MAP, DOMAIN, ) from homeassistant.const import ( @@ -77,12 +78,42 @@ async def test_duplicate_error(hass: HomeAssistant, client_login): 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_options_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="32.87336, -117.22743", + data={ + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_LATITUDE: 32.87336, + CONF_LONGITUDE: -117.22743, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.watttime.async_setup_entry", return_value=True + ): + await hass.config_entries.async_setup(entry.entry_id) + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == {CONF_SHOW_ON_MAP: False} + + async def test_show_form_coordinates(hass: HomeAssistant, client_login) -> None: """Test showing the form to input custom latitude/longitude.""" result = await hass.config_entries.flow.async_init( @@ -97,7 +128,6 @@ async def test_show_form_coordinates(hass: HomeAssistant, client_login) -> None: 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" @@ -109,7 +139,6 @@ async def test_show_form_user(hass: HomeAssistant) -> None: 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" @@ -137,7 +166,6 @@ async def test_step_coordinates_unknown_coordinates( 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"} @@ -158,7 +186,6 @@ async def test_step_coordinates_unknown_error( 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"} @@ -241,7 +268,6 @@ async def test_step_reauth_invalid_credentials(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_PASSWORD: "password"}, ) - await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_auth"} @@ -323,7 +349,6 @@ async def test_step_user_invalid_credentials(hass: HomeAssistant) -> None: 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": "invalid_auth"} @@ -342,7 +367,6 @@ async def test_step_user_unknown_error(hass: HomeAssistant, client_login) -> Non 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"}