Skip to content

Commit a5b4aff

Browse files
committed
fix: extract form config normalization to reduce duplication
1 parent 043322b commit a5b4aff

3 files changed

Lines changed: 98 additions & 87 deletions

File tree

custom_components/powercalc/flow_helper/flows/virtual_power.py

Lines changed: 7 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@
4747
PowercalcFormStep,
4848
Step,
4949
fill_schema_defaults,
50-
unwrap_choose_selector,
51-
wrap_choose_selector,
5250
)
5351
from custom_components.powercalc.flow_helper.flows.global_configuration import get_global_powercalc_config
5452
from custom_components.powercalc.flow_helper.flows.library import (
@@ -61,6 +59,13 @@
6159
SCHEMA_SENSOR_ENERGY_OPTIONS,
6260
SCHEMA_UTILITY_METER_TOGGLE,
6361
)
62+
from custom_components.powercalc.flow_helper.strategy_form import (
63+
FIXED_CHOICES,
64+
find_present_choice,
65+
order_choices_for_default,
66+
unwrap_strategy_user_input,
67+
wrap_strategy_form_data,
68+
)
6469
from custom_components.powercalc.power_profile.power_profile import DeviceType
6570
from custom_components.powercalc.strategy.wled import CONFIG_SCHEMA as SCHEMA_POWER_WLED
6671

@@ -110,37 +115,6 @@
110115
}
111116

112117

