Skip to content

Commit

Permalink
Also include manually config entries in include group (#1746)
Browse files Browse the repository at this point in the history
  • Loading branch information
bramstroker authored Jul 8, 2023
1 parent 4c63894 commit ea73ce7
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 56 deletions.
4 changes: 3 additions & 1 deletion custom_components/powercalc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
CONF_FIXED,
CONF_FORCE_UPDATE_FREQUENCY,
CONF_IGNORE_UNAVAILABLE_STATE,
CONF_INCLUDE,
CONF_POWER,
CONF_POWER_SENSOR_CATEGORY,
CONF_POWER_SENSOR_FRIENDLY_NAMING,
Expand Down Expand Up @@ -221,7 +222,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await discovery_manager.start_discovery()

sensors: list = domain_config.get(CONF_SENSORS, [])
for sensor_config in sensors:
sorted_sensors = sorted(sensors, key=lambda x: (CONF_INCLUDE in x, x.get(CONF_INCLUDE, False)))
for sensor_config in sorted_sensors:
sensor_config.update({DISCOVERY_TYPE: PowercalcDiscoveryType.USER_YAML})
hass.async_create_task(
async_load_platform(
Expand Down
43 changes: 6 additions & 37 deletions custom_components/powercalc/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,12 @@
SensorType,
UnitPrefix,
)
from .discovery import autodiscover_model
from .errors import (
ModelNotSupportedError,
PowercalcSetupError,
SensorAlreadyConfiguredError,
SensorConfigurationError,
)
from .group_include.include import resolve_include_entities
from .power_profile.factory import get_power_profile
from .sensors.abstract import BaseEntity
from .sensors.daily_energy import (
DAILY_FIXED_ENERGY_SCHEMA,
Expand Down Expand Up @@ -544,7 +541,7 @@ def convert_config_entry_to_sensor_config(config_entry: ConfigEntry) -> ConfigTy
return sensor_config


async def create_sensors(
async def create_sensors( # noqa: C901
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
Expand Down Expand Up @@ -603,13 +600,11 @@ async def create_sensors(

# Automatically add a bunch of entities by area or evaluating template
if CONF_INCLUDE in config:
entities = resolve_include_entities(hass, config.get(CONF_INCLUDE)) # type: ignore
_LOGGER.debug("Found include entities: %s", entities)
sensor_configs = {
entity.entity_id: {CONF_ENTITY_ID: entity.entity_id}
for entity in entities
if await is_auto_configurable(hass, entity)
} | sensor_configs
include_entities = resolve_include_entities(hass, config.get(CONF_INCLUDE)) # type: ignore
_LOGGER.debug("Found include entities: %s", include_entities)
for source_entity in include_entities:
if source_entity.entity_id in hass.data[DOMAIN][DATA_CONFIGURED_ENTITIES]:
entities_to_add.existing.extend(hass.data[DOMAIN][DATA_CONFIGURED_ENTITIES][source_entity.entity_id])

# Create sensors for each entity
for sensor_config in sensor_configs.values():
Expand Down Expand Up @@ -832,32 +827,6 @@ async def check_entity_not_already_configured(
raise SensorAlreadyConfiguredError(source_entity.entity_id, existing_entities)


async def is_auto_configurable(
hass: HomeAssistant,
entity_entry: er.RegistryEntry,
sensor_config: ConfigType | None = None,
) -> bool:
try:
model_info = await autodiscover_model(hass, entity_entry)
if not model_info:
return False
power_profile = await get_power_profile(
hass,
sensor_config or {},
model_info=model_info,
)
if not power_profile:
return False
source_entity = await create_source_entity(entity_entry.entity_id, hass)
if not power_profile.is_entity_domain_supported(source_entity):
return False
if power_profile.has_sub_profiles and power_profile.sub_profile:
return True
return not power_profile.is_additional_configuration_required
except ModelNotSupportedError:
return False


@dataclass
class EntitiesBucket:
new: list[Entity] = field(default_factory=list)
Expand Down
29 changes: 29 additions & 0 deletions docs/source/strategies/composite.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
=========
Composite
=========

The composite strategy allows you to compose the power calculation of multiple strategies.
So you could use the ``fixed`` strategy when a certain condition applies, and the ``linear`` when another condition applies.

Currently this is a YAML only feature

Configuration options
---------------------

+---------------+-------+--------------+----------+------------------------------------+
| Name | Type | Requirement | Default | Description |
+===============+=======+==============+==========+====================================+
| strategies | list | **Required** | | List of objects with strategy configuration and condition | |
+---------------+-------+--------------+----------+------------------------------------+

**Example**

.. code-block:: yaml
powercalc:
sensors:
- entity_id: light.test
composite:
strategies:
- condition:
94 changes: 91 additions & 3 deletions tests/group_include/test_include.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import logging
import uuid

import pytest
from homeassistant.components import light
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_ENTITY_ID, STATE_OFF
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_ENTITY_ID, CONF_UNIQUE_ID, STATE_OFF
from homeassistant.core import HomeAssistant
from homeassistant.helpers.area_registry import AreaRegistry
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntry
from homeassistant.setup import async_setup_component
from pytest_homeassistant_custom_component.common import (
MockConfigEntry,
mock_device_registry,
mock_registry,
)
Expand All @@ -18,9 +20,14 @@
CONF_AREA,
CONF_CREATE_GROUP,
CONF_FILTER,
CONF_FIXED,
CONF_GROUP,
CONF_INCLUDE,
CONF_POWER,
CONF_SENSOR_TYPE,
CONF_TEMPLATE,
DOMAIN,
SensorType,
)
from custom_components.test.light import MockLight
from tests.common import (
Expand All @@ -47,9 +54,9 @@ async def test_include_area(
await create_mock_light_entity(hass, create_discoverable_light("bathroom_mirror"))

area = area_reg.async_get_or_create("Bathroom 1")
await hass.async_block_till_done()
entity_reg.async_update_entity("light.bathroom_mirror", area_id=area.id)
await hass.async_block_till_done()

_create_powercalc_config_entry(hass, "light.bathroom_mirror")

await run_powercalc_setup(
hass,
Expand Down Expand Up @@ -78,6 +85,8 @@ async def test_include_area_not_found(

async def test_include_light_group(hass: HomeAssistant) -> None:
discoverable_light = create_discoverable_light("bathroom_mirror")
_create_powercalc_config_entry(hass, "light.bathroom_mirror")

non_discoverable_light = MockLight("bathroom_spots")

await create_mock_light_entity(hass, [discoverable_light, non_discoverable_light])
Expand Down Expand Up @@ -126,6 +135,9 @@ async def test_include_domain(hass: HomeAssistant) -> None:
],
)

_create_powercalc_config_entry(hass, "light.bathroom_spots")
_create_powercalc_config_entry(hass, "light.kitchen")

await run_powercalc_setup(
hass,
[
Expand Down Expand Up @@ -156,6 +168,9 @@ async def test_include_template(hass: HomeAssistant) -> None:
],
)

_create_powercalc_config_entry(hass, "light.bathroom_spots")
_create_powercalc_config_entry(hass, "light.kitchen")

template = "{{ states|selectattr('entity_id', 'eq', 'light.bathroom_spots')|map(attribute='entity_id')|list}}"
await run_powercalc_setup(
hass,
Expand Down Expand Up @@ -187,6 +202,10 @@ async def test_combine_include_with_entities(hass: HomeAssistant) -> None:
[light_a, light_b, light_c, light_d, light_e, light_f],
)

_create_powercalc_config_entry(hass, "light.light_a")
_create_powercalc_config_entry(hass, "light.light_e")
_create_powercalc_config_entry(hass, "light.light_f")

# Ugly hack, maybe I can figure out something better in the future.
# Light domain is already setup for platform test, remove the component so we can setup light group
if light.DOMAIN in hass.config.components:
Expand Down Expand Up @@ -302,6 +321,9 @@ async def test_include_filter_domain(
},
)

_create_powercalc_config_entry(hass, "light.test_light")
_create_powercalc_config_entry(hass, "switch.test_switch")

await run_powercalc_setup(
hass,
{
Expand All @@ -319,3 +341,69 @@ async def test_include_filter_domain(
group_state = hass.states.get("sensor.test_include_power")
assert group_state
assert group_state.attributes.get(ATTR_ENTITIES) == {"sensor.test_light_power"}


async def test_include_yaml_configured_entity(hass: HomeAssistant, entity_reg: EntityRegistry,
area_reg: AreaRegistry) -> None:
"""Test that include also includes entities that the user configured with YAML"""

light_a = MockLight("light_a")
light_b = MockLight("light_b")
light_c = create_discoverable_light("light_c")
light_d = MockLight("light_d")
await create_mock_light_entity(
hass,
[light_a, light_b, light_c, light_d],
)

area = area_reg.async_get_or_create("My area")
entity_reg.async_update_entity(light_a.entity_id, area_id=area.id)
entity_reg.async_update_entity(light_b.entity_id, area_id=area.id)
entity_reg.async_update_entity(light_c.entity_id, area_id=area.id)

_create_powercalc_config_entry(hass, light_a.entity_id)

await run_powercalc_setup(
hass,
[
{
CONF_CREATE_GROUP: "Test include",
CONF_INCLUDE: {
CONF_AREA: "my_area",
},
},
{
CONF_ENTITY_ID: light_b.entity_id,
CONF_FIXED: {
CONF_POWER: 50,
},
},
{
CONF_ENTITY_ID: light_c.entity_id,
},
],
)

group_state = hass.states.get("sensor.test_include_power")
assert group_state
assert group_state.attributes.get(ATTR_ENTITIES) == {
"sensor.light_a_power",
"sensor.light_b_power",
"sensor.light_c_power",
}


def _create_powercalc_config_entry(hass: HomeAssistant, source_entity_id: str) -> MockConfigEntry:
unique_id = str(uuid.uuid4())
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_SENSOR_TYPE: SensorType.VIRTUAL_POWER,
CONF_UNIQUE_ID: unique_id,
CONF_ENTITY_ID: source_entity_id,
CONF_FIXED: {CONF_POWER: 50},
},
unique_id=unique_id,
)
entry.add_to_hass(hass)
return entry
15 changes: 0 additions & 15 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
CalculationStrategy,
SensorType,
)
from custom_components.powercalc.sensor import is_auto_configurable

from .common import (
create_input_boolean,
Expand Down Expand Up @@ -720,20 +719,6 @@ async def test_sensors_with_errors_are_skipped_for_multiple_entity_setup(
assert "Skipping sensor setup" in caplog.text


async def test_is_autoconfigurable_returns_false(
hass: HomeAssistant,
mock_entity_with_model_information: MockEntityWithModel,
) -> None:
"""
is_autoconfigurable should return False when the manufacturer / model is not found in the library
"""
mock_entity_with_model_information("light.testa", "Foo", "Bar")

entity_reg = er.async_get(hass)
entity_entry = entity_reg.async_get("light.testa")
assert not await is_auto_configurable(hass, entity_entry)


async def test_create_config_entry_without_energy_sensor(
hass: HomeAssistant,
) -> None:
Expand Down

0 comments on commit ea73ce7

Please sign in to comment.