diff --git a/README.md b/README.md index 15f1ff2..4039937 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@
-
+
@@ -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.
-[](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.**
+[](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..95d9f72 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:
+ 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,100 @@ 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 (
+ (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 (
+ (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
+ 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.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
+ # 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 +293,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 +323,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..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.6",
+ "version": "2025.7.12",
"zeroconf": []
}
diff --git a/custom_components/utility_meter_next_gen/schemas.py b/custom_components/utility_meter_next_gen/schemas.py
index 22c7ab5..3cc494e 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."""
@@ -206,15 +336,13 @@ 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",
),
),
}
- #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.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.get(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..08635fd 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.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]
@@ -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:
+ 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: # noqa: PLR5501
+ 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)
@@ -364,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."""
@@ -376,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
@@ -398,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
@@ -410,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,
)
@@ -419,16 +591,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 +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,
@@ -466,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:
@@ -485,7 +659,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 +673,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,8 +684,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_last_value = Decimal(0)
+ self._attr_calculated_current_value = Decimal(
+ self._calibrate_calc_value
+ )
self.async_write_ha_state()
@staticmethod
@@ -568,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:
@@ -587,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
@@ -603,25 +779,27 @@ 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 (
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:
- 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((
- 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(adjustment)
+ * Decimal(self._attr_multiplier)
+ ),
+ PRECISION,
+ )
except (DecimalException, InvalidOperation) as err:
_LOGGER.error(
"Error while parsing value %s from sensor %s: %s",
@@ -646,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
)
@@ -707,19 +885,14 @@ 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:
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
- 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):
@@ -745,9 +918,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
@@ -758,11 +933,24 @@ 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):
"""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
)
@@ -849,23 +1037,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
@@ -879,6 +1074,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:
@@ -890,24 +1087,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 +1124,21 @@ 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_suggested_display_precision = PRECISION
+ 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 +1152,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 +1176,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 +1186,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 +1210,12 @@ 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_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")
@@ -1020,7 +1224,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
@@ -1039,9 +1243,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 1b %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..07ef44a 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,46 @@
"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.",
+ "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 +125,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 +144,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 +159,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 +185,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 +200,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..07ef44a 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,46 @@
"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.",
+ "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 +125,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 +136,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 +144,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 +159,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 +185,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 +200,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/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