113-
def order_choices_for_default[T](
114-
choices: dict[str, T],
115-
default_choice: str | None,
116-
) -> dict[str, T]:
117-
"""Put the default choice first because HA initializes choose selectors from the first choice."""
118-
if default_choice not in choices:
119-
return choices
120-
return {
121-
default_choice: choices[default_choice],
122-
**{choice: config for choice, config in choices.items() if choice != default_choice},
123-
}
124-
125-
126-
def has_saved_choice_value(value: object) -> bool:
127-
"""Return whether a saved strategy value should drive the selected form choice."""
128-
if value is None:
129-
return False
130-
if isinstance(value, (str, list, dict)):
131-
return bool(value)
132-
return True
133-
134-
135-
def find_present_choice(form_data: dict[str, Any], choices: dict[str, list[str] | str]) -> str | None:
136-
"""Find the first choice that has matching config data."""
137-
for choice_id, mapping in choices.items():
138-
keys = [mapping] if isinstance(mapping, str) else mapping
139-
if any(has_saved_choice_value(form_data.get(key)) for key in keys):
140-
return choice_id
141-
return None
142-
143-
144118
SCHEMA_POWER_FIXED = vol.Schema(
145119
{
146120
vol.Required(CONF_FIXED_VALUE): selector.ChooseSelector(
@@ -177,49 +151,6 @@ def find_present_choice(form_data: dict[str, Any], choices: dict[str, list[str]
177151
},
178152
)
179153

180-
FIXED_CHOICES: dict[str, list[str] | str] = {
181-
CONF_STATES_POWER: CONF_STATES_POWER,
182-
CONF_POWER_TEMPLATE: CONF_POWER_TEMPLATE,
183-
CONF_POWER: CONF_POWER,
184-
}
185-
186-
187-
def fixed_choice_key_from_validated_value(value: object) -> str:
188-
"""Infer the fixed strategy config key from a validated ChooseSelector value."""
189-
if isinstance(value, list):
190-
return CONF_STATES_POWER
191-
if isinstance(value, str):
192-
return CONF_POWER_TEMPLATE
193-
return CONF_POWER
194-
195-
196-
def unwrap_strategy_user_input(strategy: CalculationStrategy, user_input: dict[str, Any]) -> dict[str, Any]:
197-
"""Unwrap ChooseSelector wrappers and normalize list/dict shapes for strategy user input."""
198-
if strategy == CalculationStrategy.FIXED:
199-
unwrap_choose_selector(user_input, CONF_FIXED_VALUE, fixed_choice_key_from_validated_value)
200-
if CONF_STATE_TRIGGER in user_input and isinstance(user_input[CONF_STATE_TRIGGER], list):
201-
user_input[CONF_STATE_TRIGGER] = {
202-
item[CONF_STATE]: item[CONF_PLAYBOOK_ID] for item in user_input[CONF_STATE_TRIGGER]
203-
}
204-
return user_input
205-
206-
207-
def wrap_strategy_form_data(strategy: CalculationStrategy, form_data: dict[str, Any]) -> dict[str, Any]:
208-
"""Wrap flat strategy config back into ChooseSelector form structure for display."""
209-
if strategy == CalculationStrategy.FIXED:
210-
choices = order_choices_for_default(FIXED_CHOICES, find_present_choice(form_data, FIXED_CHOICES))
211-
form_data = wrap_choose_selector(form_data, CONF_FIXED_VALUE, choices, raw_value=True)
212-
if CONF_STATE_TRIGGER in form_data and isinstance(form_data[CONF_STATE_TRIGGER], dict):
213-
form_data = {
214-
**form_data,
215-
CONF_STATE_TRIGGER: [
216-
{CONF_STATE: state, CONF_PLAYBOOK_ID: playbook_id}
217-
for state, playbook_id in form_data[CONF_STATE_TRIGGER].items()
218-
],
219-
}
220-
return form_data
221-
222-
223154
SCHEMA_POWER_MULTI_SWITCH_MANUAL = vol.Schema(
224155
{
225156
vol.Required(CONF_POWER): vol.Coerce(float),

custom_components/powercalc/flow_helper/profile_preview.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import voluptuous as vol
1414

1515
from custom_components.powercalc.common import SourceEntity
16-
from custom_components.powercalc.const import CONF_FIXED_VALUE, CalculationStrategy
16+
from custom_components.powercalc.const import CalculationStrategy
1717
from custom_components.powercalc.errors import StrategyConfigurationError, UnsupportedStrategyError
18-
from custom_components.powercalc.flow_helper.common import unwrap_choose_selector
18+
from custom_components.powercalc.flow_helper.strategy_form import unwrap_strategy_user_input
1919
from custom_components.powercalc.power_profile.power_profile import PowerProfile
2020
from custom_components.powercalc.strategy.factory import PowerCalculatorStrategyFactory
2121
from custom_components.powercalc.strategy.selector import detect_calculation_strategy
@@ -125,18 +125,10 @@ def _build_preview_sensor_config(flow: PreviewFlowProtocol, step_id: str, user_i
125125
except ValueError:
126126
return sensor_config
127127

128-
sensor_config[strategy] = _unwrap_preview_strategy_input(strategy, user_input)
128+
sensor_config[strategy] = unwrap_strategy_user_input(strategy, dict(user_input))
129129
return sensor_config
130130

131131

132-
def _unwrap_preview_strategy_input(strategy: CalculationStrategy, user_input: dict[str, Any]) -> dict[str, Any]:
133-
"""Unwrap form-only selector wrappers before building a preview strategy config."""
134-
unwrapped = dict(user_input)
135-
if strategy == CalculationStrategy.FIXED:
136-
unwrap_choose_selector(unwrapped, CONF_FIXED_VALUE)
137-
return unwrapped
138-
139-
140132
async def build_profile_preview(
141133
hass: HomeAssistant,
142134
sensor_config: ConfigType,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from custom_components.powercalc.const import (
6+
CONF_FIXED_VALUE,
7+
CONF_PLAYBOOK_ID,
8+
CONF_POWER,
9+
CONF_POWER_TEMPLATE,
10+
CONF_STATE,
11+
CONF_STATE_TRIGGER,
12+
CONF_STATES_POWER,
13+
CalculationStrategy,
14+
)
15+
from custom_components.powercalc.flow_helper.common import unwrap_choose_selector, wrap_choose_selector
16+
17+
FIXED_CHOICES: dict[str, list[str] | str] = {
18+
CONF_STATES_POWER: CONF_STATES_POWER,
19+
CONF_POWER_TEMPLATE: CONF_POWER_TEMPLATE,
20+
CONF_POWER: CONF_POWER,
21+
}
22+
23+
24+
def order_choices_for_default[T](
25+
choices: dict[str, T],
26+
default_choice: str | None,
27+
) -> dict[str, T]:
28+
"""Put the default choice first because HA initializes choose selectors from the first choice."""
29+
if default_choice not in choices:
30+
return choices
31+
return {
32+
default_choice: choices[default_choice],
33+
**{choice: config for choice, config in choices.items() if choice != default_choice},
34+
}
35+
36+
37+
def has_saved_choice_value(value: object) -> bool:
38+
"""Return whether a saved strategy value should drive the selected form choice."""
39+
if value is None:
40+
return False
41+
if isinstance(value, (str, list, dict)):
42+
return bool(value)
43+
return True
44+
45+
46+
def find_present_choice(form_data: dict[str, Any], choices: dict[str, list[str] | str]) -> str | None:
47+
"""Find the first choice that has matching config data."""
48+
for choice_id, mapping in choices.items():
49+
keys = [mapping] if isinstance(mapping, str) else mapping
50+
if any(has_saved_choice_value(form_data.get(key)) for key in keys):
51+
return choice_id
52+
return None
53+
54+
55+
def fixed_choice_key_from_validated_value(value: object) -> str:
56+
"""Infer the fixed strategy config key from a validated ChooseSelector value."""
57+
if isinstance(value, list):
58+
return CONF_STATES_POWER
59+
if isinstance(value, str):
60+
return CONF_POWER_TEMPLATE
61+
return CONF_POWER
62+
63+
64+
def unwrap_strategy_user_input(strategy: CalculationStrategy, user_input: dict[str, Any]) -> dict[str, Any]:
65+
"""Unwrap form-only selector wrappers and normalize strategy user input."""
66+
if strategy == CalculationStrategy.FIXED:
67+
unwrap_choose_selector(user_input, CONF_FIXED_VALUE, fixed_choice_key_from_validated_value)
68+
if CONF_STATE_TRIGGER in user_input and isinstance(user_input[CONF_STATE_TRIGGER], list):
69+
user_input[CONF_STATE_TRIGGER] = {
70+
item[CONF_STATE]: item[CONF_PLAYBOOK_ID] for item in user_input[CONF_STATE_TRIGGER]
71+
}
72+
return user_input
73+
74+
75+
def wrap_strategy_form_data(strategy: CalculationStrategy, form_data: dict[str, Any]) -> dict[str, Any]:
76+
"""Wrap stored strategy config back into form-only selector structures."""
77+
if strategy == CalculationStrategy.FIXED:
78+
choices = order_choices_for_default(FIXED_CHOICES, find_present_choice(form_data, FIXED_CHOICES))
79+
form_data = wrap_choose_selector(form_data, CONF_FIXED_VALUE, choices, raw_value=True)
80+
if CONF_STATE_TRIGGER in form_data and isinstance(form_data[CONF_STATE_TRIGGER], dict):
81+
form_data = {
82+
**form_data,
83+
CONF_STATE_TRIGGER: [
84+
{CONF_STATE: state, CONF_PLAYBOOK_ID: playbook_id}
85+
for state, playbook_id in form_data[CONF_STATE_TRIGGER].items()
86+
],
87+
}
88+
return form_data

0 commit comments

Comments
 (0)