From 5ba349304150fee3fbffd52a0993743df318bf4f Mon Sep 17 00:00:00 2001 From: magico13 Date: Sat, 1 Feb 2025 14:10:29 -0500 Subject: [PATCH] Allow selecting different behavior for merged circuits, as needed by your setup. --- custom_components/emporia_vue/__init__.py | 28 ++++-- custom_components/emporia_vue/config_flow.py | 86 +++++++++++++------ custom_components/emporia_vue/const.py | 34 +------- custom_components/emporia_vue/strings.json | 6 +- .../emporia_vue/translations/en.json | 12 +-- 5 files changed, 96 insertions(+), 70 deletions(-) diff --git a/custom_components/emporia_vue/__init__.py b/custom_components/emporia_vue/__init__.py index 9762a32..d57c1e4 100644 --- a/custom_components/emporia_vue/__init__.py +++ b/custom_components/emporia_vue/__init__.py @@ -38,6 +38,9 @@ ENABLE_1D, ENABLE_1M, ENABLE_1MON, + MERGED_ABS, + MERGED_BEHAVIOR, + MERGED_INVERT, VUE_DATA, ) @@ -51,6 +54,7 @@ LAST_MINUTE_DATA: dict[str, Any] = {} LAST_DAY_DATA: dict[str, Any] = {} LAST_DAY_UPDATE: datetime | None = None +CONF_MERGED_BEHAVIOR: str = MERGED_INVERT async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Emporia Vue component.""" @@ -59,6 +63,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not conf: return True + global CONF_MERGED_BEHAVIOR + if MERGED_BEHAVIOR in conf: + CONF_MERGED_BEHAVIOR = conf[MERGED_BEHAVIOR] + hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, @@ -69,6 +77,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ENABLE_1M: conf[ENABLE_1M], ENABLE_1D: conf[ENABLE_1D], ENABLE_1MON: conf[ENABLE_1MON], + MERGED_BEHAVIOR: conf[MERGED_BEHAVIOR], CUSTOMER_GID: conf[CUSTOMER_GID], CONFIG_TITLE: conf[CONFIG_TITLE], }, @@ -481,11 +490,9 @@ async def parse_flattened_usage_data( fixed_usage, ) - bidirectional = ( - "bidirectional" in info_channel.type.lower() - or "merged" in info_channel.type.lower() - ) - fixed_usage = fix_usage_sign(channel_num, fixed_usage, bidirectional) + bidirectional = "bidirectional" in info_channel.type.lower() + merged = "merged" in info_channel.type.lower() + fixed_usage = fix_usage_sign(channel_num, fixed_usage, bidirectional, merged, CONF_MERGED_BEHAVIOR) data[identifier] = { "device_gid": gid, @@ -565,11 +572,20 @@ def make_channel_id(channel: VueDeviceChannel, scale: str) -> str: return f"{channel.device_gid}-{channel.channel_num}-{scale}" -def fix_usage_sign(channel_num: str, usage: float, bidirectional: bool) -> float: +def fix_usage_sign(channel_num: str, usage: float, bidirectional: bool, merged:bool, merged_behavior: str) -> float: """If the channel is not '1,2,3' or 'Balance' we need it to be positive. + Merged circuits are a special case, as they can be inverted, abs'd, or left alone to support different solar setups. + (see https://github.com/magico13/ha-emporia-vue/issues/57) """ + if merged: + # for merged channels, we need to either invert or abs the value as requested + if merged_behavior == MERGED_ABS: + return abs(usage) + if merged_behavior == MERGED_INVERT: + return -usage + return usage 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, diff --git a/custom_components/emporia_vue/config_flow.py b/custom_components/emporia_vue/config_flow.py index e93fce7..afb3b37 100644 --- a/custom_components/emporia_vue/config_flow.py +++ b/custom_components/emporia_vue/config_flow.py @@ -10,6 +10,8 @@ from homeassistant import config_entries, exceptions from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import selector from .const import ( CONFIG_TITLE, @@ -18,7 +20,10 @@ ENABLE_1D, ENABLE_1M, ENABLE_1MON, - USER_CONFIG_SCHEMA, + MERGED_ABS, + MERGED_BEHAVIOR, + MERGED_INVERT, + MERGED_NONE, ) _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -42,7 +47,7 @@ async def authenticate(self, username, password) -> bool: return await loop.run_in_executor(None, self.vue.login, username, password) -async def validate_input(data: dict): +async def validate_input(data: dict | Mapping[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -59,15 +64,21 @@ async def validate_input(data: dict): if not hub.vue.customer: raise InvalidAuth + new_data = dict(data) + + if MERGED_BEHAVIOR not in new_data: + new_data[MERGED_BEHAVIOR] = MERGED_INVERT + # Return info that you want to store in the config entry. return { CONFIG_TITLE: f"{hub.vue.customer.email} ({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], + ENABLE_1M: new_data[ENABLE_1M], + ENABLE_1D: new_data[ENABLE_1D], + ENABLE_1MON: new_data[ENABLE_1MON], + MERGED_BEHAVIOR: new_data[MERGED_BEHAVIOR], + CONF_EMAIL: new_data[CONF_EMAIL], + CONF_PASSWORD: new_data[CONF_PASSWORD], } @@ -98,8 +109,22 @@ async def async_step_user(self, user_input=None) -> config_entries.ConfigFlowRes _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" + config_schema = { + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(ENABLE_1M, default=True): cv.boolean, + vol.Optional(ENABLE_1D, default=True): cv.boolean, + vol.Optional(ENABLE_1MON, default=True): cv.boolean, + } + + config_schema[vol.Required(MERGED_BEHAVIOR)] = selector({ + "select": { + "options": [MERGED_INVERT, MERGED_ABS, MERGED_NONE], + } + }) + return self.async_show_form( - step_id="user", data_schema=USER_CONFIG_SCHEMA, errors=errors + step_id="user", data_schema=vol.Schema(config_schema), errors=errors ) async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) -> config_entries.ConfigFlowResult: @@ -111,7 +136,7 @@ async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) 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 + info = await validate_input(current_config.data) await self.async_set_unique_id(info[CUSTOMER_GID]) self._abort_if_unique_id_mismatch(reason="wrong_account") @@ -119,6 +144,7 @@ async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) ENABLE_1M: user_input[ENABLE_1M], ENABLE_1D: user_input[ENABLE_1D], ENABLE_1MON: user_input[ENABLE_1MON], + MERGED_BEHAVIOR: user_input[MERGED_BEHAVIOR], CUSTOMER_GID: info[CUSTOMER_GID], CONFIG_TITLE: info[CONFIG_TITLE], } @@ -127,24 +153,30 @@ async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) data_updates=data, ) + data_schema : dict[vol.Optional | vol.Required, Any] = { + vol.Optional( + ENABLE_1M, + default=current_config.data.get(ENABLE_1M, True), + ): cv.boolean, + vol.Optional( + ENABLE_1D, + default=current_config.data.get(ENABLE_1D, True), + ): cv.boolean, + vol.Optional( + ENABLE_1MON, + default=current_config.data.get(ENABLE_1MON, True), + ): cv.boolean, + } + + data_schema[vol.Required(MERGED_BEHAVIOR)] = selector({ + "select": { + "options": [MERGED_INVERT, MERGED_ABS, MERGED_NONE], + } + }) + 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, - } - ), + data_schema=vol.Schema(data_schema), ) async def async_step_reauth( @@ -184,8 +216,8 @@ async def async_step_reauth_confirm( step_id="reauth_confirm", data_schema=vol.Schema( { - vol.Required(CONF_EMAIL, default=existing_entry.data[CONF_EMAIL]): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_EMAIL, default=existing_entry.data[CONF_EMAIL]): cv.string, + vol.Required(CONF_PASSWORD): cv.string, } ), errors=errors, diff --git a/custom_components/emporia_vue/const.py b/custom_components/emporia_vue/const.py index 9a381e4..249314f 100644 --- a/custom_components/emporia_vue/const.py +++ b/custom_components/emporia_vue/const.py @@ -1,42 +1,16 @@ """Constants for the Emporia Vue integration.""" -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" ENABLE_1S = "enable_1s" ENABLE_1M = "enable_1m" ENABLE_1D = "enable_1d" ENABLE_1MON = "enable_1mon" +MERGED_BEHAVIOR = "merged_behavior" CUSTOMER_GID = "customer_gid" CONFIG_TITLE = "title" -USER_CONFIG_SCHEMA = 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 - } -) +MERGED_INVERT = "Invert" +MERGED_ABS = "Absolute Value" +MERGED_NONE = "None" -CONFIG_SCHEMA = vol.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/strings.json b/custom_components/emporia_vue/strings.json index febd5a2..30fe5fe 100644 --- a/custom_components/emporia_vue/strings.json +++ b/custom_components/emporia_vue/strings.json @@ -7,14 +7,16 @@ "password": "[%key:common::config_flow::data::password%]", "enable_1m": "Power Minute Average Sensor", "enable_1d": "Energy Today Sensor", - "enable_1mon": "Energy This Month Sensor" + "enable_1mon": "Energy This Month Sensor", + "merged_behavior": "Merged Circuit Operation" } }, "reconfigure": { "data": { "enable_1m": "[%key:component::emporia_vue::config::step::user::data::enable_1m%]", "enable_1d": "[%key:component::emporia_vue::config::step::user::data::enable_1d%]", - "enable_1mon": "[%key:component::emporia_vue::config::step::user::data::enable_1mon%]" + "enable_1mon": "[%key:component::emporia_vue::config::step::user::data::enable_1mon%]", + "merged_behavior": "[%key:component::emporia_vue::config::step::user::data::merged_behavior%]" } }, "reauth_confirm": { diff --git a/custom_components/emporia_vue/translations/en.json b/custom_components/emporia_vue/translations/en.json index 7fa4c9e..6936fc3 100644 --- a/custom_components/emporia_vue/translations/en.json +++ b/custom_components/emporia_vue/translations/en.json @@ -12,18 +12,19 @@ }, "step": { "reauth_confirm": { - "description": "The Emporia Vue integration needs to re-authenticate your account", - "title": "Authentication expired for {name}", - "data":{ + "data": { "email": "Email", "password": "Password" - } + }, + "description": "The Emporia Vue integration needs to re-authenticate your account", + "title": "Authentication expired for {name}" }, "reconfigure": { "data": { "enable_1d": "Energy Today Sensor", "enable_1m": "Power Minute Average Sensor", - "enable_1mon": "Energy This Month Sensor" + "enable_1mon": "Energy This Month Sensor", + "merged_behavior": "Merged Circuit Operation" } }, "user": { @@ -32,6 +33,7 @@ "enable_1d": "Energy Today Sensor", "enable_1m": "Power Minute Average Sensor", "enable_1mon": "Energy This Month Sensor", + "merged_behavior": "Merged Circuit Operation", "password": "Password" } }