Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to forcefully create riemann sensor for powercalc group #2169

Merged
merged 1 commit into from
Mar 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions custom_components/powercalc/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
CONF_FIXED = "fixed"
CONF_FORCE_UPDATE_FREQUENCY = "force_update_frequency"
CONF_FORCE_ENERGY_SENSOR_CREATION = "force_energy_sensor_creation"
CONF_FORCE_CALCULATE_GROUP_ENERGY = "force_calculate_energy"
CONF_GROUP = "group"
CONF_GROUP_POWER_ENTITIES = "group_power_entities"
CONF_GROUP_ENERGY_ENTITIES = "group_energy_entities"
Expand Down
5 changes: 3 additions & 2 deletions custom_components/powercalc/device_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,17 @@
)


def get_device_info(hass: HomeAssistant, sensor_config: ConfigType, source_entity: SourceEntity) -> DeviceInfo | None:
def get_device_info(hass: HomeAssistant, sensor_config: ConfigType, source_entity: SourceEntity | None) -> DeviceInfo | None:

Check notice on line 57 in custom_components/powercalc/device_binding.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

PEP 8 coding style violation

PEP 8: E501 line too long (125 \> 120 characters)
"""
Get device info for a given powercalc entity configuration.
Prefer user configured device, when it is not set fallback to the same device as the source entity
"""
device_id = sensor_config.get(CONF_DEVICE)
device = None
if device_id is not None:
device_reg = device_registry.async_get(hass)
device = device_reg.async_get(device_id)
else:
elif source_entity:
device = source_entity.device_entry

if device is None:
Expand Down
2 changes: 2 additions & 0 deletions custom_components/powercalc/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
CONF_ENERGY_SENSOR_UNIT_PREFIX,
CONF_FILTER,
CONF_FIXED,
CONF_FORCE_CALCULATE_GROUP_ENERGY,
CONF_FORCE_ENERGY_SENSOR_CREATION,
CONF_GROUP,
CONF_HIDE_MEMBERS,
Expand Down Expand Up @@ -177,6 +178,7 @@
vol.Optional(CONF_CUSTOM_MODEL_DIRECTORY): cv.string,
vol.Optional(CONF_POWER_SENSOR_ID): cv.entity_id,
vol.Optional(CONF_FORCE_ENERGY_SENSOR_CREATION): cv.boolean,
vol.Optional(CONF_FORCE_CALCULATE_GROUP_ENERGY): cv.boolean,
vol.Optional(CONF_FIXED): FIXED_SCHEMA,
vol.Optional(CONF_LINEAR): LINEAR_SCHEMA,
vol.Optional(CONF_WLED): WLED_SCHEMA,
Expand Down
34 changes: 17 additions & 17 deletions custom_components/powercalc/sensors/energy.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@ async def create_energy_sensor(
entity_id=entity_id,
entity_category=entity_category,
name=name,
round_digits=sensor_config.get(CONF_ENERGY_SENSOR_PRECISION), # type: ignore
unit_prefix=unit_prefix,
unit_time=UnitOfTime.HOURS,
integration_method=sensor_config.get(CONF_ENERGY_INTEGRATION_METHOD)
or DEFAULT_ENERGY_INTEGRATION_METHOD,
powercalc_source_entity=source_entity.entity_id,
powercalc_source_domain=source_entity.domain,
sensor_config=sensor_config,
Expand Down Expand Up @@ -205,25 +201,26 @@ class VirtualEnergySensor(IntegrationSensor, EnergySensor):
def __init__(
self,
source_entity: str,
unique_id: str | None,
entity_id: str,
entity_category: EntityCategory | None,
name: str | None,
round_digits: int,
unit_prefix: str | None,
unit_time: UnitOfTime,
integration_method: str,
powercalc_source_entity: str,
powercalc_source_domain: str,
sensor_config: ConfigType,
device_info: DeviceInfo | None,
powercalc_source_entity: str | None = None,
powercalc_source_domain: str | None = None,
unique_id: str | None = None,
entity_category: EntityCategory | None = None,
name: str | None = None,
unit_prefix: str | None = None,
device_info: DeviceInfo | None = None,
) -> None:

round_digits: int = sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, 2)
integration_method: str = sensor_config.get(CONF_ENERGY_INTEGRATION_METHOD, DEFAULT_ENERGY_INTEGRATION_METHOD)

