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

Implement auto populate multi switch entities #2876

Merged
merged 2 commits into from
Jan 2, 2025
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

# <img src="https://docs.powercalc.nl/img/logo2_light.svg" width="400">

**PowerCalc** is a versatile custom component for Home Assistant that estimates power consumption for devices like lights, fans, smart speakers, and more—especially those without built-in power meters.
**PowerCalc** is a versatile custom component for Home Assistant that estimates power consumption for devices like lights, fans, smart speakers, and more—especially those without built-in power meters.
It acts as a virtual energy monitor, using advanced strategies to calculate power usage. For light entities, PowerCalc analyzes factors such as brightness, hue, saturation, and color temperature to deliver accurate consumption estimates. For other devices, it offers extensive configuration possibilities.

Additionally, PowerCalc includes a powerful measurement utility, enabling users to assess their devices' power usage and contribute custom power profiles to the growing PowerCalc library.
Expand Down
114 changes: 78 additions & 36 deletions custom_components/powercalc/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import selector
from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
Expand Down Expand Up @@ -198,15 +199,15 @@
LIBRARY_URL = "https://library.powercalc.nl"
UNIQUE_ID_TRACKED_UNTRACKED = "pc_tracked_untracked"

STRATEGY_STEP_MAPPING = {
STRATEGY_STEP_MAPPING: dict[CalculationStrategy, Step] = {
CalculationStrategy.FIXED: Step.FIXED,
CalculationStrategy.LINEAR: Step.LINEAR,
CalculationStrategy.MULTI_SWITCH: Step.MULTI_SWITCH,
CalculationStrategy.PLAYBOOK: Step.PLAYBOOK,
CalculationStrategy.WLED: Step.WLED,
}

GROUP_STEP_MAPPING = {
GROUP_STEP_MAPPING: dict[GroupType, Step] = {
GroupType.CUSTOM: Step.GROUP_CUSTOM,
GroupType.DOMAIN: Step.GROUP_DOMAIN,
GroupType.STANDBY: Step.GROUP_DOMAIN,
Expand Down Expand Up @@ -366,16 +367,8 @@
},
)

SCHEMA_POWER_MULTI_SWITCH = vol.Schema(
{
vol.Required(CONF_ENTITIES): selector.EntitySelector(
selector.EntitySelectorConfig(domain=Platform.SWITCH, multiple=True),
),
},
)
SCHEMA_POWER_MULTI_SWITCH_MANUAL = vol.Schema(
{
**SCHEMA_POWER_MULTI_SWITCH.schema,
vol.Required(CONF_POWER): vol.Coerce(float),
vol.Required(CONF_POWER_OFF): vol.Coerce(float),
},
Expand Down Expand Up @@ -552,13 +545,19 @@
},
)

