diff --git a/custom_components/emporia_vue/__init__.py b/custom_components/emporia_vue/__init__.py index 4d74de8..0ba4187 100644 --- a/custom_components/emporia_vue/__init__.py +++ b/custom_components/emporia_vue/__init__.py @@ -31,7 +31,15 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, ENABLE_1D, ENABLE_1M, ENABLE_1MON, VUE_DATA +from .const import ( + CONFIG_TITLE, + CUSTOMER_GID, + DOMAIN, + ENABLE_1D, + ENABLE_1M, + ENABLE_1MON, + VUE_DATA, +) _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -44,7 +52,6 @@ LAST_DAY_DATA: dict[str, Any] = {} LAST_DAY_UPDATE: datetime | None = None - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Emporia Vue component.""" hass.data.setdefault(DOMAIN, {}) @@ -62,6 +69,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ENABLE_1M: conf[ENABLE_1M], ENABLE_1D: conf[ENABLE_1D], ENABLE_1MON: conf[ENABLE_1MON], + CUSTOMER_GID: conf[CUSTOMER_GID], + CONFIG_TITLE: conf[CONFIG_TITLE], }, ) ) @@ -76,6 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DEVICE_INFORMATION = {} entry_data = entry.data + _LOGGER.warning("Setting up Emporia Vue with entry data: %s", entry_data) email: str = entry_data[CONF_EMAIL] password: str = entry_data[CONF_PASSWORD] vue = PyEmVue() @@ -551,10 +561,11 @@ def make_channel_id(channel: VueDeviceChannel, scale: str) -> str: def fix_usage_sign(channel_num: str, usage: float, bidirectional: bool) -> float: + """If the channel is not '1,2,3' or 'Balance' we need it to be positive. + + (see https://github.com/magico13/ha-emporia-vue/issues/57) """ - If the channel is not '1,2,3' or 'Balance' we need it to be positive - (see https://github.com/magico13/ha-emporia-vue/issues/57). - """ + if usage and not bidirectional and channel_num not in ["1,2,3", "Balance"]: # With bidirectionality, we need to also check if bidirectional. If yes, # we either don't abs, or we flip the sign. diff --git a/custom_components/emporia_vue/config_flow.py b/custom_components/emporia_vue/config_flow.py index 21bb33e..85b316b 100644 --- a/custom_components/emporia_vue/config_flow.py +++ b/custom_components/emporia_vue/config_flow.py @@ -7,10 +7,18 @@ from pyemvue import PyEmVue import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, exceptions from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from .const import DOMAIN, DOMAIN_SCHEMA, ENABLE_1D, ENABLE_1M, ENABLE_1MON +from .const import ( + CONFIG_TITLE, + CUSTOMER_GID, + DOMAIN, + ENABLE_1D, + ENABLE_1M, + ENABLE_1MON, + USER_CONFIG_SCHEMA, +) _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -47,11 +55,13 @@ async def validate_input(data: dict): # Return info that you want to store in the config entry. return { - "title": f"Customer {hub.vue.customer.customer_gid}", - "gid": f"{hub.vue.customer.customer_gid}", + CONFIG_TITLE: f"Customer {hub.vue.customer.customer_gid}", + CUSTOMER_GID: f"{hub.vue.customer.customer_gid}", ENABLE_1M: data[ENABLE_1M], ENABLE_1D: data[ENABLE_1D], ENABLE_1MON: data[ENABLE_1MON], + CONF_EMAIL: data[CONF_EMAIL], + CONF_PASSWORD: data[CONF_PASSWORD], } @@ -61,7 +71,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - async def async_step_user(self, user_input=None) -> config_entries.FlowResult: + async def async_step_user(self, user_input=None) -> config_entries.ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -72,7 +82,7 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: self._abort_if_unique_id_configured() return self.async_create_entry( - title=info["title"], data=user_input, options=user_input + title=info["title"], data=user_input ) except CannotConnect: errors["base"] = "cannot_connect" @@ -83,20 +93,66 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: errors["base"] = "unknown" return self.async_show_form( - step_id="user", data_schema=DOMAIN_SCHEMA, errors=errors + step_id="user", data_schema=USER_CONFIG_SCHEMA, errors=errors + ) + + async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None): + """Handle the reconfiguration step.""" + current_config = self._get_reconfigure_entry() + if user_input is not None: + _LOGGER.warning("User input on reconfigure was the following: %s", user_input) + _LOGGER.warning("Current config is: %s", current_config.data) + info = current_config.data + # if gid is not in current config, reauth and get gid again + if CUSTOMER_GID not in current_config.data or not current_config.data[CUSTOMER_GID]: + info = await validate_input(current_config.data) # type: ignore + + await self.async_set_unique_id(info[CUSTOMER_GID]) + self._abort_if_unique_id_mismatch(reason="wrong_account") + data = { + ENABLE_1M: user_input[ENABLE_1M], + ENABLE_1D: user_input[ENABLE_1D], + ENABLE_1MON: user_input[ENABLE_1MON], + CUSTOMER_GID: info[CUSTOMER_GID], + CONFIG_TITLE: info[CONFIG_TITLE], + } + return self.async_update_reload_and_abort( + self._get_reconfigure_entry(), + data_updates=data, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=vol.Schema( + { + vol.Optional( + ENABLE_1M, + default=current_config.data.get(ENABLE_1M, True), + ): bool, + vol.Optional( + ENABLE_1D, + default=current_config.data.get(ENABLE_1D, True), + ): bool, + vol.Optional( + ENABLE_1MON, + default=current_config.data.get(ENABLE_1MON, True), + ): bool, + } + ), ) async def async_step_reauth( self, entry_data: dict[str, Any] - ) -> config_entries.FlowResult: + ) -> config_entries.ConfigFlowResult: """Perform reauthentication upon an API authentication error.""" - return await self.async_step_reauth_confirm() + return await self.async_step_reauth_confirm(entry_data) async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> config_entries.FlowResult: + ) -> config_entries.ConfigFlowResult: """Confirm reauthentication dialog.""" errors: dict[str, str] = {} + existing_entry = self._get_reauth_entry() if user_input: gid = 0 try: @@ -112,7 +168,7 @@ async def async_step_reauth_confirm( await self.async_set_unique_id(str(gid)) self._abort_if_unique_id_mismatch(reason="wrong_account") return self.async_update_reload_and_abort( - self._get_reauth_entry(), + existing_entry, data_updates={ CONF_EMAIL: user_input[CONF_EMAIL], CONF_PASSWORD: user_input[CONF_PASSWORD], @@ -122,54 +178,15 @@ async def async_step_reauth_confirm( step_id="reauth_confirm", data_schema=vol.Schema( { - vol.Required(CONF_EMAIL): str, + vol.Required(CONF_EMAIL, default=existing_entry.data[CONF_EMAIL]): str, vol.Required(CONF_PASSWORD): str, } ), errors=errors, ) - @staticmethod - @core.callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: - """Create the options flow.""" - return OptionsFlowHandler(config_entry) - - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle an options flow for Emporia Vue.""" - - async def async_step_init(self, user_input=None) -> config_entries.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.Optional( - ENABLE_1M, - default=self.config_entry.options.get(ENABLE_1M, True), - ): bool, - vol.Optional( - ENABLE_1D, - default=self.config_entry.options.get(ENABLE_1D, True), - ): bool, - vol.Optional( - ENABLE_1MON, - default=self.config_entry.options.get(ENABLE_1MON, True), - ): bool, - } - ), - ) - - class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/custom_components/emporia_vue/const.py b/custom_components/emporia_vue/const.py index 4bd28e8..9a381e4 100644 --- a/custom_components/emporia_vue/const.py +++ b/custom_components/emporia_vue/const.py @@ -1,8 +1,9 @@ """Constants for the Emporia Vue integration.""" -import homeassistant.helpers.config_validation as cv import voluptuous as vol + from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv DOMAIN = "emporia_vue" VUE_DATA = "vue_data" @@ -10,8 +11,10 @@ ENABLE_1M = "enable_1m" ENABLE_1D = "enable_1d" ENABLE_1MON = "enable_1mon" +CUSTOMER_GID = "customer_gid" +CONFIG_TITLE = "title" -DOMAIN_SCHEMA = vol.Schema( +USER_CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_EMAIL): cv.string, vol.Required(CONF_PASSWORD): cv.string, @@ -23,7 +26,17 @@ CONFIG_SCHEMA = vol.Schema( { - DOMAIN: DOMAIN_SCHEMA, + DOMAIN: vol.Schema( + { + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(ENABLE_1M, default=True): cv.boolean, # type: ignore + vol.Optional(ENABLE_1D, default=True): cv.boolean, # type: ignore + vol.Optional(ENABLE_1MON, default=True): cv.boolean, # type: ignore + vol.Required(CUSTOMER_GID): cv.positive_int, + vol.Required(CONFIG_TITLE): cv.string, + } + ), }, extra=vol.ALLOW_EXTRA, ) diff --git a/custom_components/emporia_vue/sensor.py b/custom_components/emporia_vue/sensor.py index 91e990a..fcd6e46 100644 --- a/custom_components/emporia_vue/sensor.py +++ b/custom_components/emporia_vue/sensor.py @@ -1,8 +1,10 @@ """Platform for sensor integration.""" -import logging from datetime import datetime -from functools import cached_property +import logging + +from pyemvue.device import VueDevice, VueDeviceChannel +from pyemvue.enums import Scale from homeassistant.components.sensor import ( SensorDeviceClass, @@ -15,8 +17,6 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from pyemvue.device import VueDevice, VueDeviceChannel -from pyemvue.enums import Scale from .const import DOMAIN @@ -100,7 +100,6 @@ def __init__(self, coordinator, identifier) -> None: self._attr_suggested_display_precision = 1 self._attr_name = f"Power {self.scale_readable()}" - @cached_property def device_info(self) -> DeviceInfo: """Return the device info.""" device_name = self._channel.name or self._device.device_name @@ -114,14 +113,12 @@ def device_info(self) -> DeviceInfo: manufacturer="Emporia", ) - @cached_property def last_reset(self) -> datetime | None: - """The time when the daily/monthly sensor was reset. Midnight local time.""" + """Reset time of the daily/monthly sensor. Midnight local time.""" if self._id in self.coordinator.data: return self.coordinator.data[self._id]["reset"] return None - @cached_property def native_value(self) -> float | None: """Return the state of the sensor.""" if self._id in self.coordinator.data: @@ -129,9 +126,8 @@ def native_value(self) -> float | None: return self.scale_usage(usage) if usage is not None else None return None - @cached_property def unique_id(self) -> str: - """Unique ID for the sensor.""" + """Return the Unique ID for the sensor.""" if self._scale == Scale.MINUTE.value: return ( "sensor.emporia_vue.instant." diff --git a/custom_components/emporia_vue/strings.json b/custom_components/emporia_vue/strings.json index 044b498..19b3f56 100644 --- a/custom_components/emporia_vue/strings.json +++ b/custom_components/emporia_vue/strings.json @@ -9,6 +9,15 @@ "enable_1d": "Energy Today Sensor", "enable_1mon": "Energy This Month Sensor" } + }, + "reconfigure": { + "enable_1m": "Power Minute Average Sensor", + "enable_1d": "Energy Today Sensor", + "enable_1mon": "Energy This Month Sensor" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Emporia Vue integration needs to re-authenticate your account" } }, "error": { @@ -17,20 +26,9 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - } - }, - "options": { - "step": { - "init": { - "data": { - "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", - "enable_1m": "Power Minute Average Sensor", - "enable_1d": "Energy Today Sensor", - "enable_1mon": "Energy This Month Sensor" - } - } + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } } -} \ No newline at end of file +} diff --git a/custom_components/emporia_vue/translations/en.json b/custom_components/emporia_vue/translations/en.json index 875558d..0f5f3b8 100644 --- a/custom_components/emporia_vue/translations/en.json +++ b/custom_components/emporia_vue/translations/en.json @@ -1,36 +1,34 @@ { - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", + "config": { + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password", + "enable_1m": "Power Minute Average Sensor", + "enable_1d": "Energy Today Sensor", + "enable_1mon": "Energy This Month Sensor" + } + }, + "reconfigure": { + "enable_1m": "Power Minute Average Sensor", + "enable_1d": "Energy Today Sensor", + "enable_1mon": "Energy This Month Sensor" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Emporia Vue integration needs to re-authenticate your account" + } + }, + "error": { + "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "email": "Email", - "enable_1d": "Energy Today Sensor", - "enable_1m": "Power Minute Average Sensor", - "enable_1mon": "Energy This Month Sensor", - "password": "Password" - } - } - } }, - "options": { - "step": { - "init": { - "data": { - "email": "Email", - "enable_1d": "Energy Today Sensor", - "enable_1m": "Power Minute Average Sensor", - "enable_1mon": "Energy This Month Sensor", - "password": "Password" - } - } - } + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } -} \ No newline at end of file + } +}