super().__init__(
source_entity=source_entity,
name=name,
round_digits=round_digits,
unit_prefix=unit_prefix,
unit_time=unit_time,
unit_time=UnitOfTime.HOURS,
integration_method=integration_method,
unique_id=unique_id,
device_info=device_info,
Expand All @@ -243,9 +240,12 @@ def extra_state_attributes(self) -> dict[str, str] | None:
if self._sensor_config.get(CONF_DISABLE_EXTENDED_ATTRIBUTES):
return super().extra_state_attributes

if self._powercalc_source_entity is None:
return None

attrs = {
ATTR_SOURCE_ENTITY: self._powercalc_source_entity,
ATTR_SOURCE_DOMAIN: self._powercalc_source_domain,
ATTR_SOURCE_ENTITY: self._powercalc_source_entity or "",
ATTR_SOURCE_DOMAIN: self._powercalc_source_domain or "",
}
super_attrs = super().extra_state_attributes
if super_attrs:
Expand Down
36 changes: 26 additions & 10 deletions custom_components/powercalc/sensors/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
CONF_DISABLE_EXTENDED_ATTRIBUTES,
CONF_ENERGY_SENSOR_PRECISION,
CONF_ENERGY_SENSOR_UNIT_PREFIX,
CONF_FORCE_CALCULATE_GROUP_ENERGY,
CONF_GROUP,
CONF_GROUP_ENERGY_ENTITIES,
CONF_GROUP_MEMBER_SENSORS,
Expand All @@ -78,6 +79,7 @@
SensorType,
UnitPrefix,
)
from custom_components.powercalc.device_binding import get_device_info
from custom_components.powercalc.group_include.include import resolve_include_entities

from .abstract import (
Expand All @@ -87,7 +89,7 @@
generate_power_sensor_entity_id,
generate_power_sensor_name,
)
from .energy import EnergySensor
from .energy import EnergySensor, VirtualEnergySensor
from .power import PowerSensor
from .utility_meter import create_utility_meters