GROUP_SCHEMAS = {
GROUP_SCHEMAS: dict[GroupType, vol.Schema] = {
GroupType.CUSTOM: SCHEMA_GROUP,
GroupType.DOMAIN: SCHEMA_GROUP_DOMAIN,
GroupType.SUBTRACT: SCHEMA_GROUP_SUBTRACT,
GroupType.TRACKED_UNTRACKED: SCHEMA_GROUP_TRACKED_UNTRACKED,
}

STRATEGY_SCHEMAS: dict[CalculationStrategy, vol.Schema] = {
CalculationStrategy.FIXED: SCHEMA_POWER_FIXED,
CalculationStrategy.PLAYBOOK: SCHEMA_POWER_PLAYBOOK,
CalculationStrategy.WLED: SCHEMA_POWER_WLED,
}


@dataclass(slots=True)
class PowercalcFormStep:
Expand Down Expand Up @@ -590,6 +589,7 @@
self.is_library_flow: bool = False
self.skip_advanced_step: bool = False
self.is_options_flow: bool = isinstance(self, OptionsFlow)
self.strategy: CalculationStrategy | None = None
self.name: str | None = None
super().__init__()

Expand Down Expand Up @@ -625,26 +625,72 @@
if not any(key in (user_input or {}) for key in required_keys):
raise SchemaFlowError("group_mandatory")

def create_strategy_schema(self, strategy: CalculationStrategy, source_entity_id: str) -> vol.Schema:
def create_strategy_schema(self) -> vol.Schema:
"""Get the config schema for a given power calculation strategy."""
if strategy == CalculationStrategy.LINEAR:
return SCHEMA_POWER_LINEAR.extend( # type: ignore
if not self.strategy:
raise ValueError("No strategy selected") # pragma: no cover

if hasattr(self, f"create_schema_{self.strategy.value.lower()}"):
return getattr(self, f"create_schema_{self.strategy.value.lower()}")() # type: ignore

return STRATEGY_SCHEMAS[self.strategy]

def create_schema_linear(self) -> vol.Schema:
"""Create the config schema for linear strategy."""
return SCHEMA_POWER_LINEAR.extend( # type: ignore
{
vol.Optional(CONF_ATTRIBUTE): selector.AttributeSelector(
selector.AttributeSelectorConfig(
entity_id=self.source_entity_id, # type: ignore
hide_attributes=[],
),
),
},
)

def create_schema_multi_switch(self) -> vol.Schema:
"""Create the config schema for multi switch strategy."""

entity_registry = er.async_get(self.hass)
entities: list[str] = []
if self.source_entity and self.source_entity.device_entry:
entities = [
entity.entity_id
for entity in entity_registry.entities.get_entries_for_device_id(self.source_entity.device_entry.id)
if entity.domain == Platform.SWITCH
]

# pre-populate the entity selector with the switches from the device
if entities:
schema = vol.Schema(
{
vol.Required(
CONF_ENTITIES,
description={"suggested_value": entities},
): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=Platform.SWITCH,
multiple=True,
include_entities=entities,
),
),
},
)
else:
schema = vol.Schema(
{
vol.Optional(CONF_ATTRIBUTE): selector.AttributeSelector(
selector.AttributeSelectorConfig(
entity_id=source_entity_id,
hide_attributes=[],
vol.Required(CONF_ENTITIES): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=Platform.SWITCH,
multiple=True,
),
),
},
)
if strategy == CalculationStrategy.PLAYBOOK:
return SCHEMA_POWER_PLAYBOOK
if strategy == CalculationStrategy.MULTI_SWITCH:
return SCHEMA_POWER_MULTI_SWITCH if self.is_library_flow else SCHEMA_POWER_MULTI_SWITCH_MANUAL
if strategy == CalculationStrategy.WLED:
return SCHEMA_POWER_WLED
return SCHEMA_POWER_FIXED
if not self.is_library_flow:
schema = schema.extend(SCHEMA_POWER_MULTI_SWITCH_MANUAL.schema)

return schema

def create_schema_group_custom(
self,
Expand Down Expand Up @@ -772,12 +818,10 @@

def build_strategy_config(
self,
strategy: CalculationStrategy,
source_entity_id: str,
user_input: dict[str, Any],
) -> dict[str, Any]:
"""Build the config dict needed for the configured strategy."""
strategy_schema = self.create_strategy_schema(strategy, source_entity_id)
strategy_schema = self.create_strategy_schema()
strategy_options: dict[str, Any] = {}
for key in strategy_schema.schema:
if user_input.get(key) is None:
Expand Down Expand Up @@ -1120,13 +1164,15 @@
user_input: dict[str, Any] | None = None,
validate: Callable[[dict[str, Any]], None] | None = None,
) -> FlowResult:
self.strategy = strategy

async def _validate(user_input: dict[str, Any]) -> dict[str, Any]:
if validate:
validate(user_input)
await self.validate_strategy_config({strategy: user_input})
return {strategy: user_input}

schema = self.create_strategy_schema(strategy, self.source_entity_id) # type: ignore
schema = self.create_strategy_schema()

