From 679a874bbf2e5b227dbd04dcacd41c6a13ff0432 Mon Sep 17 00:00:00 2001 From: Chris Abberley <26394346+cabberley@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:51:17 +1000 Subject: [PATCH 01/11] Changes to be committed: modified: README.md modified: custom_components/utility_meter_next_gen/__init__.py modified: custom_components/utility_meter_next_gen/config_flow.py modified: custom_components/utility_meter_next_gen/const.py modified: custom_components/utility_meter_next_gen/manifest.json modified: custom_components/utility_meter_next_gen/schemas.py modified: custom_components/utility_meter_next_gen/sensor.py modified: custom_components/utility_meter_next_gen/strings.json modified: custom_components/utility_meter_next_gen/translations/en.json --- README.md | 41 +- .../utility_meter_next_gen/__init__.py | 29 +- .../utility_meter_next_gen/config_flow.py | 154 +++++- .../utility_meter_next_gen/const.py | 73 ++- .../utility_meter_next_gen/manifest.json | 2 +- .../utility_meter_next_gen/schemas.py | 322 ++++++++++++- .../utility_meter_next_gen/sensor.py | 454 ++++++++++++------ .../utility_meter_next_gen/strings.json | 73 ++- .../translations/en.json | 75 ++- 9 files changed, 1037 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index 15f1ff2..df81910 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,15 @@ This custom HACS integration for Home Assistant provides an enhanced set of capa Based on the current "Utility Meter" component code in the Home-Assistant/Core. Acknowledgements to [DGomes](https://github.com/dgomes) who is the Code Owner of the core utlity Meter who really did all the hard logic work for the Meter Utility. -The main enhancements that the Utility Meter Next Gen has added to the original Utility Meter include: +There are lots of enhancements that the Utility Meter Next Gen has added to the original Utility Meter include: +- **Create multiple period sensors, optionally with Tariffs and individual Calculation sensors from a single Source and Calculation Sensor** - Creating Cron schedule patterns via the Frontend of HA, you no longer need to create them in configuration.yaml! - Additional Predefined schedules that should accomodate the majority of people's needs. - The addition of an optional secondary sensor/entity that will be used to calculate a value based on the Meters value. - Option to create an additional Sensor for the Calculated Value, if you have added a sensor to create a calculated value. - All settings of the Meter can be modified through the Frontend. The options reflect the schedule type you created the Meter with (Predefined or CRON). -- An option to create a "Total" Tariff that will not pause like a normal Tariff does. (You can create a sungle Sensor set that collects for each tariff period plus the total) +- An option to create a "Total" Tariff that will not pause like a normal Tariff does. (You can create a single Sensor set that collects for each tariff period plus the total) - Additional extra attributes have been added to the Sensor, so you can see quickly the key information about the sensor and what it is doing. - Additional attributes that don't change are not recorded in thhe Recorder DB to avoid unnecessary recorder data bloat. - Calibration values can now be set and modified through the Utility Meter configuration. @@ -43,13 +44,16 @@ These enhancements should provide a very versatile solution to simplify creating ## Installation -Add this [repository](https://github.com/cabberley/utility_meter_evolved) via your custom Repositories option in the HACS dashboard as an "Integration Type" and then find "Utility Meter Next Gen" in the repository list, download and restart your Home Assistant. +**The Utility Meter Next Gen is now available directly from HACS, no need to add the repository anymore!!** -Or preferably easier use the link below to add it to your system: +Go to the HACS Dashboard in your Home Assistant, and search for "Utility Meter Next Gen", download and restart your HA. -[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=cabberley&repository=utility_meter_evolved&category=integration) +OR + +1. Add this [repository](https://github.com/cabberley/utility_meter_evolved) via your custom Repositories option in the HACS dashboard as an "Integration Type" and then find "Utility Meter Next Gen" in the repository list, download and restart your Home Assistant. +2. Use the link below to add it to your system: -> **NOTE: Currently waiting for this new integration to be added to the HACS repository of Custom Integrations.** +[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=cabberley&repository=utility_meter_evolved&category=integration) ## Using the Utility Meter Next Gen @@ -57,11 +61,31 @@ After installing the integration, you should now find **Utility Meter Next Gen** Follow the instructions to setup your new Meter with an optional Caculation Sensor. +## Using the Multi Predefined reset cycle Meters + +This great new feature will take the pain away from setting multiple individual Meters to track your usuage and $$. If you like to track your meters for various periods like every 5 minutes, 30 minutes, hourly, daily, monthly or yearly, this feature enables you to create a single config that will deliver: + +- Individual sensors for each time period. +- Individual Tariff based sensors for each tariff for each time period. +- Create a separate Calculation Sensor for every Meter created. +- A single Select for the tariff selection that will pause/collect all the sensors in alignment with your Select choice. +- After creation, you can go back and change (add/remove) Predefiend Reset Cycles and if you created a Tariff based set change the list of Tariffs. You can also add or remove the Calculation Sensor if you created a Utility Meter with a Calculation source. + +In addition to all of that, this special Multiple sensor config, always you to calibrate one of those periods. For example, those that are tracking their daily energy costs, may have a daily "service/connection" charge. With this Config, you can tell the setup to only apply the calibration calculation value to just the "Daily" meter. + +When you create a new Utlity Meter using the new "Multi Predefined reset cycle Meters", it is very similar to creating a normal predefined recycle Meter with a few additional options. + +1. After choosing your Source Sensors, you are then presented with the next step, select the various time periods you would like meters for. You can choose 1 or choose them all! +2. After submitting the Predefined list you are then able to optional add some other more granular settings. +3. The settings will be familiar to the normal Predefined Recycle Meter with the following changes: + 1. For both the calibration sensors there is a new option to select which of the predefined cycles to apply it to. + 2. For simplicity, the ability to set an Offset to the recycle times has been removed. + ## Some Additional helpers ### Warning -**Do not change the "Entity ID" after creating your meter, it will break the Sennors that this integration creates** +**Do not change the "Entity ID" after creating your meter, it will break the Sensors that this integration creates** ### Selection of Sensors to Monitor @@ -101,6 +125,3 @@ A good example of using the calibration sensor is if you are tracking the Cost o - if your Consumption is being recorded as MWh and your Calculation Sensor is $/kWh then you want to convert your MWh to kWH to achieve this the multiplier should be set to 1000. - if your Consumption is being recorded as Wh and your Calculation Sensor is $/kWh then you want to convert your MWh to kWH to achieve this the multiplier should be set to 0.0001. -### If you want to track one of the attributes like "Last period calculated value" as a Sensor? - -If you want to track via another sensor the calculated attribute variable, I would suggest another Great HACS Custom Integration written by gjohansson-ST [Attribute as Sensor](https://github.com/gjohansson-ST/attribute_as_sensor) this integration can monitor an attribute in the Utility Meter Next Gen and surface it as a Sensor in its own right. diff --git a/custom_components/utility_meter_next_gen/__init__.py b/custom_components/utility_meter_next_gen/__init__.py index 4c864c5..3f035bc 100644 --- a/custom_components/utility_meter_next_gen/__init__.py +++ b/custom_components/utility_meter_next_gen/__init__.py @@ -27,6 +27,8 @@ from homeassistant.helpers.typing import ConfigType from .const import ( + CONF_CONFIG_CALIBRATE_APPLY, + CONF_CONFIG_CALIBRATE_CALC_APPLY, CONF_CONFIG_CALIBRATE_CALC_VALUE, CONF_CONFIG_CALIBRATE_VALUE, CONF_CREATE_CALCULATION_SENSOR, @@ -58,6 +60,7 @@ def validate_cron_pattern(pattern): """Check that the pattern is well-formed.""" try: + _LOGGER.debug("Testing cron pattern: %s", pattern) CronSim(pattern, datetime(2020, 1, 1)) # any date will do except CronSimError as err: _LOGGER.error("Invalid cron pattern %s: %s", pattern, err) @@ -85,7 +88,8 @@ def period_or_cron(config): vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_METER_TYPE): vol.In(CONF_METER_TYPES), - vol.Optional(CONF_METER_OFFSET, default=CONF_METER_OFFSET_DURATION_DEFAULT): cv.ensure_list, + vol.Optional(CONF_METER_OFFSET, + default=CONF_METER_OFFSET_DURATION_DEFAULT): cv.ensure_list, vol.Optional(CONF_METER_DELTA_VALUES, default=False): cv.boolean, vol.Optional(CONF_METER_NET_CONSUMPTION, default=False): cv.boolean, vol.Optional(CONF_METER_PERIODICALLY_RESETTING, default=True): cv.boolean, @@ -291,23 +295,38 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> _LOGGER.info("Migration to version %s successful", config_entry.version) - if config_entry.version == 3 or config_entry.version is None: + if config_entry.version == 3: new = {**config_entry.options, CONF_CONFIG_CALIBRATE_CALC_VALUE: 0} new = {**config_entry.options, CONF_CONFIG_CALIBRATE_VALUE: 0} hass.config_entries.async_update_entry(config_entry, options=new, version=4) _LOGGER.info("Migration to version %s successful", config_entry.version) - if config_entry.version == 4 or config_entry.version is None: + if config_entry.version == 4: new = {**config_entry.options, CONF_CONFIG_CALIBRATE_CALC_VALUE: 0} hass.config_entries.async_update_entry(config_entry, options=new, version=5) _LOGGER.info("Migration to version %s successful", config_entry.version) - if config_entry.version == 5 or config_entry.version is None: - new = {**config_entry.options, CONF_CREATE_CALCULATION_SENSOR: CONF_CREATE_CALCULATION_SENSOR_DEFAULT} + if config_entry.version == 5: + new = {**config_entry.options, + CONF_CREATE_CALCULATION_SENSOR: CONF_CREATE_CALCULATION_SENSOR_DEFAULT} hass.config_entries.async_update_entry(config_entry, options=new, version=6) _LOGGER.info("Migration to version %s successful", config_entry.version) + if config_entry.version == 6: + new = {**config_entry.options, + CONF_CONFIG_CALIBRATE_CALC_APPLY: None} + hass.config_entries.async_update_entry(config_entry, options=new, version=7) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + if config_entry.version == 7: + new = {**config_entry.options, + CONF_CONFIG_CALIBRATE_APPLY: None} + hass.config_entries.async_update_entry(config_entry, options=new, version=8) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + return True diff --git a/custom_components/utility_meter_next_gen/config_flow.py b/custom_components/utility_meter_next_gen/config_flow.py index 9c667de..927b023 100644 --- a/custom_components/utility_meter_next_gen/config_flow.py +++ b/custom_components/utility_meter_next_gen/config_flow.py @@ -4,18 +4,22 @@ from datetime import datetime from decimal import Decimal, DecimalException +import logging from typing import Any, Optional from cronsim import CronSim, CronSimError -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import callback from .const import ( + CONF_CONFIG_CALIBRATE_APPLY, + CONF_CONFIG_CALIBRATE_CALC_APPLY, CONF_CONFIG_CALIBRATE_CALC_VALUE, CONF_CONFIG_CALIBRATE_VALUE, CONF_CONFIG_CRON, + CONF_CONFIG_MULTI, CONF_CONFIG_PREDEFINED, CONF_CONFIG_TYPE, CONF_CREATE_CALCULATION_SENSOR, @@ -34,20 +38,24 @@ BASE_CONFIG_SCHEMA, create_cron_config_schema, create_cron_option_schema, + create_multi_config_schema_step_1, + create_multi_config_schema_step_2, + create_multi_option_schema_step_2, create_predefined_config_schema, create_predefined_option_schema, ) +_LOGGER = logging.getLogger(__name__) async def validate(): """Validate the configuration.""" # This function can be extended to include any validation logic needed. return True -class UtilityMeterEvolvedCustomConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class UtilityMeterEvolvedCustomConfigFlow(ConfigFlow, domain=DOMAIN): """Github Custom config flow.""" - VERSION = 6 + VERSION = 8 data: Optional[dict[str, Any]] # noqa: UP045 @@ -105,6 +113,8 @@ async def async_step_user(self, user_input: Optional[dict[str, Any]] = None): # return await self.async_step_cron() elif user_input.get(CONF_CONFIG_TYPE) == CONF_CONFIG_PREDEFINED: # noqa: RET505 return await self.async_step_predefined() + elif user_input.get(CONF_CONFIG_TYPE) == CONF_CONFIG_MULTI: # noqa: RET505 + return await self.async_step_multi_step_1() #return await self.async_step_repo() return self.async_show_form( @@ -124,6 +134,8 @@ async def async_step_cron(self, user_input: Optional[dict[str, Any]] = None): # if not errors: # Input is valid, set data. + self.data[CONF_CONFIG_CALIBRATE_APPLY] = None + self.data[CONF_CONFIG_CALIBRATE_CALC_APPLY] = None self.data[CONF_CONFIG_CALIBRATE_CALC_VALUE] = 0 self.data[CONF_CONFIG_CALIBRATE_VALUE] = 0 self.data[CONF_CREATE_CALCULATION_SENSOR] = CONF_CREATE_CALCULATION_SENSOR_DEFAULT @@ -157,6 +169,8 @@ async def async_step_predefined(self, user_input: if not errors: # Input is valid, set data. + self.data[CONF_CONFIG_CALIBRATE_APPLY] = None + self.data[CONF_CONFIG_CALIBRATE_CALC_APPLY] = None self.data[CONF_CONFIG_CALIBRATE_CALC_VALUE] = 0 self.data[CONF_CONFIG_CALIBRATE_VALUE] = 0 self.data[CONF_CONFIG_CRON] = None # noqa: PGH003 # type: ignore @@ -172,25 +186,93 @@ async def async_step_predefined(self, user_input: data_schema=create_predefined_config_schema(self.data), errors=errors ) + + async def async_step_multi_step_1(self, user_input: + Optional[dict[str, Any]] = None): # noqa: UP045 + """Second step in config flow to add a repo to watch.""" + errors: dict[str, str] = {} + self.data = self.data or {} # Initialize data if not set + if user_input is not None: + # Validate the path. + try: + await validate() + + except ValueError: + errors["base"] = "invalid_path" + + if not errors: + # Input is valid, set data. + self.data[CONF_METER_OFFSET] = CONF_METER_OFFSET_DURATION_DEFAULT + self.data[CONF_CONFIG_CALIBRATE_CALC_VALUE] = 0 + self.data[CONF_CONFIG_CALIBRATE_VALUE] = 0 + self.data[CONF_CONFIG_CRON] = None # noqa: PGH003 # type: ignore + self.data[CONF_CREATE_CALCULATION_SENSOR] = CONF_CREATE_CALCULATION_SENSOR_DEFAULT + self.data[CONF_SOURCE_CALC_MULTIPLIER] = 1 + self.data[CONF_TARIFFS] = [] # noqa: PGH003 # type: ignore + self.data.update(user_input) # noqa: PGH003 # type: ignore + return await self.async_step_multi_step_2() + + return self.async_show_form( + step_id="multi_step_1", + data_schema=create_multi_config_schema_step_1(self.data), + errors=errors + ) + async def async_step_multi_step_2(self, user_input: + Optional[dict[str, Any]] = None): # noqa: UP045 + """Second step in config flow to add a repo to watch.""" + errors: dict[str, str] = {} + self.data = self.data or {} # Initialize data if not set + if user_input is not None: + # Validate the path. + try: + await validate() + + except ValueError: + errors["base"] = "invalid_path" + + if not errors: + # Input is valid, set data. + if user_input[CONF_CONFIG_CALIBRATE_APPLY] == "none": + user_input[CONF_CONFIG_CALIBRATE_APPLY] = None + if user_input[CONF_CONFIG_CALIBRATE_CALC_APPLY] == "none": + user_input[CONF_CONFIG_CALIBRATE_CALC_APPLY] = None + self.data.update(user_input) # noqa: PGH003 # type: ignore + return self.async_create_entry(title=self.data["name"], # noqa: PGH003 # type: ignore + data={}, options=self.data) + + return self.async_show_form( + step_id="multi_step_2", + data_schema=create_multi_config_schema_step_2(self.data), + errors=errors + ) @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handles options flow for the component.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the options flow handler.""" - #self.config_entry = config_entry self.options_schema =None - + self.config_type = None + #self.config_entry = config_entry + self.data = config_entry.options.copy() # type: ignore + _LOGGER.debug("async Option init self.data: %s", self.data) if config_entry.options["config_type"] == CONF_CONFIG_CRON: + self.config_type = CONF_CONFIG_CRON + # Create the options schema for cron config. self.options_schema = create_cron_option_schema(config_entry.options) - else: + elif config_entry.options["config_type"] == CONF_CONFIG_PREDEFINED: + self.config_type = CONF_CONFIG_PREDEFINED + # Create the options schema for predefined config. self.options_schema = create_predefined_option_schema(config_entry.options) + elif config_entry.options["config_type"] == CONF_CONFIG_MULTI: + self.config_type = CONF_CONFIG_MULTI + # Create the options schema for multi config. + self.options_schema = create_multi_option_schema_step_2(config_entry.options) @staticmethod def _validate_state(state: State | None) -> Decimal | None: # noqa: F821 @@ -204,6 +286,7 @@ def _validate_state(state: State | None) -> Decimal | None: # noqa: F821 except DecimalException: return None + async def async_step_init( self, user_input = None ): @@ -233,18 +316,63 @@ async def async_step_init( user_input[CONF_CREATE_CALCULATION_SENSOR] = False except DecimalException: errors["base"] = "source_calc_sensor_not_a_number" - if self.config_entry.options[CONF_TARIFFS] == [] or self.config_entry.options[CONF_TARIFFS] is None: + _LOGGER.debug("Option self.data: %s", self) + _LOGGER.debug("Option user_input: %s", user_input) + ###Issues here somehow + if self.data[CONF_TARIFFS] == [] or self.data is None: user_input[CONF_TARIFFS] = [] - if self.config_entry.options["config_type"] == CONF_CONFIG_CRON: + if self.data["config_type"] == CONF_CONFIG_CRON: + user_input[CONF_CONFIG_CALIBRATE_APPLY] = None + user_input[CONF_CONFIG_CALIBRATE_CALC_APPLY] = None user_input[CONF_CONFIG_TYPE] = CONF_CONFIG_CRON user_input[CONF_METER_OFFSET] =CONF_METER_OFFSET_DURATION_DEFAULT user_input[CONF_METER_TYPE] = None - else: + elif self.data["config_type"] == CONF_CONFIG_PREDEFINED: + user_input[CONF_CONFIG_CALIBRATE_APPLY] = None + user_input[CONF_CONFIG_CALIBRATE_CALC_APPLY] = None user_input[CONF_CONFIG_CRON] = None user_input[CONF_CONFIG_TYPE] = CONF_CONFIG_PREDEFINED + elif self.data["config_type"] == CONF_CONFIG_MULTI: + user_input[CONF_CONFIG_CRON] = None + user_input[CONF_CONFIG_TYPE] = CONF_CONFIG_MULTI + user_input[CONF_METER_OFFSET] = CONF_METER_OFFSET_DURATION_DEFAULT + #user_input[CONF_TARIFFS] = [] # noqa: PGH003 # type: ignore + + _LOGGER.debug("async Option step 1 self.data: %s", self.data) + #if self.data["config_type"] == CONF_CONFIG_MULTI: + # return await self.async_multi_option_step_2() + + _LOGGER.debug("async Option NOT MULTI: %s", self.data["config_type"]) return self.async_create_entry(title=self.config_entry.title, data=user_input) + # For multi config, we need to show the next step. + #self.data = user_input + #return await self.async_multi_step_2() #options_schema = OPTIONS_SCHEMA + _LOGGER.debug("async Option init schema: %s", self.options_schema) + return self.async_show_form( + step_id="init", + data_schema=self.options_schema, + errors=errors + ) + + async def async_multi_option_step_2(self, user_input = None): + """Second step in config flow to add a repo to watch.""" + #errors: dict[str, str] = {} + #self.data = self.data or {} # Initialize data if not set + _LOGGER.debug("async Option step 2 self.data: %s", self.data) + if user_input is not None: + # Validate the path. + await validate() + + # Input is valid, set data. + self.data.update(user_input) # noqa: PGH003 # type: ignore + return self.async_create_entry( + title=self.config_entry.title, + data=self.data + ) + _LOGGER.debug("async Option step 2 schema: %s", create_multi_option_schema_step_2(self.data)) return self.async_show_form( - step_id="init", data_schema=self.options_schema, errors=errors + step_id="init_2", + data_schema=create_multi_option_schema_step_2(self.data) ) diff --git a/custom_components/utility_meter_next_gen/const.py b/custom_components/utility_meter_next_gen/const.py index 90ef0fa..d3a1a45 100644 --- a/custom_components/utility_meter_next_gen/const.py +++ b/custom_components/utility_meter_next_gen/const.py @@ -2,7 +2,24 @@ DOMAIN = "utility_meter_next_gen" +METER_NAME_EVERY_FIVE_MINUTES = "5 Minute" +METER_NAME_EVERY_TEN_MINUTES = "10 Minute" +METER_NAME_EVERY_TWENTY_MINUTES = "20 Minute" +METER_NAME_HALF_HOURLY = "30 Minute" +METER_NAME_QUARTER_HOURLY = "15 Minute" +METER_NAME_HOURLY = "Hourly" +METER_NAME_DAILY = "Daily" +METER_NAME_WEEKLY = "Weekly" +METER_NAME_MONTHLY = "Monthly" +METER_NAME_BIMONTHLY = "Bi Monthly" +METER_NAME_QUARTERLY = "Quarterly" +METER_NAME_HALF_YEARLY = "Half Yearly" +METER_NAME_YEARLY = "Yearly" + +# Meter types EVERY_FIVE_MINUTES = "every-five-minutes" +EVERY_TEN_MINUTES = "every-ten-minutes" +EVERY_TWENTY_MINUTES = "every-twenty-minutes" HALF_HOURLY = "half-hourly" QUARTER_HOURLY = "quarter-hourly" HOURLY = "hourly" @@ -14,10 +31,47 @@ HALF_YEARLY = "half-yearly" YEARLY = "yearly" +# Meter type names +METER_NAME_TYPES = { + EVERY_FIVE_MINUTES: METER_NAME_EVERY_FIVE_MINUTES, + EVERY_TEN_MINUTES: METER_NAME_EVERY_TEN_MINUTES, + QUARTER_HOURLY: METER_NAME_QUARTER_HOURLY, + EVERY_TWENTY_MINUTES: METER_NAME_EVERY_TWENTY_MINUTES, + HALF_HOURLY: METER_NAME_HALF_HOURLY, + HOURLY: METER_NAME_HOURLY, + DAILY: METER_NAME_DAILY, + WEEKLY: METER_NAME_WEEKLY, + MONTHLY: METER_NAME_MONTHLY, + BIMONTHLY: METER_NAME_BIMONTHLY, + QUARTERLY: METER_NAME_QUARTERLY, + HALF_YEARLY: METER_NAME_HALF_YEARLY, + YEARLY: METER_NAME_YEARLY, +} +# List of meter types +# This list is used to validate the meter type in the configuration. +# It should match the keys in METER_NAME_TYPES. +# The order of the list is important for the UI display. METER_TYPES = [ "none", EVERY_FIVE_MINUTES, + EVERY_TEN_MINUTES, QUARTER_HOURLY, + EVERY_TWENTY_MINUTES, + HALF_HOURLY, + HOURLY, + DAILY, + WEEKLY, + MONTHLY, + BIMONTHLY, + QUARTERLY, + HALF_YEARLY, + YEARLY, +] +MULTI_METER_TYPES = [ + EVERY_FIVE_MINUTES, + EVERY_TEN_MINUTES, + QUARTER_HOURLY, + EVERY_TWENTY_MINUTES, HALF_HOURLY, HOURLY, DAILY, @@ -38,10 +92,12 @@ SINGLE_TARIFF = "single_tariff" TOTAL_TARIFF = "total" - +CONF_CONFIG_CALIBRATE_APPLY = "calibrate_apply" +CONF_CONFIG_CALIBRATE_CALC_APPLY = "calibrate_calc_apply" CONF_CONFIG_CALIBRATE_CALC_VALUE = "calibrate_calc_value" CONF_CONFIG_CALIBRATE_VALUE = "calibrate_value" CONF_CONFIG_CRON = "cron" +CONF_CONFIG_MULTI = "multi" CONF_CONFIG_PREDEFINED = "predefined" CONF_CONFIG_TYPE = "config_type" CONF_CREATE_CALCULATION_SENSOR = "create_calculation_sensor" @@ -67,6 +123,7 @@ CONFIG_TYPES = [ CONF_CONFIG_CRON, CONF_CONFIG_PREDEFINED, + CONF_CONFIG_MULTI, ] CONF_METER_TYPES = [ @@ -89,8 +146,10 @@ ATTR_CRON_PATTERN = "cron pattern" ATTR_LAST_PERIOD = "last_period" ATTR_LAST_VALID_STATE = "last_valid_state" +ATTR_LINKED_METER = "linked_utility_meter" ATTR_NEXT_RESET = "next_reset" ATTR_PERIOD = "meter_period" +ATTR_PREDEFINED_CYCLE = "Predefined Cycle" ATTR_SOURCE_ID = "source" ATTR_STATUS = "status" ATTR_TARIFF = "tariff" @@ -121,14 +180,16 @@ PERIOD2CRON = { EVERY_FIVE_MINUTES: "{minute}/5 * * * *", + EVERY_TEN_MINUTES: "{minute}/10 * * * *", QUARTER_HOURLY: "{minute}/15 * * * *", + EVERY_TWENTY_MINUTES: "{minute}/20 * * * *", HALF_HOURLY: "{minute}/30 * * * *", HOURLY: "{minute} * * * *", DAILY: "{minute} {hour} * * *", WEEKLY: "{minute} {hour} * * {day}", - MONTHLY: "{minute} {hour} {day} * *", - BIMONTHLY: "{minute} {hour} {day} */2 *", - QUARTERLY: "{minute} {hour} {day} */3 *", - HALF_YEARLY: "{minute} {hour} {day} */6 *", - YEARLY: "{minute} {hour} {day} 1/12 *", + MONTHLY: "{minute} {hour} 1 * *", + BIMONTHLY: "{minute} {hour} 1 */2 *", + QUARTERLY: "{minute} {hour} 1 */3 *", + HALF_YEARLY: "{minute} {hour} 1 */6 *", + YEARLY: "{minute} {hour} 1 1 *", } diff --git a/custom_components/utility_meter_next_gen/manifest.json b/custom_components/utility_meter_next_gen/manifest.json index 1f0da82..c8fa527 100644 --- a/custom_components/utility_meter_next_gen/manifest.json +++ b/custom_components/utility_meter_next_gen/manifest.json @@ -11,6 +11,6 @@ "issue_tracker": "https://github.com/cabberley/utility_meter_evolved/issues", "requirements": ["cronsim==2.6"], "ssdp": [], - "version": "2025.7.6", + "version": "2025.7.7", "zeroconf": [] } diff --git a/custom_components/utility_meter_next_gen/schemas.py b/custom_components/utility_meter_next_gen/schemas.py index 22c7ab5..3ac3770 100644 --- a/custom_components/utility_meter_next_gen/schemas.py +++ b/custom_components/utility_meter_next_gen/schemas.py @@ -8,6 +8,8 @@ from homeassistant.helpers import selector from .const import ( + CONF_CONFIG_CALIBRATE_APPLY, + CONF_CONFIG_CALIBRATE_CALC_APPLY, CONF_CONFIG_CALIBRATE_CALC_VALUE, CONF_CONFIG_CALIBRATE_VALUE, CONF_CONFIG_CRON, @@ -30,6 +32,7 @@ CONFIG_TYPES, DEVICE_CLASSES_METER, METER_TYPES, + MULTI_METER_TYPES, ) _LOGGER = logging.getLogger(__name__) @@ -60,7 +63,8 @@ { vol.Required(CONF_METER_TYPE): selector.SelectSelector( selector.SelectSelectorConfig( - options=METER_TYPES, translation_key=CONF_METER_TYPE + options=METER_TYPES, + translation_key=CONF_METER_TYPE ), ), vol.Optional(CONF_METER_OFFSET, @@ -78,11 +82,54 @@ } ) +BASE_MULTI_CONFIG_SCHEMA_STEP_1 = vol.Schema( + { + vol.Required(CONF_METER_TYPE,default=[]): selector.SelectSelector( + selector.SelectSelectorConfig( + options=MULTI_METER_TYPES, + translation_key=CONF_METER_TYPE, + mode=selector.SelectSelectorMode.DROPDOWN, + custom_value =False, + multiple=True + ), + ), + } +) + +BASE_COMMON_CONFIG_SCHEMA = { + vol.Optional( + CONF_CONFIG_CALIBRATE_VALUE, + default=0 + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + step="any", + ), + ), + vol.Required(CONF_TARIFFS, default=[]): selector.SelectSelector( + selector.SelectSelectorConfig(options=[], custom_value=True, multiple=True), + ), + vol.Required( + CONF_METER_NET_CONSUMPTION, default=False + ): selector.BooleanSelector(), + vol.Required( + CONF_METER_DELTA_VALUES, default=False + ): selector.BooleanSelector(), + vol.Required( + CONF_METER_PERIODICALLY_RESETTING, + default=True, + ): selector.BooleanSelector(), + vol.Optional( + CONF_SENSOR_ALWAYS_AVAILABLE, + default=True, + ): selector.BooleanSelector(), +} def create_calc_extras_schema(data): """Create the calibration schema for predefined and cron cycles.""" calibrate_default = data.get(CONF_CONFIG_CALIBRATE_CALC_VALUE,0) multiplier_default = data.get(CONF_SOURCE_CALC_MULTIPLIER, 1) - create_calc_sensor_default = data.get(CONF_CREATE_CALCULATION_SENSOR, CONF_CREATE_CALCULATION_SENSOR_DEFAULT) + create_calc_sensor_default = data.get(CONF_CREATE_CALCULATION_SENSOR, + CONF_CREATE_CALCULATION_SENSOR_DEFAULT) if data[CONF_SOURCE_CALC_SENSOR] is not None: return { vol.Required( @@ -110,8 +157,10 @@ def create_calc_extras_schema(data): } return {} - -BASE_COMMON_CONFIG_SCHEMA = { +def create_common_multi_config_schema_step_2a(data): + """Create the common configuration schema for multi configurations.""" + return vol.Schema( + { vol.Optional( CONF_CONFIG_CALIBRATE_VALUE, default=0 @@ -121,6 +170,68 @@ def create_calc_extras_schema(data): step="any", ), ), + vol.Optional( + CONF_CONFIG_CALIBRATE_APPLY): + selector.SelectSelector( + selector.SelectSelectorConfig( + options=data.get(CONF_METER_TYPE, []), + translation_key=CONF_METER_TYPE, + mode=selector.SelectSelectorMode.DROPDOWN, + custom_value =False, + multiple=False + ), + ), + } + ) + +def create_common_multi_config_schema_step_2b(data): + """Create the calibration schema for multi cycles.""" + calibrate_default = data.get(CONF_CONFIG_CALIBRATE_CALC_VALUE,0) + multiplier_default = data.get(CONF_SOURCE_CALC_MULTIPLIER, 1) + selected_meter_types = data.get(CONF_METER_TYPE, []) + create_calc_sensor_default = data.get(CONF_CREATE_CALCULATION_SENSOR, + CONF_CREATE_CALCULATION_SENSOR_DEFAULT) + if data[CONF_SOURCE_CALC_SENSOR] is not None: + return { + vol.Required( + CONF_CREATE_CALCULATION_SENSOR, + default=create_calc_sensor_default): + selector.BooleanSelector( + selector.BooleanSelectorConfig() + ), + vol.Required( + CONF_SOURCE_CALC_MULTIPLIER, default=multiplier_default + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + step="any", + ), + ), + vol.Required( + CONF_CONFIG_CALIBRATE_CALC_VALUE, + default=calibrate_default): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + step="any", + ), + ), + vol.Optional( + CONF_CONFIG_CALIBRATE_CALC_APPLY): + selector.SelectSelector( + selector.SelectSelectorConfig( + options=selected_meter_types, + translation_key=CONF_METER_TYPE, + mode=selector.SelectSelectorMode.DROPDOWN, + custom_value =False, + multiple=False + ), + ), + } + return {} + +def create_common_multi_config_schema_step_2c(): + """Create the common configuration schema for multi configurations.""" + return { vol.Required(CONF_TARIFFS, default=[]): selector.SelectSelector( selector.SelectSelectorConfig(options=[], custom_value=True, multiple=True), ), @@ -162,6 +273,25 @@ def create_cron_config_schema(data): } ) +def create_multi_config_schema_step_1(data): + """Create the configuration schema for predefined cycles.""" + + return vol.Schema( + { + **BASE_MULTI_CONFIG_SCHEMA_STEP_1.schema, + } + ) + +def create_multi_config_schema_step_2(data): + """Create the configuration schema for predefined cycles.""" + + return vol.Schema( + { + **(create_common_multi_config_schema_step_2a(data)).schema, + **create_common_multi_config_schema_step_2b(data), + **create_common_multi_config_schema_step_2c() + } + ) def create_base_predefined_option_schema(data): """Create the base options schema for predefined cycles.""" @@ -213,8 +343,6 @@ def create_common_option_schema(data): ), ), } - #if not hasattr(data, CONF_TARIFFS): - # _LOGGER.debug("Config has no Current Tariffs") if data[CONF_TARIFFS] != [] and data[CONF_TARIFFS] is not None: _LOGGER.debug("Schema Current Tariffs: %s", data[CONF_TARIFFS] ) option_schema[ @@ -367,7 +495,187 @@ def create_cron_option_schema(data): return vol.Schema( {**cron_begin.schema, **create_base_cron_option_schema(data), - #**(create_calc_extras_schema(data) or {}), **create_common_option_schema(data) } ) + +def create_multi_option_schema_step_1(data): + """Create the options step 1 schema for multi configurations.""" + if data[CONF_SOURCE_CALC_SENSOR] is None: + multi_option_step_1 = vol.Schema( + { + vol.Required(CONF_SOURCE_SENSOR, + default=data[CONF_SOURCE_SENSOR]): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=SENSOR_DOMAIN, filter={"device_class": DEVICE_CLASSES_METER}), + ), + vol.Optional(CONF_SOURCE_CALC_SENSOR ): selector.EntitySelector( + selector.EntitySelectorConfig(domain=SENSOR_DOMAIN), + ), + vol.Required( + CONF_CREATE_CALCULATION_SENSOR, + default=data[CONF_CREATE_CALCULATION_SENSOR]): + selector.BooleanSelector( + selector.BooleanSelectorConfig() + ), + vol.Required( + CONF_SOURCE_CALC_MULTIPLIER, default=1 + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + step="any", + ), + ), + } + ) + else: + multi_option_step_1 = vol.Schema( + { + vol.Required(CONF_SOURCE_SENSOR, + default=data[CONF_SOURCE_SENSOR]): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=SENSOR_DOMAIN, filter={"device_class": DEVICE_CLASSES_METER}), + ), + vol.Optional(CONF_SOURCE_CALC_SENSOR, + default=data[CONF_SOURCE_CALC_SENSOR] ): selector.EntitySelector( + selector.EntitySelectorConfig(domain=SENSOR_DOMAIN), + ), + vol.Required( + CONF_CREATE_CALCULATION_SENSOR, + default=data[CONF_CREATE_CALCULATION_SENSOR]): + selector.BooleanSelector( + selector.BooleanSelectorConfig() + ), + vol.Required( + CONF_REMOVE_CALC_SENSOR, + default=False, + ): selector.BooleanSelector(), + vol.Required( + CONF_SOURCE_CALC_MULTIPLIER, default=data[CONF_SOURCE_CALC_MULTIPLIER] + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + step="any", + ), + ), + + } + ) + meter_types ={ + vol.Required(CONF_METER_TYPE,default=data[CONF_METER_TYPE]): selector.SelectSelector( + selector.SelectSelectorConfig( + options=METER_TYPES, + translation_key=CONF_METER_TYPE, + mode=selector.SelectSelectorMode.DROPDOWN, + custom_value =False, + multiple=True + ), + ), + } + if data[CONF_TARIFFS] != [] and data[CONF_TARIFFS] is not None: + _LOGGER.debug("Schema Current Tariffs: %s", data[CONF_TARIFFS] ) + multi_option_step_tariffs = { + vol.Optional(CONF_TARIFFS, default=data[CONF_TARIFFS]): + selector.SelectSelector( + selector.SelectSelectorConfig( + options=[], custom_value=True, multiple=True + ), + ) + } + else: + _LOGGER.debug("Schema Current Tariffs: %s", data[CONF_TARIFFS] ) + multi_option_step_tariffs = {} + # + + return vol.Schema( + {**multi_option_step_1.schema, + **meter_types, + **multi_option_step_tariffs, + } + ) + +def create_multi_option_schema_step_2(data): + """Create the options step 2 schema for multi configurations.""" + multi_option_step_2 = vol.Schema( + { + vol.Optional( + CONF_CONFIG_CALIBRATE_VALUE, + default=data[CONF_CONFIG_CALIBRATE_VALUE]): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + step="any", + ), + ), + vol.Required( + CONF_CONFIG_CALIBRATE_APPLY, + default=data[CONF_CONFIG_CALIBRATE_APPLY]): + selector.SelectSelector( + selector.SelectSelectorConfig( + options=data.get(CONF_METER_TYPE, []), + translation_key=CONF_METER_TYPE, + mode=selector.SelectSelectorMode.DROPDOWN, + custom_value =False, + multiple=False + ), + ), + } + ) + if data[CONF_SOURCE_CALC_SENSOR] is not None: + multi_option_calc = { + vol.Required( + CONF_SOURCE_CALC_MULTIPLIER, data[CONF_SOURCE_CALC_MULTIPLIER] + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + step="any", + ), + ), + vol.Optional( + CONF_CONFIG_CALIBRATE_CALC_VALUE, + default=data[CONF_CONFIG_CALIBRATE_CALC_VALUE]): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + step="any", + ), + ), + vol.Required( + CONF_CONFIG_CALIBRATE_CALC_APPLY, default=data[CONF_CONFIG_CALIBRATE_CALC_APPLY]): + selector.SelectSelector( + selector.SelectSelectorConfig( + options=data.get(CONF_METER_TYPE, []), + translation_key=CONF_METER_TYPE, + mode=selector.SelectSelectorMode.DROPDOWN, + custom_value =False, + multiple=False + ), + ), + } + else: + multi_option_calc = {} + + multi_option_common = { + vol.Required( + CONF_METER_NET_CONSUMPTION, + default=data[CONF_METER_NET_CONSUMPTION], + ): selector.BooleanSelector(), + vol.Required( + CONF_METER_DELTA_VALUES, default=data[CONF_METER_DELTA_VALUES] + ): selector.BooleanSelector(), + vol.Required( + CONF_METER_PERIODICALLY_RESETTING, + default=data[ + CONF_METER_PERIODICALLY_RESETTING + ], # Assuming this is the correct key + ): selector.BooleanSelector(), + vol.Optional( + CONF_SENSOR_ALWAYS_AVAILABLE, + default=data[CONF_SENSOR_ALWAYS_AVAILABLE], + ): selector.BooleanSelector(), + } + return vol.Schema( + {**create_multi_option_schema_step_1(data).schema, + **multi_option_step_2.schema, + **multi_option_calc, + **multi_option_common, + } + ) diff --git a/custom_components/utility_meter_next_gen/sensor.py b/custom_components/utility_meter_next_gen/sensor.py index b6a2cc7..4414e67 100644 --- a/custom_components/utility_meter_next_gen/sensor.py +++ b/custom_components/utility_meter_next_gen/sensor.py @@ -60,12 +60,16 @@ ATTR_CALC_LAST_VALUE, ATTR_LAST_PERIOD, ATTR_LAST_VALID_STATE, + ATTR_LINKED_METER, ATTR_NEXT_RESET, + ATTR_PREDEFINED_CYCLE, ATTR_SOURCE_ID, ATTR_STATUS, ATTR_TARIFF, ATTR_VALUE, COLLECTING, + CONF_CONFIG_CALIBRATE_APPLY, + CONF_CONFIG_CALIBRATE_CALC_APPLY, CONF_CONFIG_CALIBRATE_CALC_VALUE, CONF_CONFIG_CALIBRATE_VALUE, CONF_CREATE_CALCULATION_SENSOR, @@ -85,6 +89,7 @@ CONF_TARIFFS, DATA_TARIFF_SENSORS, DATA_UTILITY, + METER_NAME_TYPES, PAUSED, PERIOD2CRON, PRECISION, @@ -96,20 +101,30 @@ _LOGGER = logging.getLogger(__name__) + def validate_is_number(value): """Validate value is a number.""" if is_number(value): return value raise vol.Invalid("Value is not a number") + def clean_string(input_string): """Replace non-alphanumeric characters with underscores.""" - result = re.sub(r'[^a-zA-Z0-9]', '_', input_string) + result = re.sub(r"[^a-zA-Z0-9]", "_", input_string) # Remove consecutive underscores - result = re.sub(r'_+', '_', result) + result = re.sub(r"_+", "_", result) # Convert to lowercase return result.lower() +def clean_string_display(input_string): + """Replace non-alphanumeric characters with underscores.""" + result = re.sub(r"[^a-zA-Z0-9]", " ", input_string) + # Remove consecutive underscores + result = re.sub(r" +", " ", result) + # Convert to lowercase + return result.title() + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -132,8 +147,10 @@ async def async_setup_entry( ) else: source_calc_entity_id = None - calibrate_calc_value=config_entry.options[CONF_CONFIG_CALIBRATE_CALC_VALUE] - calibrate_value=config_entry.options[CONF_CONFIG_CALIBRATE_VALUE] + calibrate_calc_apply = config_entry.options[CONF_CONFIG_CALIBRATE_CALC_APPLY] + calibrate_calc_value = config_entry.options[CONF_CONFIG_CALIBRATE_CALC_VALUE] + calibrate_apply = config_entry.options[CONF_CONFIG_CALIBRATE_APPLY] + calibrate_value = config_entry.options[CONF_CONFIG_CALIBRATE_VALUE] create_calc_sensor = config_entry.options[CONF_CREATE_CALCULATION_SENSOR] cron_pattern = config_entry.options[CONF_CRON_PATTERN] delta_values = config_entry.options[CONF_METER_DELTA_VALUES] @@ -154,57 +171,139 @@ async def async_setup_entry( calc_sensors = [] tariffs = config_entry.options[CONF_TARIFFS] - if not tariffs: - # Add single sensor, not gated by a tariff selector - meter_sensor = UtilityMeterSensor( - calibrate_calc_value=calibrate_calc_value, - calibrate_value=calibrate_value, - cron_pattern=cron_pattern, - delta_values=delta_values, - device_info=device_info, - meter_offset=meter_offset, - meter_type=meter_type, - name=name, - net_consumption=net_consumption, - parent_meter=entry_id, - periodically_resetting=periodically_resetting, - sensor_always_available=sensor_always_available, - source_calc_entity=source_calc_entity_id, - source_calc_multiplier=source_calc_multiplier, - source_entity=source_entity_id, - tariff_entity=tariff_entity, - tariff=None, - unique_id=entry_id, - ) - meters.append(meter_sensor) - hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(meter_sensor) + if meter_type is not None and not isinstance(meter_type, str) and len(meter_type) > 1: + for meter in meter_type: + if not tariffs: + # Add single sensor, not gated by a tariff selector + meter_sensor = UtilityMeterSensor( + calibrate_calc_value=( + calibrate_calc_value + if calibrate_calc_apply == meter + else Decimal(0) + ), + calibrate_value=( + calibrate_value if calibrate_apply == meter else Decimal(0) + ), + cron_pattern=cron_pattern, + delta_values=delta_values, + device_info=device_info, + meter_offset=meter_offset, + meter_type=meter, + name=f"{name} {METER_NAME_TYPES[meter]}", + net_consumption=net_consumption, + parent_meter=entry_id, + periodically_resetting=periodically_resetting, + sensor_always_available=sensor_always_available, + source_calc_entity=source_calc_entity_id, + source_calc_multiplier=source_calc_multiplier, + source_entity=source_entity_id, + tariff_entity=tariff_entity, + tariff=None, + unique_id=f"{entry_id}_{METER_NAME_TYPES[meter]}", + ) + meters.append(meter_sensor) + hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append( + meter_sensor + ) - if create_calc_sensor: - # Create a calculated sensor for the total consumption - calc_sensor = UtilityMeterCalculatedSensor( - attribute = ATTR_CALC_CURRENT_VALUE, - calibrate_calc_value=calibrate_calc_value, - cron_pattern=cron_pattern, - device_class = None, #device_class, - device_info = device_info, - entity_id = f"sensor.{clean_string(name)}", - hass = hass, - icon = None, - meter_type=meter_type, - name = f"{name} Calculated", - source_calc_entity=source_calc_entity_id, - source_entity=source_entity_id, - state_class = None, #state_class, - tariff=None, - unique_id = f"{name} Calculated", - uom = None, - ) - calc_sensors.append(calc_sensor) - hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(calc_sensor) + if create_calc_sensor: + # Create a calculated sensor for the total consumption + calc_sensor = UtilityMeterCalculatedSensor( + attribute=ATTR_CALC_CURRENT_VALUE, + calibrate_calc_value=( + calibrate_calc_value + if calibrate_calc_apply == meter + else Decimal(0) + ), + cron_pattern=cron_pattern, + device_class=None, + device_info=device_info, + entity_id=( + f"sensor.{clean_string(name)}_{clean_string(METER_NAME_TYPES[meter])}" + ), + hass=hass, + icon=None, + meter_type=meter, + name=f"{name} {METER_NAME_TYPES[meter]} Calculated", + source_calc_entity=source_calc_entity_id, + source_entity=source_entity_id, + state_class=None, + tariff=None, + unique_id=f"{name} {METER_NAME_TYPES[meter]} Calculated", + uom=None, + ) + calc_sensors.append(calc_sensor) + hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append( + calc_sensor + ) + + else: + # Add sensors for each tariff + for tariff in tariffs: + meter_sensor = UtilityMeterSensor( + calibrate_calc_value=( + calibrate_calc_value + if calibrate_calc_apply == meter + else Decimal(0) + ), + calibrate_value=( + calibrate_value if calibrate_apply == meter else Decimal(0) + ), + cron_pattern=cron_pattern, + delta_values=delta_values, + device_info=device_info, + meter_offset=meter_offset, + meter_type=meter, + name=f"{name} {METER_NAME_TYPES[meter]} {tariff}", + net_consumption=net_consumption, + parent_meter=entry_id, + periodically_resetting=periodically_resetting, + sensor_always_available=sensor_always_available, + source_calc_entity=source_calc_entity_id, + source_calc_multiplier=source_calc_multiplier, + source_entity=source_entity_id, + tariff_entity=tariff_entity, + tariff=tariff, + unique_id=f"{entry_id}_{METER_NAME_TYPES[meter]}_{tariff}", + ) + meters.append(meter_sensor) + hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append( + meter_sensor + ) + if create_calc_sensor: + # Create a calculated sensor for the tariff consumption + calc_sensor = UtilityMeterCalculatedSensor( + attribute=ATTR_CALC_CURRENT_VALUE, + calibrate_calc_value=( + calibrate_calc_value + if calibrate_calc_apply == meter + else Decimal(0) + ), + cron_pattern=cron_pattern, + device_class=None, + device_info=device_info, + entity_id=( + f"sensor.{clean_string(name)}_{clean_string(METER_NAME_TYPES[meter])}_{clean_string(tariff)}" + ), + hass=hass, + icon=None, + meter_type=meter, + name=f"{name} {METER_NAME_TYPES[meter]} {tariff} Calculated", + source_calc_entity=source_calc_entity_id, + source_entity=source_entity_id, + state_class=None, + tariff=tariff, + unique_id=f"{name} {METER_NAME_TYPES[meter]} {tariff} Calculated", + uom=None, + ) + calc_sensors.append(calc_sensor) + hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append( + calc_sensor + ) else: - # Add sensors for each tariff - for tariff in tariffs: + if not tariffs: + # Add single sensor, not gated by a tariff selector meter_sensor = UtilityMeterSensor( calibrate_calc_value=calibrate_calc_value, calibrate_value=calibrate_value, @@ -213,42 +312,97 @@ async def async_setup_entry( device_info=device_info, meter_offset=meter_offset, meter_type=meter_type, - name=f"{name} {tariff}", + name=name, net_consumption=net_consumption, parent_meter=entry_id, periodically_resetting=periodically_resetting, sensor_always_available=sensor_always_available, source_calc_entity=source_calc_entity_id, - source_calc_multiplier = source_calc_multiplier, + source_calc_multiplier=source_calc_multiplier, source_entity=source_entity_id, tariff_entity=tariff_entity, - tariff=tariff, - unique_id=f"{entry_id}_{tariff}", + tariff=None, + unique_id=entry_id, ) meters.append(meter_sensor) hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(meter_sensor) + if create_calc_sensor: - # Create a calculated sensor for the tariff consumption + # Create a calculated sensor for the total consumption calc_sensor = UtilityMeterCalculatedSensor( - attribute = ATTR_CALC_CURRENT_VALUE, - calibrate_calc_value=calibrate_calc_value, - cron_pattern=cron_pattern, - device_class = None, #device_class, - device_info = device_info, - entity_id = f"sensor.{clean_string(name)}_{clean_string(tariff)}", - hass = hass, - icon = None, - meter_type=meter_type, - name = f"{name} {tariff} Calculated", - source_calc_entity=source_calc_entity_id, - source_entity=source_entity_id, - state_class = None, #state_class, - tariff=tariff, - unique_id = f"{name} {tariff} Calculated", - uom = None, + attribute=ATTR_CALC_CURRENT_VALUE, + calibrate_calc_value=calibrate_calc_value, + cron_pattern=cron_pattern, + device_class=None, + device_info=device_info, + entity_id=f"sensor.{clean_string(name)}", + hass=hass, + icon=None, + meter_type=meter_type, + name=f"{name} Calculated", + source_calc_entity=source_calc_entity_id, + source_entity=source_entity_id, + state_class=None, + tariff=None, + unique_id=f"{name} Calculated", + uom=None, ) calc_sensors.append(calc_sensor) - hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(calc_sensor) + hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append( + calc_sensor + ) + + else: + # Add sensors for each tariff + for tariff in tariffs: + meter_sensor = UtilityMeterSensor( + calibrate_calc_value=calibrate_calc_value, + calibrate_value=calibrate_value, + cron_pattern=cron_pattern, + delta_values=delta_values, + device_info=device_info, + meter_offset=meter_offset, + meter_type=meter_type, + name=f"{name} {tariff}", + net_consumption=net_consumption, + parent_meter=entry_id, + periodically_resetting=periodically_resetting, + sensor_always_available=sensor_always_available, + source_calc_entity=source_calc_entity_id, + source_calc_multiplier=source_calc_multiplier, + source_entity=source_entity_id, + tariff_entity=tariff_entity, + tariff=tariff, + unique_id=f"{entry_id}_{tariff}", + ) + meters.append(meter_sensor) + hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append( + meter_sensor + ) + if create_calc_sensor: + # Create a calculated sensor for the tariff consumption + calc_sensor = UtilityMeterCalculatedSensor( + attribute=ATTR_CALC_CURRENT_VALUE, + calibrate_calc_value=calibrate_calc_value, + cron_pattern=cron_pattern, + device_class=None, # device_class, + device_info=device_info, + entity_id=f"sensor.{clean_string(name)}_{clean_string(tariff)}", + hass=hass, + icon=None, + meter_type=meter_type, + name=f"{name} {tariff} Calculated", + source_calc_entity=source_calc_entity_id, + source_entity=source_entity_id, + state_class=None, + tariff=tariff, + unique_id=f"{name} {tariff} Calculated", + uom=None, + ) + calc_sensors.append(calc_sensor) + hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append( + calc_sensor + ) async_add_entities(meters) async_add_entities(calc_sensors) @@ -419,16 +573,18 @@ class UtilityMeterSensor(RestoreSensor): _attr_translation_key = "utility_meter" _attr_should_poll = False _unrecorded_attributes = frozenset( - {ATTR_NEXT_RESET, - CONF_CRON_PATTERN, - CONF_METER_TYPE, - ATTR_SOURCE_ID, - CONF_SOURCE_CALC_SENSOR, - CONF_SOURCE_CALC_MULTIPLIER, - CONF_CONFIG_CALIBRATE_CALC_VALUE, - CONF_CONFIG_CALIBRATE_VALUE + { + ATTR_NEXT_RESET, + CONF_CRON_PATTERN, + CONF_METER_TYPE, + ATTR_SOURCE_ID, + CONF_SOURCE_CALC_SENSOR, + CONF_SOURCE_CALC_MULTIPLIER, + CONF_CONFIG_CALIBRATE_CALC_VALUE, + CONF_CONFIG_CALIBRATE_VALUE, } - ) + ) + def __init__( self, *, @@ -443,8 +599,8 @@ def __init__( source_entity, source_calc_entity, source_calc_multiplier, - calibrate_value, #=Decimal(0), - calibrate_calc_value, #=Decimal(0), + calibrate_value, # =Decimal(0), + calibrate_calc_value, # =Decimal(0), tariff_entity, tariff, unique_id, @@ -485,7 +641,7 @@ def __init__( self._sensor_delta_values = delta_values self._sensor_net_consumption = net_consumption self._sensor_periodically_resetting = periodically_resetting - self._calibrate_value = Decimal(calibrate_value) or Decimal(0) + self._calibrate_value = Decimal(calibrate_value) or Decimal(0) self._calibrate_calc_value = calibrate_calc_value or Decimal(0) self._tariff = tariff self._tariff_entity = tariff_entity @@ -499,7 +655,7 @@ def _config_scheduler(self): self._cron_pattern, dt_util.now( dt_util.get_default_time_zone() - ), # we need timezone for DST purposes (see issue #102984) + ), ) if self._cron_pattern else None @@ -510,7 +666,9 @@ def start(self, attributes: Mapping[str, Any]) -> None: self._input_device_class = attributes.get(ATTR_DEVICE_CLASS) self._attr_native_unit_of_measurement = attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._attr_native_value = Decimal(self._calibrate_value) - self._attr_calculated_current_value = Decimal(self._calibrate_calc_value) # Decimal(0) + self._attr_calculated_current_value = Decimal( + self._calibrate_calc_value + ) self._attr_calculated_last_value = Decimal(0) self.async_write_ha_state() @@ -607,7 +765,11 @@ def async_reading(self, event: Event[EventStateChangedData]) -> None: if ( self._sensor_calc_source_id is not None - and (source_calc_state := self.hass.states.get(self._sensor_calc_source_id)) + and ( + source_calc_state := self.hass.states.get( + self._sensor_calc_source_id + ) + ) and source_calc_state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN] ): try: @@ -617,11 +779,15 @@ def async_reading(self, event: Event[EventStateChangedData]) -> None: else: # If the tariff is not a single tariff or total, we use 0 as the calibration value calibrate_value = Decimal(0) - self._attr_calculated_current_value = round(( - Decimal(source_calc_state.state) - * Decimal(self._attr_native_value) - * Decimal(self._attr_multiplier) - ) + calibrate_value,PRECISION) + self._attr_calculated_current_value = round( + ( + Decimal(source_calc_state.state) + * Decimal(self._attr_native_value) + * Decimal(self._attr_multiplier) + ) + + calibrate_value, + PRECISION, + ) except (DecimalException, InvalidOperation) as err: _LOGGER.error( "Error while parsing value %s from sensor %s: %s", @@ -707,7 +873,8 @@ async def async_reset_meter(self, entity_id): self._last_reset = dt_util.utcnow() self._last_period = ( Decimal(self.native_value) # noqa: PGH003 # type: ignore - if self.native_value else Decimal(0) + if self.native_value + else Decimal(0) ) # update Calculated value if we have a calculation sensor if self._sensor_calc_source_id is not None: @@ -745,9 +912,11 @@ async def async_added_to_hass(self) -> None: ) if (last_sensor_data := await self.async_get_last_sensor_data()) is not None: - self._attr_native_value = (None - if last_sensor_data.native_value is None - else Decimal(last_sensor_data.native_value)) + self._attr_native_value = ( + None + if last_sensor_data.native_value is None + else Decimal(last_sensor_data.native_value) + ) self._input_device_class = last_sensor_data.input_device_class self._attr_native_unit_of_measurement = ( last_sensor_data.native_unit_of_measurement @@ -849,23 +1018,30 @@ def extra_state_attributes(self): if self._cron_pattern is not None: state_attr[CONF_CRON_PATTERN] = self._cron_pattern if self._period is not None: - state_attr[CONF_METER_TYPE] = self._period - if (self._calibrate_value != 0 - and str(self._tariff).lower() in [SINGLE_TARIFF, TOTAL_TARIFF] - ): + state_attr[ATTR_PREDEFINED_CYCLE] = METER_NAME_TYPES[self._period] + if self._calibrate_value != 0 and str(self._tariff).lower() in [ + SINGLE_TARIFF, + TOTAL_TARIFF, + ]: state_attr[CONF_CONFIG_CALIBRATE_VALUE] = str(self._calibrate_value) if self._sensor_source_id is not None: state_attr[ATTR_SOURCE_ID] = self._sensor_source_id if self._sensor_calc_source_id is not None: state_attr[CONF_SOURCE_CALC_SENSOR] = self._sensor_calc_source_id - state_attr[ATTR_CALC_CURRENT_VALUE] = ( - str(round(self._attr_calculated_current_value,PRECISION))) - state_attr[ATTR_CALC_LAST_VALUE] = ( - str(round(self._attr_calculated_last_value,PRECISION))) + state_attr[ATTR_CALC_CURRENT_VALUE] = str( + round(self._attr_calculated_current_value, PRECISION) + ) + state_attr[ATTR_CALC_LAST_VALUE] = str( + round(self._attr_calculated_last_value, PRECISION) + ) state_attr[CONF_SOURCE_CALC_MULTIPLIER] = str(self._attr_multiplier) - if (self._calibrate_calc_value != 0 - and str(self._tariff).lower() in [SINGLE_TARIFF, TOTAL_TARIFF]): - state_attr[CONF_CONFIG_CALIBRATE_CALC_VALUE] = str(self._calibrate_calc_value) + if self._calibrate_calc_value != 0 and str(self._tariff).lower() in [ + SINGLE_TARIFF, + TOTAL_TARIFF, + ]: + state_attr[CONF_CONFIG_CALIBRATE_CALC_VALUE] = str( + self._calibrate_calc_value + ) return state_attr @property @@ -890,24 +1066,25 @@ async def async_get_last_sensor_data(self) -> UtilitySensorExtraStoredData | Non restored_last_extra_data.as_dict() ) - -################################################################################################# class UtilityMeterCalculatedSensor(RestoreSensor): """Representation of an Seperate Calculated sensor.""" _attr_should_poll = False _attr_collecting_status = None _unrecorded_attributes = frozenset( - {ATTR_NEXT_RESET, - ATTR_SOURCE_ID, - CONF_CONFIG_CALIBRATE_CALC_VALUE, - CONF_CONFIG_CALIBRATE_VALUE, - CONF_CRON_PATTERN, - CONF_METER_TYPE, - CONF_SOURCE_CALC_MULTIPLIER, - CONF_SOURCE_CALC_SENSOR + { + ATTR_NEXT_RESET, + ATTR_SOURCE_ID, + CONF_CONFIG_CALIBRATE_CALC_VALUE, + CONF_CONFIG_CALIBRATE_VALUE, + CONF_CRON_PATTERN, + CONF_METER_TYPE, + CONF_SOURCE_CALC_MULTIPLIER, + CONF_SOURCE_CALC_SENSOR, + ATTR_LINKED_METER, } - ) + ) + def __init__( self, hass: HomeAssistant, @@ -926,17 +1103,20 @@ def __init__( tariff: str | None, unique_id: str | None, uom: str | None, - #value_template: Template | None, + ) -> None: """Initialize the sensor.""" self._attr_collecting_status = None - self._attr_device_class = (SensorDeviceClass.MONETARY - if device_class is None else device_class) + self._attr_device_class = ( + SensorDeviceClass.MONETARY if device_class is None else device_class + ) self._attr_device_info = device_info self._attr_icon = "mdi:currency-usd" if icon is None else icon self._attr_name = name self._attr_native_unit_of_measurement = CURRENCY_DOLLAR if uom is None else uom - self._attr_state_class = SensorStateClass.TOTAL if state_class is None else state_class + self._attr_state_class = ( + SensorStateClass.TOTAL if state_class is None else state_class + ) self._attr_unique_id = unique_id self._attribute = attribute self._calibrate_calc_value = calibrate_calc_value or Decimal(0) @@ -950,7 +1130,6 @@ def __init__( def start(self, attributes: Mapping[str, Any]) -> None: """Initialize unit and state upon source initial update.""" - #self._attr_device_class = attributes.get(ATTR_DEVICE_CLASS) self.async_write_ha_state() async def async_added_to_hass(self) -> None: @@ -975,8 +1154,9 @@ async def async_added_to_hass(self) -> None: @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" + state_attr = { - "linked_utility_meter": self._entity_id, + ATTR_LINKED_METER: self._entity_id, ATTR_STATUS: ( self._attr_collecting_status if self._attr_collecting_status is not None @@ -984,16 +1164,20 @@ def extra_state_attributes(self) -> dict[str, Any]: ), } if self._period is not None: - state_attr[CONF_METER_TYPE] = self._period + state_attr[ATTR_PREDEFINED_CYCLE] = METER_NAME_TYPES[self._period] elif self._cron_pattern is not None: state_attr[CONF_CRON_PATTERN] = self._cron_pattern if self._sensor_source_id is not None: state_attr[ATTR_SOURCE_ID] = self._sensor_source_id if self._source_calc_entity is not None: state_attr[CONF_SOURCE_CALC_SENSOR] = self._source_calc_entity - if (self._calibrate_calc_value != 0 - and str(self._tariff).lower() in [SINGLE_TARIFF, TOTAL_TARIFF]): - state_attr[CONF_CONFIG_CALIBRATE_CALC_VALUE] = str(self._calibrate_calc_value) + if self._calibrate_calc_value != 0 and str(self._tariff).lower() in [ + SINGLE_TARIFF, + TOTAL_TARIFF, + ]: + state_attr[CONF_CONFIG_CALIBRATE_CALC_VALUE] = str( + self._calibrate_calc_value + ) return state_attr @callback @@ -1004,14 +1188,14 @@ def _async_attribute_sensor_state_listener( new_state = event.data["new_state"] _LOGGER.debug("Received new state: %s", new_state) - self._attr_available = True #new_state #!= STATE_UNAVAILABLE + self._attr_available = True self._attr_native_value = None if ( new_state is None or new_state.state is None or new_state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN] - or hasattr(new_state, "attributes") is False + or hasattr(new_state, "attributes") is False ): if not update_state: _LOGGER.debug("Ignoring state update") @@ -1039,9 +1223,9 @@ def _async_attribute_sensor_state_listener( return _LOGGER.debug("state update step 1") - _LOGGER.debug("state update step 1 %s",self._attribute) - _LOGGER.debug("state update step 1a %s",new_state.state) - _LOGGER.debug("state update step 1a %s",new_state.attributes) + _LOGGER.debug("state update step 1 %s", self._attribute) + _LOGGER.debug("state update step 1a %s", new_state.state) + _LOGGER.debug("state update step 1a %s", new_state.attributes) self._has_logged = False if self._attribute in new_state.attributes: diff --git a/custom_components/utility_meter_next_gen/strings.json b/custom_components/utility_meter_next_gen/strings.json index 7792de0..0083380 100644 --- a/custom_components/utility_meter_next_gen/strings.json +++ b/custom_components/utility_meter_next_gen/strings.json @@ -14,7 +14,7 @@ "source_calc_sensor": "Input Calculation Sensor (Optional if you wish to create a price meter)" }, "data_description": { - "config_type": "Choose a predefined reset cycle or create a custom CRON pattern.", + "config_type": "Choose the type of Utility Meter to create, predefined reset cycle, custom CRON pattern or a set of predefiend Utility Meters (generate various reset cycles from the one config).", "source_calc_sensor": "If you want to use a sensor to Calculate a price attribute for the Meter, please select it here. If not selected, the meter will just report the source value." }, "description": "Create a sensor which tracks consumption of various utilities (e.g., energy, gas, water, heating) over a configured period of time. The utility meter sensor optionally supports splitting the consumption by tariffs, in that case one sensor for each tariff is created as well as a select entity to choose the current tariff and optionally calculating a $$ cost by combining with a price sensor.", @@ -77,6 +77,47 @@ "source_calc_multiplier": "If you need to adjust the calculation to match the raw source sensor, you can set the multiplier here. For example if your Input Calculator Sensor is $/kWh and your Consumption Sensor is in MW, you would set this to 1000 to get the correct price.", "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed. If you want to track the total consumption, add a 'Total' tariff." } + }, + "multi_step_1": { + "data": { + "cycle": "Predefined Reset Cycle" + }, + "description": "Select Predefined reset cycles for the Utility Meter.", + "data_description": { + "cycle": "Select one or more predefined reset cycles for the meter." + } + }, + "multi_step_2": { + "data": { + "always_available": "Sensor always available", + "calibrate_apply": "Apply Calibration to which Predefined Cycle? (Optional)", + "calibrate_calc_apply": "Apply Calculation Calibration to which Predefined Cycle? (Optional)", + "calibrate_calc_value": "Calibration Value for Calculation (Optional)", + "calibrate_value": "Calibration Value for Meter (Optional)", + "create_calculation_sensor": "Create a separate Calculation Sensor (Optional)", + "delta_values": "Delta Values", + "net_consumption": "Net Consumption", + "periodically_resetting": "Periodically resetting", + "source_calc_multiplier": "Adjustment factor for the calculation sensor (Optional)", + "tariffs": "Tariffs - add a 'total' tariff to track total consumption" + }, + "description": "Configure the Utility Meter options.", + "data_description": { + "always_available": "If activated, the sensor will always show the last known value, even if the source entity is unavailable or unknown.", + "calibrate_apply": "Apply Calibration to which Predefined Cycle? (If your Calibration is not needed and is set to 0, just leave this to the defaul displayed Cycle)", + "calibrate_calc_apply": "Apply Calculation Calibration to which Predefined Cycle? (If your Calibration is not needed and is set to 0, just leave this to the defaul displayed Cycle)", + "calibrate_calc_value": "When the reset cycle is reached, the calculation sensor will be set to this value. This is useful if you want to start the meter Calc to a specific value.", + "calibrate_value": "When the reset cycle is reached, the consumption sensor will be set to this value. This is useful if you want to start the meter on a specific value.", + "create_calculation_sensor": "In addition to the Consumption Sensor, a Calculation Sensor will be created which can be used to calculate a price attribute for the Meter. If you do not need this, you can disable it here.", + "delta_values": "Enable if the source values are delta values since the last reading instead of absolute values.", + "net_consumption": "Enable if the source is a net meter, meaning it can both increase and decrease.", + "offset": "Offset the day/hour/minute of the meter reset (seconds are ignored atm). This is useful if you want to start the meter on a specific day of the month.", + "periodically_resetting": "Enable if the source may periodically reset to 0, for example at boot of the measuring device. If disabled, new readings are directly recorded after data inavailability.", + "source_calc_multiplier": "If you need to adjust the calculation to match the raw source sensor, you can set the multiplier here. For esxample if your Input Calculator Sensor is $/kWh and your Consumption Sensor is in MW, you would set this to 1000 to get the correct price.", + "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed. If you want to track the total consumption, add a 'total' tariff." + + }, + "title": "Configure Options" } } }, @@ -85,8 +126,10 @@ "init": { "data": { "always_available": "Sensor always available", + "calibrate_apply": "Apply Calibration to which Predefined Cycle? (Optional)", + "calibrate_calc_apply": "Apply Calculation Calibration to which Predefined Cycle? (Optional)", "calibrate_calc_value": "Calibration Value for Calculation (Optional)", - "calibrate_value": "Calibration Value for Consumption (Optional)", + "calibrate_value": "Calibration Value for Meter (Optional)", "create_calculation_sensor": "Create a separate Calculation Sensor (Optional)", "cron": "Custom CRON Pattern", "cycle": "Predefined Reset Cycle", @@ -102,6 +145,8 @@ }, "data_description": { "always_available": "If activated, the sensor will always show the last known value, even if the source entity is unavailable or unknown.", + "calibrate_apply": "Apply Calibration to which Predefined Cycle? (If your Calibration is not needed and is set to 0, just leave this to the defaul displayed Cycle)", + "calibrate_calc_apply": "Apply Calculation Calibration to which Predefined Cycle? (If your Calibration is not needed and is set to 0, just leave this to the defaul displayed Cycle)", "calibrate_calc_value": "When the reset cycle is reached, the calculation sensor will be set to this value. This is useful if you want to start the meter Calc to a specific value.", "calibrate_value": "When the reset cycle is reached, the consumption sensor will be set to this value. This is useful if you want to start the meter on a specific value.", "create_calculation_sensor": "In addition to the Consumption Sensor, a Calculation Sensor will be created which can be used to calculate a price attribute for the Meter. If you do not need this, you can disable it here.", @@ -115,6 +160,23 @@ "source_calc_sensor": "If you want to use a sensor to Calculate a price attribute for the Meter, please select it here. If not selected, the meter will just report the source value.", "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed. If you want to track the total consumption, add a 'Total' tariff." } + }, + "init_2": { + "data": { + "config_calibrate_apply": "Apply Calibration", + "config_calibrate_value": "Calibration Value", + "config_cron": "Custom CRON Pattern", + "config_type": "Configuration Type", + "source_calc_multiplier": "Adjustment factor for the calculation sensor (Optional)" + }, + "data_description": { + "config_calibrate_apply": "Select the sensor to which the calibration value should be applied.", + "config_calibrate_value": "Set the value to which the meter should be calibrated.", + "config_cron": "Enter a custom CRON pattern for the reset cycle.", + "config_type": "Choose between predefined cycles or custom CRON patterns.", + "source_calc_multiplier": "If you need to adjust the calculation to match the raw source sensor, you can set the multiplier here." + }, + "title": "Configure Options" } } }, @@ -124,6 +186,8 @@ "bimonthly": "Every two months", "daily": "Daily", "every-five-minutes": "Every 5 minutes", + "every-ten-minutes": "Every 10 minutes", + "every-twenty-minutes": "Every 20 minutes", "half-hourly": "Every 30 minutes", "half-yearly": "Half Yearly", "hourly": "Hourly", @@ -137,8 +201,9 @@ }, "config_type": { "options": { - "cron": "Create Custom CRON Pattern", - "predefined": "Use Predefined reset cycle" + "cron": "Create using a custom CRON pattern", + "predefined": "Create using a Predefined reset cycle", + "multi": "Create Multiple Predefined reset cycle Meters" } } }, diff --git a/custom_components/utility_meter_next_gen/translations/en.json b/custom_components/utility_meter_next_gen/translations/en.json index b035381..0083380 100644 --- a/custom_components/utility_meter_next_gen/translations/en.json +++ b/custom_components/utility_meter_next_gen/translations/en.json @@ -14,7 +14,7 @@ "source_calc_sensor": "Input Calculation Sensor (Optional if you wish to create a price meter)" }, "data_description": { - "config_type": "Choose a predefined reset cycle or create a custom CRON pattern.", + "config_type": "Choose the type of Utility Meter to create, predefined reset cycle, custom CRON pattern or a set of predefiend Utility Meters (generate various reset cycles from the one config).", "source_calc_sensor": "If you want to use a sensor to Calculate a price attribute for the Meter, please select it here. If not selected, the meter will just report the source value." }, "description": "Create a sensor which tracks consumption of various utilities (e.g., energy, gas, water, heating) over a configured period of time. The utility meter sensor optionally supports splitting the consumption by tariffs, in that case one sensor for each tariff is created as well as a select entity to choose the current tariff and optionally calculating a $$ cost by combining with a price sensor.", @@ -77,6 +77,47 @@ "source_calc_multiplier": "If you need to adjust the calculation to match the raw source sensor, you can set the multiplier here. For example if your Input Calculator Sensor is $/kWh and your Consumption Sensor is in MW, you would set this to 1000 to get the correct price.", "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed. If you want to track the total consumption, add a 'Total' tariff." } + }, + "multi_step_1": { + "data": { + "cycle": "Predefined Reset Cycle" + }, + "description": "Select Predefined reset cycles for the Utility Meter.", + "data_description": { + "cycle": "Select one or more predefined reset cycles for the meter." + } + }, + "multi_step_2": { + "data": { + "always_available": "Sensor always available", + "calibrate_apply": "Apply Calibration to which Predefined Cycle? (Optional)", + "calibrate_calc_apply": "Apply Calculation Calibration to which Predefined Cycle? (Optional)", + "calibrate_calc_value": "Calibration Value for Calculation (Optional)", + "calibrate_value": "Calibration Value for Meter (Optional)", + "create_calculation_sensor": "Create a separate Calculation Sensor (Optional)", + "delta_values": "Delta Values", + "net_consumption": "Net Consumption", + "periodically_resetting": "Periodically resetting", + "source_calc_multiplier": "Adjustment factor for the calculation sensor (Optional)", + "tariffs": "Tariffs - add a 'total' tariff to track total consumption" + }, + "description": "Configure the Utility Meter options.", + "data_description": { + "always_available": "If activated, the sensor will always show the last known value, even if the source entity is unavailable or unknown.", + "calibrate_apply": "Apply Calibration to which Predefined Cycle? (If your Calibration is not needed and is set to 0, just leave this to the defaul displayed Cycle)", + "calibrate_calc_apply": "Apply Calculation Calibration to which Predefined Cycle? (If your Calibration is not needed and is set to 0, just leave this to the defaul displayed Cycle)", + "calibrate_calc_value": "When the reset cycle is reached, the calculation sensor will be set to this value. This is useful if you want to start the meter Calc to a specific value.", + "calibrate_value": "When the reset cycle is reached, the consumption sensor will be set to this value. This is useful if you want to start the meter on a specific value.", + "create_calculation_sensor": "In addition to the Consumption Sensor, a Calculation Sensor will be created which can be used to calculate a price attribute for the Meter. If you do not need this, you can disable it here.", + "delta_values": "Enable if the source values are delta values since the last reading instead of absolute values.", + "net_consumption": "Enable if the source is a net meter, meaning it can both increase and decrease.", + "offset": "Offset the day/hour/minute of the meter reset (seconds are ignored atm). This is useful if you want to start the meter on a specific day of the month.", + "periodically_resetting": "Enable if the source may periodically reset to 0, for example at boot of the measuring device. If disabled, new readings are directly recorded after data inavailability.", + "source_calc_multiplier": "If you need to adjust the calculation to match the raw source sensor, you can set the multiplier here. For esxample if your Input Calculator Sensor is $/kWh and your Consumption Sensor is in MW, you would set this to 1000 to get the correct price.", + "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed. If you want to track the total consumption, add a 'total' tariff." + + }, + "title": "Configure Options" } } }, @@ -85,8 +126,10 @@ "init": { "data": { "always_available": "Sensor always available", + "calibrate_apply": "Apply Calibration to which Predefined Cycle? (Optional)", + "calibrate_calc_apply": "Apply Calculation Calibration to which Predefined Cycle? (Optional)", "calibrate_calc_value": "Calibration Value for Calculation (Optional)", - "calibrate_value": "Calibration Value for Consumption (Optional)", + "calibrate_value": "Calibration Value for Meter (Optional)", "create_calculation_sensor": "Create a separate Calculation Sensor (Optional)", "cron": "Custom CRON Pattern", "cycle": "Predefined Reset Cycle", @@ -94,7 +137,7 @@ "net_consumption": "Net Consumption", "offset": "Offset (optional, e.g., to set a starting value for the meter)", "periodically_resetting": "Periodically resetting", - "remove_calc_sensor": "Remove Calculation Sensor", + "remove_calc_sensor": "Remove Input Calculation Sensor", "source": "Input sensor to track consumption (Required)", "source_calc_multiplier": "Adjustment factor for the calculation sensor (Optional)", "source_calc_sensor": "Input calculation Sensor (Optional if you wish to create a price meter)", @@ -102,6 +145,8 @@ }, "data_description": { "always_available": "If activated, the sensor will always show the last known value, even if the source entity is unavailable or unknown.", + "calibrate_apply": "Apply Calibration to which Predefined Cycle? (If your Calibration is not needed and is set to 0, just leave this to the defaul displayed Cycle)", + "calibrate_calc_apply": "Apply Calculation Calibration to which Predefined Cycle? (If your Calibration is not needed and is set to 0, just leave this to the defaul displayed Cycle)", "calibrate_calc_value": "When the reset cycle is reached, the calculation sensor will be set to this value. This is useful if you want to start the meter Calc to a specific value.", "calibrate_value": "When the reset cycle is reached, the consumption sensor will be set to this value. This is useful if you want to start the meter on a specific value.", "create_calculation_sensor": "In addition to the Consumption Sensor, a Calculation Sensor will be created which can be used to calculate a price attribute for the Meter. If you do not need this, you can disable it here.", @@ -115,6 +160,23 @@ "source_calc_sensor": "If you want to use a sensor to Calculate a price attribute for the Meter, please select it here. If not selected, the meter will just report the source value.", "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed. If you want to track the total consumption, add a 'Total' tariff." } + }, + "init_2": { + "data": { + "config_calibrate_apply": "Apply Calibration", + "config_calibrate_value": "Calibration Value", + "config_cron": "Custom CRON Pattern", + "config_type": "Configuration Type", + "source_calc_multiplier": "Adjustment factor for the calculation sensor (Optional)" + }, + "data_description": { + "config_calibrate_apply": "Select the sensor to which the calibration value should be applied.", + "config_calibrate_value": "Set the value to which the meter should be calibrated.", + "config_cron": "Enter a custom CRON pattern for the reset cycle.", + "config_type": "Choose between predefined cycles or custom CRON patterns.", + "source_calc_multiplier": "If you need to adjust the calculation to match the raw source sensor, you can set the multiplier here." + }, + "title": "Configure Options" } } }, @@ -124,6 +186,8 @@ "bimonthly": "Every two months", "daily": "Daily", "every-five-minutes": "Every 5 minutes", + "every-ten-minutes": "Every 10 minutes", + "every-twenty-minutes": "Every 20 minutes", "half-hourly": "Every 30 minutes", "half-yearly": "Half Yearly", "hourly": "Hourly", @@ -137,8 +201,9 @@ }, "config_type": { "options": { - "cron": "Create Custom CRON Pattern", - "predefined": "Use Predefined reset cycle" + "cron": "Create using a custom CRON pattern", + "predefined": "Create using a Predefined reset cycle", + "multi": "Create Multiple Predefined reset cycle Meters" } } }, From 040b1adcf67e8f5c9e81a57a0a4f232b15e9a081 Mon Sep 17 00:00:00 2001 From: Chris Abberley <26394346+cabberley@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:10:47 +1000 Subject: [PATCH 02/11] Changes to be committed: modified: custom_components/utility_meter_next_gen/config_flow.py modified: custom_components/utility_meter_next_gen/schemas.py modified: custom_components/utility_meter_next_gen/sensor.py modified: custom_components/utility_meter_next_gen/strings.json modified: custom_components/utility_meter_next_gen/translations/en.json --- custom_components/utility_meter_next_gen/config_flow.py | 5 ++--- custom_components/utility_meter_next_gen/schemas.py | 1 - custom_components/utility_meter_next_gen/sensor.py | 2 +- custom_components/utility_meter_next_gen/strings.json | 1 - .../utility_meter_next_gen/translations/en.json | 1 - 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/custom_components/utility_meter_next_gen/config_flow.py b/custom_components/utility_meter_next_gen/config_flow.py index 927b023..aa5e9cb 100644 --- a/custom_components/utility_meter_next_gen/config_flow.py +++ b/custom_components/utility_meter_next_gen/config_flow.py @@ -113,7 +113,7 @@ async def async_step_user(self, user_input: Optional[dict[str, Any]] = None): # return await self.async_step_cron() elif user_input.get(CONF_CONFIG_TYPE) == CONF_CONFIG_PREDEFINED: # noqa: RET505 return await self.async_step_predefined() - elif user_input.get(CONF_CONFIG_TYPE) == CONF_CONFIG_MULTI: # noqa: RET505 + elif user_input.get(CONF_CONFIG_TYPE) == CONF_CONFIG_MULTI: return await self.async_step_multi_step_1() #return await self.async_step_repo() @@ -258,8 +258,7 @@ def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the options flow handler.""" self.options_schema =None self.config_type = None - #self.config_entry = config_entry - self.data = config_entry.options.copy() # type: ignore + self.data = config_entry.options.copy() #noqa: PGH003 # type: ignore _LOGGER.debug("async Option init self.data: %s", self.data) if config_entry.options["config_type"] == CONF_CONFIG_CRON: self.config_type = CONF_CONFIG_CRON diff --git a/custom_components/utility_meter_next_gen/schemas.py b/custom_components/utility_meter_next_gen/schemas.py index 3ac3770..49cbb62 100644 --- a/custom_components/utility_meter_next_gen/schemas.py +++ b/custom_components/utility_meter_next_gen/schemas.py @@ -585,7 +585,6 @@ def create_multi_option_schema_step_1(data): else: _LOGGER.debug("Schema Current Tariffs: %s", data[CONF_TARIFFS] ) multi_option_step_tariffs = {} - # return vol.Schema( {**multi_option_step_1.schema, diff --git a/custom_components/utility_meter_next_gen/sensor.py b/custom_components/utility_meter_next_gen/sensor.py index 4414e67..248df1e 100644 --- a/custom_components/utility_meter_next_gen/sensor.py +++ b/custom_components/utility_meter_next_gen/sensor.py @@ -301,7 +301,7 @@ async def async_setup_entry( calc_sensor ) - else: + else: # noqa: PLR5501 if not tariffs: # Add single sensor, not gated by a tariff selector meter_sensor = UtilityMeterSensor( diff --git a/custom_components/utility_meter_next_gen/strings.json b/custom_components/utility_meter_next_gen/strings.json index 0083380..07ef44a 100644 --- a/custom_components/utility_meter_next_gen/strings.json +++ b/custom_components/utility_meter_next_gen/strings.json @@ -111,7 +111,6 @@ "create_calculation_sensor": "In addition to the Consumption Sensor, a Calculation Sensor will be created which can be used to calculate a price attribute for the Meter. If you do not need this, you can disable it here.", "delta_values": "Enable if the source values are delta values since the last reading instead of absolute values.", "net_consumption": "Enable if the source is a net meter, meaning it can both increase and decrease.", - "offset": "Offset the day/hour/minute of the meter reset (seconds are ignored atm). This is useful if you want to start the meter on a specific day of the month.", "periodically_resetting": "Enable if the source may periodically reset to 0, for example at boot of the measuring device. If disabled, new readings are directly recorded after data inavailability.", "source_calc_multiplier": "If you need to adjust the calculation to match the raw source sensor, you can set the multiplier here. For esxample if your Input Calculator Sensor is $/kWh and your Consumption Sensor is in MW, you would set this to 1000 to get the correct price.", "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed. If you want to track the total consumption, add a 'total' tariff." diff --git a/custom_components/utility_meter_next_gen/translations/en.json b/custom_components/utility_meter_next_gen/translations/en.json index 0083380..07ef44a 100644 --- a/custom_components/utility_meter_next_gen/translations/en.json +++ b/custom_components/utility_meter_next_gen/translations/en.json @@ -111,7 +111,6 @@ "create_calculation_sensor": "In addition to the Consumption Sensor, a Calculation Sensor will be created which can be used to calculate a price attribute for the Meter. If you do not need this, you can disable it here.", "delta_values": "Enable if the source values are delta values since the last reading instead of absolute values.", "net_consumption": "Enable if the source is a net meter, meaning it can both increase and decrease.", - "offset": "Offset the day/hour/minute of the meter reset (seconds are ignored atm). This is useful if you want to start the meter on a specific day of the month.", "periodically_resetting": "Enable if the source may periodically reset to 0, for example at boot of the measuring device. If disabled, new readings are directly recorded after data inavailability.", "source_calc_multiplier": "If you need to adjust the calculation to match the raw source sensor, you can set the multiplier here. For esxample if your Input Calculator Sensor is $/kWh and your Consumption Sensor is in MW, you would set this to 1000 to get the correct price.", "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed. If you want to track the total consumption, add a 'total' tariff." From e7e7e015b508f4a79c7612ef13660def4ab4a2b2 Mon Sep 17 00:00:00 2001 From: Chris Abberley <26394346+cabberley@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:01:32 +1000 Subject: [PATCH 03/11] Changes to be committed: modified: custom_components/utility_meter_next_gen/config_flow.py modified: custom_components/utility_meter_next_gen/manifest.json modified: custom_components/utility_meter_next_gen/schemas.py modified: custom_components/utility_meter_next_gen/sensor.py --- .../utility_meter_next_gen/config_flow.py | 14 +++++++++++--- .../utility_meter_next_gen/manifest.json | 2 +- .../utility_meter_next_gen/schemas.py | 7 ++++--- custom_components/utility_meter_next_gen/sensor.py | 8 ++++---- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/custom_components/utility_meter_next_gen/config_flow.py b/custom_components/utility_meter_next_gen/config_flow.py index aa5e9cb..95d9f72 100644 --- a/custom_components/utility_meter_next_gen/config_flow.py +++ b/custom_components/utility_meter_next_gen/config_flow.py @@ -218,7 +218,7 @@ async def async_step_multi_step_1(self, user_input: errors=errors ) async def async_step_multi_step_2(self, user_input: - Optional[dict[str, Any]] = None): # noqa: UP045 + Optional[dict[str, Any]] = None): # noqa: UP045 """Second step in config flow to add a repo to watch.""" errors: dict[str, str] = {} self.data = self.data or {} # Initialize data if not set @@ -232,9 +232,17 @@ async def async_step_multi_step_2(self, user_input: if not errors: # Input is valid, set data. - if user_input[CONF_CONFIG_CALIBRATE_APPLY] == "none": + if ( + (CONF_CONFIG_CALIBRATE_APPLY in user_input + and user_input[CONF_CONFIG_CALIBRATE_APPLY] == "none") + or (CONF_CONFIG_CALIBRATE_APPLY not in user_input) + ): user_input[CONF_CONFIG_CALIBRATE_APPLY] = None - if user_input[CONF_CONFIG_CALIBRATE_CALC_APPLY] == "none": + if ( + (CONF_CONFIG_CALIBRATE_CALC_APPLY in user_input + and user_input[CONF_CONFIG_CALIBRATE_CALC_APPLY] == "none") + or (CONF_CONFIG_CALIBRATE_CALC_APPLY not in user_input) + ): user_input[CONF_CONFIG_CALIBRATE_CALC_APPLY] = None self.data.update(user_input) # noqa: PGH003 # type: ignore return self.async_create_entry(title=self.data["name"], # noqa: PGH003 # type: ignore diff --git a/custom_components/utility_meter_next_gen/manifest.json b/custom_components/utility_meter_next_gen/manifest.json index c8fa527..50f4f30 100644 --- a/custom_components/utility_meter_next_gen/manifest.json +++ b/custom_components/utility_meter_next_gen/manifest.json @@ -11,6 +11,6 @@ "issue_tracker": "https://github.com/cabberley/utility_meter_evolved/issues", "requirements": ["cronsim==2.6"], "ssdp": [], - "version": "2025.7.7", + "version": "2025.7.8", "zeroconf": [] } diff --git a/custom_components/utility_meter_next_gen/schemas.py b/custom_components/utility_meter_next_gen/schemas.py index 49cbb62..3cc494e 100644 --- a/custom_components/utility_meter_next_gen/schemas.py +++ b/custom_components/utility_meter_next_gen/schemas.py @@ -336,7 +336,7 @@ def create_common_option_schema(data): ), vol.Optional( CONF_CONFIG_CALIBRATE_CALC_VALUE, - default=data[CONF_CONFIG_CALIBRATE_CALC_VALUE]): selector.NumberSelector( + default=data.get(CONF_CONFIG_CALIBRATE_CALC_VALUE,0)): selector.NumberSelector( selector.NumberSelectorConfig( mode=selector.NumberSelectorMode.BOX, step="any", @@ -631,14 +631,15 @@ def create_multi_option_schema_step_2(data): ), vol.Optional( CONF_CONFIG_CALIBRATE_CALC_VALUE, - default=data[CONF_CONFIG_CALIBRATE_CALC_VALUE]): selector.NumberSelector( + default=data.get(CONF_CONFIG_CALIBRATE_CALC_VALUE,0)): selector.NumberSelector( selector.NumberSelectorConfig( mode=selector.NumberSelectorMode.BOX, step="any", ), ), vol.Required( - CONF_CONFIG_CALIBRATE_CALC_APPLY, default=data[CONF_CONFIG_CALIBRATE_CALC_APPLY]): + CONF_CONFIG_CALIBRATE_CALC_APPLY, + default=data.get(CONF_CONFIG_CALIBRATE_CALC_APPLY, [])): selector.SelectSelector( selector.SelectSelectorConfig( options=data.get(CONF_METER_TYPE, []), diff --git a/custom_components/utility_meter_next_gen/sensor.py b/custom_components/utility_meter_next_gen/sensor.py index 248df1e..3455a98 100644 --- a/custom_components/utility_meter_next_gen/sensor.py +++ b/custom_components/utility_meter_next_gen/sensor.py @@ -147,10 +147,10 @@ async def async_setup_entry( ) else: source_calc_entity_id = None - calibrate_calc_apply = config_entry.options[CONF_CONFIG_CALIBRATE_CALC_APPLY] - calibrate_calc_value = config_entry.options[CONF_CONFIG_CALIBRATE_CALC_VALUE] - calibrate_apply = config_entry.options[CONF_CONFIG_CALIBRATE_APPLY] - calibrate_value = config_entry.options[CONF_CONFIG_CALIBRATE_VALUE] + calibrate_calc_apply = config_entry.options.get(CONF_CONFIG_CALIBRATE_CALC_APPLY, None) + calibrate_calc_value = config_entry.options.get(CONF_CONFIG_CALIBRATE_CALC_VALUE, Decimal(0)) + calibrate_apply = config_entry.options.get(CONF_CONFIG_CALIBRATE_APPLY, None) + calibrate_value = config_entry.options.get(CONF_CONFIG_CALIBRATE_VALUE, Decimal(0)) create_calc_sensor = config_entry.options[CONF_CREATE_CALCULATION_SENSOR] cron_pattern = config_entry.options[CONF_CRON_PATTERN] delta_values = config_entry.options[CONF_METER_DELTA_VALUES] From eb41bd7ac6fc5cd8b1a04100e4759bf32584078e Mon Sep 17 00:00:00 2001 From: Chris Abberley <26394346+cabberley@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:49:32 +1000 Subject: [PATCH 04/11] Changes to be committed: modified: custom_components/utility_meter_next_gen/manifest.json --- custom_components/utility_meter_next_gen/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/utility_meter_next_gen/manifest.json b/custom_components/utility_meter_next_gen/manifest.json index 50f4f30..52dd759 100644 --- a/custom_components/utility_meter_next_gen/manifest.json +++ b/custom_components/utility_meter_next_gen/manifest.json @@ -11,6 +11,6 @@ "issue_tracker": "https://github.com/cabberley/utility_meter_evolved/issues", "requirements": ["cronsim==2.6"], "ssdp": [], - "version": "2025.7.8", + "version": "2025.7.9", "zeroconf": [] } From 6fa17ffe5f04c740ce542c9502837415b30b46a1 Mon Sep 17 00:00:00 2001 From: Chris Abberley <26394346+cabberley@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:04:45 +1000 Subject: [PATCH 05/11] Changes to be committed: modified: custom_components/utility_meter_next_gen/manifest.json modified: custom_components/utility_meter_next_gen/sensor.py --- .../utility_meter_next_gen/manifest.json | 2 +- .../utility_meter_next_gen/sensor.py | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/custom_components/utility_meter_next_gen/manifest.json b/custom_components/utility_meter_next_gen/manifest.json index 52dd759..e9fc20b 100644 --- a/custom_components/utility_meter_next_gen/manifest.json +++ b/custom_components/utility_meter_next_gen/manifest.json @@ -11,6 +11,6 @@ "issue_tracker": "https://github.com/cabberley/utility_meter_evolved/issues", "requirements": ["cronsim==2.6"], "ssdp": [], - "version": "2025.7.9", + "version": "2025.7.10", "zeroconf": [] } diff --git a/custom_components/utility_meter_next_gen/sensor.py b/custom_components/utility_meter_next_gen/sensor.py index 3455a98..72355bc 100644 --- a/custom_components/utility_meter_next_gen/sensor.py +++ b/custom_components/utility_meter_next_gen/sensor.py @@ -773,19 +773,15 @@ def async_reading(self, event: Event[EventStateChangedData]) -> None: and source_calc_state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN] ): try: - if str(self._tariff).lower() in [SINGLE_TARIFF, TOTAL_TARIFF]: - # If the tariff is a single tariff or total, we use the calibration value - calibrate_value = Decimal(self._calibrate_calc_value) - else: - # If the tariff is not a single tariff or total, we use 0 as the calibration value - calibrate_value = Decimal(0) - self._attr_calculated_current_value = round( + self._attr_calculated_current_value += round( ( Decimal(source_calc_state.state) - * Decimal(self._attr_native_value) + # Bug fix with Multi Meters we need to use + # the adjustment and add not just recalc + # Decimal(self._attr_native_value) + * Decimal(adjustment) * Decimal(self._attr_multiplier) - ) - + calibrate_value, + ), PRECISION, ) except (DecimalException, InvalidOperation) as err: From 30affa4904ef6ec32b3f6acde3c38b8c556b6920 Mon Sep 17 00:00:00 2001 From: Chris Abberley <26394346+cabberley@users.noreply.github.com> Date: Thu, 10 Jul 2025 13:37:43 +1000 Subject: [PATCH 06/11] Changes to be committed: modified: custom_components/utility_meter_next_gen/manifest.json modified: custom_components/utility_meter_next_gen/sensor.py --- .../utility_meter_next_gen/manifest.json | 2 +- .../utility_meter_next_gen/sensor.py | 51 ++++++++++++++----- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/custom_components/utility_meter_next_gen/manifest.json b/custom_components/utility_meter_next_gen/manifest.json index e9fc20b..6085e4b 100644 --- a/custom_components/utility_meter_next_gen/manifest.json +++ b/custom_components/utility_meter_next_gen/manifest.json @@ -11,6 +11,6 @@ "issue_tracker": "https://github.com/cabberley/utility_meter_evolved/issues", "requirements": ["cronsim==2.6"], "ssdp": [], - "version": "2025.7.10", + "version": "2025.7.11", "zeroconf": [] } diff --git a/custom_components/utility_meter_next_gen/sensor.py b/custom_components/utility_meter_next_gen/sensor.py index 72355bc..3d11efc 100644 --- a/custom_components/utility_meter_next_gen/sensor.py +++ b/custom_components/utility_meter_next_gen/sensor.py @@ -518,6 +518,9 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): last_valid_state: Decimal | None status: str input_device_class: SensorDeviceClass | None + calculated_current_value: Decimal | None + calculated_last_value: Decimal | None + def as_dict(self) -> dict[str, Any]: """Return a dict representation of the utility sensor data.""" @@ -530,6 +533,8 @@ def as_dict(self) -> dict[str, Any]: ) data["status"] = self.status data["input_device_class"] = str(self.input_device_class) + data["calculated_current_value"] = str(self.calculated_current_value) + data["calculated_last_value"] = str(self.calculated_last_value) return data @@ -552,6 +557,17 @@ def from_dict(cls, restored: dict[str, Any]) -> Self | None: input_device_class = try_parse_enum( SensorDeviceClass, restored.get("input_device_class") ) + calculated_current_value: Decimal | None = ( + Decimal(restored["calculated_current_value"]) + if restored.get("calculated_current_value") + else None + ) + calculated_last_value: Decimal | None = ( + Decimal(restored["calculated_last_value"]) + if restored.get("calculated_last_value") + else None + ) + except (KeyError, InvalidOperation): # last_period is corrupted return None @@ -564,6 +580,8 @@ def from_dict(cls, restored: dict[str, Any]) -> Self | None: last_valid_state, status, input_device_class, + calculated_current_value, + calculated_last_value, ) @@ -599,8 +617,8 @@ def __init__( source_entity, source_calc_entity, source_calc_multiplier, - calibrate_value, # =Decimal(0), - calibrate_calc_value, # =Decimal(0), + calibrate_value, + calibrate_calc_value, tariff_entity, tariff, unique_id, @@ -622,8 +640,8 @@ def __init__( self._attr_name = name self._input_device_class = None self._attr_native_unit_of_measurement = None - self._attr_calculated_current_value = 0 - self._attr_calculated_last_value = 0 + self._attr_calculated_current_value = Decimal(0) + self._attr_calculated_last_value = Decimal(0) self._attr_multiplier = source_calc_multiplier or Decimal(1) self._period = meter_type if meter_type is not None: @@ -669,7 +687,6 @@ def start(self, attributes: Mapping[str, Any]) -> None: self._attr_calculated_current_value = Decimal( self._calibrate_calc_value ) - self._attr_calculated_last_value = Decimal(0) self.async_write_ha_state() @staticmethod @@ -726,7 +743,7 @@ def async_reading(self, event: Event[EventStateChangedData]) -> None: self.async_write_ha_state() return - self._attr_available = True + #self._attr_available = True old_state = event.data["old_state"] new_state = event.data["new_state"] @@ -776,9 +793,6 @@ def async_reading(self, event: Event[EventStateChangedData]) -> None: self._attr_calculated_current_value += round( ( Decimal(source_calc_state.state) - # Bug fix with Multi Meters we need to use - # the adjustment and add not just recalc - # Decimal(self._attr_native_value) * Decimal(adjustment) * Decimal(self._attr_multiplier) ), @@ -876,7 +890,7 @@ async def async_reset_meter(self, entity_id): if self._sensor_calc_source_id is not None: self._attr_calculated_last_value = self._attr_calculated_current_value if str(self._tariff).lower() in [SINGLE_TARIFF, TOTAL_TARIFF]: - self._attr_calculated_current_value = self._calibrate_calc_value + self._attr_calculated_current_value = Decimal(self._calibrate_calc_value) else: self._attr_calculated_current_value = Decimal(0) if str(self._tariff).lower() in [SINGLE_TARIFF, TOTAL_TARIFF]: @@ -923,6 +937,16 @@ async def async_added_to_hass(self) -> None: if last_sensor_data.status == COLLECTING: # Null lambda to allow cancelling the collection on tariff change self._collecting = lambda: None + self._attr_calculated_current_value = ( + Decimal(0) + if last_sensor_data.calculated_current_value is None + else Decimal(last_sensor_data.calculated_current_value) + ) + self._attr_calculated_last_value = ( + Decimal(0) + if last_sensor_data.calculated_last_value is None + else Decimal(last_sensor_data.calculated_last_value) + ) @callback def async_source_tracking(event): @@ -1051,6 +1075,8 @@ def extra_restore_state_data(self) -> UtilitySensorExtraStoredData: self._last_valid_state, PAUSED if self._collecting is None else COLLECTING, self._input_device_class, + self._attr_calculated_current_value, + self._attr_calculated_last_value, ) async def async_get_last_sensor_data(self) -> UtilitySensorExtraStoredData | None: @@ -1110,6 +1136,7 @@ def __init__( self._attr_icon = "mdi:currency-usd" if icon is None else icon self._attr_name = name self._attr_native_unit_of_measurement = CURRENCY_DOLLAR if uom is None else uom + self._attr_suggested_display_precision = PRECISION self._attr_state_class = ( SensorStateClass.TOTAL if state_class is None else state_class ) @@ -1184,8 +1211,6 @@ def _async_attribute_sensor_state_listener( new_state = event.data["new_state"] _LOGGER.debug("Received new state: %s", new_state) - self._attr_available = True - self._attr_native_value = None if ( new_state is None @@ -1200,7 +1225,7 @@ def _async_attribute_sensor_state_listener( "State update for %s is None or unavailable, setting to STATE_UNAVAILABLE", self._entity_id, ) - self._attr_native_value = Decimal(0) + #self._attr_native_value = STATE_UNAVAILABLE #Decimal(0) self._attr_collecting_status = STATE_UNAVAILABLE self.async_write_ha_state() return From 8b3a00b8741302c6e70b7c8a96c3ac31f8178926 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:10:08 +0000 Subject: [PATCH 07/11] Bump ruff from 0.12.2 to 0.12.3 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.2 to 0.12.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.2...0.12.3) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac25ce3..ebb10d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ colorlog==6.9.0 homeassistant==2025.6.1 pip>=21.3.1 -ruff==0.12.2 \ No newline at end of file +ruff==0.12.3 \ No newline at end of file From 3a5fed8d1a62db974a38945aef6491995a5db6df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:10:30 +0000 Subject: [PATCH 08/11] Update dependency ruff to v0.12.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac25ce3..ebb10d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ colorlog==6.9.0 homeassistant==2025.6.1 pip>=21.3.1 -ruff==0.12.2 \ No newline at end of file +ruff==0.12.3 \ No newline at end of file From 78d758b2bb1ece1243464ebe0a9385c2990db4ff Mon Sep 17 00:00:00 2001 From: Chris Abberley <26394346+cabberley@users.noreply.github.com> Date: Sun, 13 Jul 2025 14:45:23 +1000 Subject: [PATCH 09/11] Changes to be committed: modified: custom_components/utility_meter_next_gen/manifest.json modified: custom_components/utility_meter_next_gen/sensor.py --- .../utility_meter_next_gen/manifest.json | 2 +- .../utility_meter_next_gen/sensor.py | 27 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/custom_components/utility_meter_next_gen/manifest.json b/custom_components/utility_meter_next_gen/manifest.json index 6085e4b..961416e 100644 --- a/custom_components/utility_meter_next_gen/manifest.json +++ b/custom_components/utility_meter_next_gen/manifest.json @@ -11,6 +11,6 @@ "issue_tracker": "https://github.com/cabberley/utility_meter_evolved/issues", "requirements": ["cronsim==2.6"], "ssdp": [], - "version": "2025.7.11", + "version": "2025.7.12", "zeroconf": [] } diff --git a/custom_components/utility_meter_next_gen/sensor.py b/custom_components/utility_meter_next_gen/sensor.py index 3d11efc..c192291 100644 --- a/custom_components/utility_meter_next_gen/sensor.py +++ b/custom_components/utility_meter_next_gen/sensor.py @@ -171,7 +171,7 @@ async def async_setup_entry( calc_sensors = [] tariffs = config_entry.options[CONF_TARIFFS] - if meter_type is not None and not isinstance(meter_type, str) and len(meter_type) > 1: + if meter_type is not None and not isinstance(meter_type, str) and len(meter_type) > 1: # noqa: PLR5501 for meter in meter_type: if not tariffs: # Add single sensor, not gated by a tariff selector @@ -743,8 +743,6 @@ def async_reading(self, event: Event[EventStateChangedData]) -> None: self.async_write_ha_state() return - #self._attr_available = True - old_state = event.data["old_state"] new_state = event.data["new_state"] if new_state is None: @@ -762,6 +760,9 @@ def async_reading(self, event: Event[EventStateChangedData]) -> None: return if self.native_value is None: + _LOGGER.debug("selfHassDATA: %s",self.hass.data[DATA_UTILITY][self._parent_meter][ + DATA_TARIFF_SENSORS + ]) # First state update initializes the utility_meter sensors for sensor in self.hass.data[DATA_UTILITY][self._parent_meter][ DATA_TARIFF_SENSORS @@ -778,6 +779,7 @@ def async_reading(self, event: Event[EventStateChangedData]) -> None: adjustment := self.calculate_adjustment(old_state, new_state) ) is not None and (self._sensor_net_consumption or adjustment >= 0): # If net_consumption is off, the adjustment must be non-negative + _LOGGER.debug("%s: Adjustment Check: %s : %s", self.name, adjustment, self._tariff) self._attr_native_value += Decimal(adjustment) # type: ignore[operator] if ( @@ -822,7 +824,7 @@ def async_tariff_change(self, event: Event[EventStateChangedData]) -> None: self._change_status(new_state.state) def _change_status(self, tariff: str) -> None: - if str(self._tariff).lower() in [tariff.lower(), TOTAL_TARIFF]: + if self._tariff == tariff: self._collecting = async_track_state_change_event( self.hass, [self._sensor_source_id], self.async_reading ) @@ -889,14 +891,8 @@ async def async_reset_meter(self, entity_id): # update Calculated value if we have a calculation sensor if self._sensor_calc_source_id is not None: self._attr_calculated_last_value = self._attr_calculated_current_value - if str(self._tariff).lower() in [SINGLE_TARIFF, TOTAL_TARIFF]: - self._attr_calculated_current_value = Decimal(self._calibrate_calc_value) - else: - self._attr_calculated_current_value = Decimal(0) - if str(self._tariff).lower() in [SINGLE_TARIFF, TOTAL_TARIFF]: - self._attr_native_value = Decimal(self._calibrate_value) - else: - self._attr_native_value = Decimal(0) + self._attr_calculated_current_value = Decimal(self._calibrate_calc_value) + self._attr_native_value = Decimal(self._calibrate_value) self.async_write_ha_state() async def async_calibrate(self, value): @@ -951,7 +947,10 @@ async def async_added_to_hass(self) -> None: @callback def async_source_tracking(event): """Wait for source to be ready, then start meter.""" - if self._tariff_entity is not None: + if str(self._tariff).lower() != TOTAL_TARIFF and self._tariff_entity is not None: + # If the tariff is not TOTAL_TARIFF, we need to track the tariff entity + # and change the status of the utility meter sensor accordingly + #if self._tariff_entity is not None: _LOGGER.debug( "<%s> tracks utility meter %s", self.name, self._tariff_entity ) @@ -1246,7 +1245,7 @@ def _async_attribute_sensor_state_listener( _LOGGER.debug("state update step 1") _LOGGER.debug("state update step 1 %s", self._attribute) _LOGGER.debug("state update step 1a %s", new_state.state) - _LOGGER.debug("state update step 1a %s", new_state.attributes) + _LOGGER.debug("state update step 1b %s", new_state.attributes) self._has_logged = False if self._attribute in new_state.attributes: From 7971e5dd87c98336a08abb771e04348c72e700f5 Mon Sep 17 00:00:00 2001 From: Chris Abberley <26394346+cabberley@users.noreply.github.com> Date: Sun, 13 Jul 2025 14:49:30 +1000 Subject: [PATCH 10/11] Changes to be committed: modified: custom_components/utility_meter_next_gen/sensor.py --- custom_components/utility_meter_next_gen/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/utility_meter_next_gen/sensor.py b/custom_components/utility_meter_next_gen/sensor.py index c192291..08635fd 100644 --- a/custom_components/utility_meter_next_gen/sensor.py +++ b/custom_components/utility_meter_next_gen/sensor.py @@ -171,7 +171,7 @@ async def async_setup_entry( calc_sensors = [] tariffs = config_entry.options[CONF_TARIFFS] - if meter_type is not None and not isinstance(meter_type, str) and len(meter_type) > 1: # noqa: PLR5501 + if meter_type is not None and not isinstance(meter_type, str) and len(meter_type) > 1: for meter in meter_type: if not tariffs: # Add single sensor, not gated by a tariff selector From 21ce520ea5d647fc8d03ef65cafc362d1ec9ca46 Mon Sep 17 00:00:00 2001 From: Chris Abberley <26394346+cabberley@users.noreply.github.com> Date: Tue, 15 Jul 2025 05:23:42 +1000 Subject: [PATCH 11/11] Changes to be committed: modified: README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df81910..4039937 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

- Current version GitHub Release Date + Current version GitHub Release Date GitHub GitHub Actions Workflow Status GitHub Issues or Pull Requests GitHub User's stars GitHub Downloads (all assets, all releases)