Expand Down Expand Up @@ -171,24 +173,26 @@ async def create_group_sensors(

group_sensors: list[Entity] = []

power_sensor = None
if power_sensor_ids:
group_sensors.append(
create_grouped_power_sensor(
hass,
group_name,
sensor_config,
set(power_sensor_ids),
),
power_sensor = create_grouped_power_sensor(
hass,
group_name,
sensor_config,
set(power_sensor_ids),
)
group_sensors.append(power_sensor)

create_energy_sensor: bool = sensor_config.get(CONF_CREATE_ENERGY_SENSOR, True)
if energy_sensor_ids and create_energy_sensor:
if create_energy_sensor:
energy_sensor = create_grouped_energy_sensor(
hass,
group_name,
sensor_config,
set(energy_sensor_ids),
power_sensor,
)

group_sensors.append(energy_sensor)

group_sensors.extend(
Expand Down Expand Up @@ -423,7 +427,8 @@ def create_grouped_energy_sensor(
group_name: str,
sensor_config: dict,
energy_sensor_ids: set[str],
) -> GroupedEnergySensor:
power_sensor: GroupedPowerSensor | None,
) -> EnergySensor:
name = generate_energy_sensor_name(sensor_config, group_name)
unique_id = sensor_config.get(CONF_UNIQUE_ID)
energy_unique_id = None
Expand All @@ -438,6 +443,17 @@ def create_grouped_energy_sensor(

_LOGGER.debug("Creating grouped energy sensor: %s (entity_id=%s)", name, entity_id)

force_calculate_energy = bool(sensor_config.get(CONF_FORCE_CALCULATE_GROUP_ENERGY, False))
if power_sensor and (force_calculate_energy or not energy_sensor_ids):
return VirtualEnergySensor(
source_entity=power_sensor.entity_id,
entity_id=entity_id,
name=name,
unique_id=energy_unique_id,
sensor_config=sensor_config,
device_info=get_device_info(hass, sensor_config, None),
)

return GroupedEnergySensor(
hass=hass,
name=name,
Expand Down
2 changes: 2 additions & 0 deletions docs/source/configuration/sensor-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Not all configuration params listed below are available in the GUI, when you wan
+-------------------------------------------+-----------+--------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----+
| force_energy_sensor_creation | boolean | **Optional** | Set this to ``true`` when you want a new energy sensor to be created for the power sensor with ``power_sensor_id``, even if the device already has an energy sensor entity of its own. | |
+-------------------------------------------+-----------+--------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----+
| force_calculate_group_energy | boolean | **Optional** | Set this to ``true`` when you want to create a Riemann sum sensor for powercalc group. By default the group energy sensor sums all individual energy sensors from member entities. | |
+-------------------------------------------+-----------+--------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----+
| energy_sensor_id | string | **Optional** | Entity id of an existing energy sensor. Mostly used in conjunction with ``power_sensor_id``. | |
+-------------------------------------------+-----------+--------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----+
| ignore_unavailable_state | boolean | **Optional** | Set this to ``true`` when you want the power sensor to display a value (``unavailable_power``, ``standby_power`` or 0) regardless of whether the source entity is available. The can be useful for example on a TV which state can become unavailable when it is set to off. | X |
Expand Down
33 changes: 32 additions & 1 deletion docs/source/sensor-types/group.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,35 @@ Automatically include entities
Powercalc has some options to automatically include entities in your group matching certain criteria.
This can be useful to you don't have to manually specify each and every sensor.

See :doc:`group/include-entities` for more information.
See :doc:`group/include-entities` for more information.

Force creating Riemann sum sensor
---------------------------------

By default the group energy sensor created by Powercalc is a simple sum of the energy sensors of the individual entities.
When you have ``create_energy_sensor: false`` for the individual entities, the group energy sensor will not be created.
``force_calculate_group_energy`` can be used to force the creation of a Riemann sum sensor for the group. This will take the group power sensor as the source and integrate it over time.

For example:

.. code-block:: yaml

powercalc:
sensors:
- create_group: all lights
create_energy_sensor: true
force_calculate_group_energy: true
entities:
- create_group: living lights
create_energy_sensor: false
entities:
- entity_id: light.tv_lamp
- entity_id: light.reading_light
- ...

This way you can still create an energy sensor even when the individual entities don't have one.
When you are not interested in the individual energy sensors of each light this could be a good solution.

.. important::
Beware that if your group also consists of :doc:`daily-energy` sensors the Riemann sum sensor will not be accurate, as it's could be missing the data of this sensor, because it does not always have a power sensor.
So you only must use this option when the group power sensor contains all the power data from individual entities.
4 changes: 0 additions & 4 deletions tests/sensors/test_energy.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
EntityCategory,
UnitOfEnergy,
UnitOfPower,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
Expand Down Expand Up @@ -324,12 +323,9 @@ async def test_set_entity_category(hass: HomeAssistant) -> None:
source_entity="sensor.test_power",
entity_id="sensor.test_energy",
name="Test energy",
round_digits=2,
unit_prefix="k",
unit_time=UnitOfTime(UnitOfTime.HOURS),
unique_id="1234",
entity_category=EntityCategory(EntityCategory.DIAGNOSTIC),
integration_method="",
powercalc_source_entity="light.test",
powercalc_source_domain="light",
sensor_config={},
Expand Down
53 changes: 53 additions & 0 deletions tests/sensors/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
CONF_ENERGY_SENSOR_NAMING,
CONF_ENERGY_SENSOR_UNIT_PREFIX,
CONF_FIXED,
CONF_FORCE_CALCULATE_GROUP_ENERGY,
CONF_GROUP,
CONF_GROUP_ENERGY_ENTITIES,
CONF_GROUP_MEMBER_SENSORS,
Expand Down Expand Up @@ -1480,6 +1481,58 @@ async def test_additional_energy_sensors(hass: HomeAssistant) -> None:
assert energy_state.attributes.get(ATTR_ENTITIES) == {"sensor.ceiling_fan_energy", "sensor.furnace_energy"}


async def test_force_calculate_energy_sensor(hass: HomeAssistant) -> None:
"""
When `force_calculate_group_energy` is set to true,
the energy sensor should be a Riemann sensor integrating the power sensor
"""

mock_registry(
hass,
{
"sensor.furnace_power": RegistryEntry(
entity_id="sensor.furnace_power",
unique_id="1111",
platform="sensor",
device_class=SensorDeviceClass.POWER,
),
"sensor.lights_power": RegistryEntry(
entity_id="sensor.lights_power",
unique_id="2222",
platform="sensor",
device_class=SensorDeviceClass.POWER,
),
},
)

await run_powercalc_setup(
hass,
[
{
CONF_CREATE_GROUP: "TestGroup",
CONF_IGNORE_UNAVAILABLE_STATE: True,
CONF_CREATE_ENERGY_SENSOR: True,
CONF_FORCE_CALCULATE_GROUP_ENERGY: True,
CONF_ENTITIES: [
{
CONF_POWER_SENSOR_ID: "sensor.furnace_power",
},
{
CONF_POWER_SENSOR_ID: "sensor.lights_power",
},
],
},
],
{
CONF_CREATE_ENERGY_SENSORS: False,
},
)

energy_state = hass.states.get("sensor.testgroup_energy")
assert energy_state
assert energy_state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY


async def _create_energy_group(
hass: HomeAssistant,
name: str,
Expand Down