return await self.handle_form_step(
PowercalcFormStep(
Expand Down Expand Up @@ -1634,7 +1680,7 @@
self.sensor_config = dict(config_entry.data)
self.sensor_type: SensorType = self.sensor_config.get(CONF_SENSOR_TYPE) or SensorType.VIRTUAL_POWER
self.source_entity_id: str = self.sensor_config.get(CONF_ENTITY_ID) # type: ignore
self.strategy: CalculationStrategy | None = self.sensor_config.get(CONF_MODE)
self.strategy = self.sensor_config.get(CONF_MODE)

async def async_step_init(
self,
Expand Down Expand Up @@ -1693,7 +1739,7 @@
menu = [Step.BASIC_OPTIONS]
if self.sensor_type == SensorType.VIRTUAL_POWER:
if self.strategy and self.strategy != CalculationStrategy.LUT:
strategy_step = STRATEGY_STEP_MAPPING[self.strategy]

Check warning on line 1742 in custom_components/powercalc/config_flow.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect type

Expected type 'CalculationStrategy' (matched generic type '_KT'), got 'str \| None' instead
menu.append(strategy_step)
if self.selected_profile:
menu.append(Step.LIBRARY_OPTIONS)
Expand Down Expand Up @@ -1854,9 +1900,9 @@
if not self.strategy:
return self.async_abort(reason="no_strategy_selected") # pragma: no cover

step = STRATEGY_STEP_MAPPING.get(self.strategy, Step.FIXED)

Check warning on line 1903 in custom_components/powercalc/config_flow.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect type

Expected type 'CalculationStrategy' (matched generic type '_KT'), got 'str' instead

schema = self.create_strategy_schema(self.strategy, self.source_entity_id)
schema = self.create_strategy_schema()
if self.selected_profile and self.selected_profile.device_type == DeviceType.SMART_SWITCH:
schema = SCHEMA_POWER_SMART_SWITCH

Expand Down Expand Up @@ -1905,11 +1951,7 @@
self._process_user_input(user_input, SCHEMA_POWER_SMART_SWITCH)
user_input = {CONF_POWER: user_input.get(CONF_POWER, 0)}

strategy_options = self.build_strategy_config(
self.strategy,
self.source_entity_id,
user_input or {},
)
strategy_options = self.build_strategy_config(user_input or {})

if self.strategy != CalculationStrategy.LUT:
self.sensor_config.update({str(self.strategy): strategy_options})
Expand Down
4 changes: 3 additions & 1 deletion custom_components/powercalc/sensors/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
from custom_components.powercalc.helpers import evaluate_power
from custom_components.powercalc.power_profile.factory import get_power_profile
from custom_components.powercalc.power_profile.power_profile import (
DiscoveryBy,
PowerProfile,
SubProfileSelectConfig,
SubProfileSelector,
Expand Down Expand Up @@ -505,7 +506,8 @@ async def _handle_source_entity_state_change(
self._sleep_power_timer()
self._sleep_power_timer = None

if self.source_entity == DUMMY_ENTITY_ID:
discovery_by = self._power_profile.discovery_by if self._power_profile else DiscoveryBy.ENTITY
if self.source_entity == DUMMY_ENTITY_ID and discovery_by == DiscoveryBy.ENTITY:
state = State(self.source_entity, STATE_ON)

if not state or not self._has_valid_state(state):
Expand Down
1 change: 1 addition & 0 deletions docs/source/library/device-types/smart-switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Examples of this type of smart switch are:
```json
{
"calculation_strategy": "multi_switch",
"discovery_by": "device",
"multi_switch_config": {
"power": 0.8,
"power_off": 0.25
Expand Down
2 changes: 2 additions & 0 deletions profile_library/tp-link/HS300/model.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "Kasa Smart Wi-Fi Power Strip",
"device_type": "smart_switch",
"discovery_by": "device",
"calculation_strategy": "multi_switch",
"measure_method": "manual",
"measure_device": "Kill-a-Watt",
Expand All @@ -14,5 +15,6 @@
"aliases": [
"HS300(US)"
],
"only_self_usage": true,
"author": "Bram Gerritsen <[email protected]>"
}
Loading
Loading