Skip to content
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
15 changes: 15 additions & 0 deletions custom_components/simple_pid_controller/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
CONF_INPUT_RANGE_MAX,
CONF_OUTPUT_RANGE_MIN,
CONF_OUTPUT_RANGE_MAX,
CONF_STEP_PREFIX,
DEFAULT_INPUT_RANGE_MIN,
DEFAULT_INPUT_RANGE_MAX,
DEFAULT_OUTPUT_RANGE_MIN,
DEFAULT_OUTPUT_RANGE_MAX,
DEFAULT_STEPS,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -146,6 +148,18 @@ async def async_step_init(
CONF_OUTPUT_RANGE_MAX, DEFAULT_OUTPUT_RANGE_MAX
)

step_fields = {
vol.Optional(
f"{CONF_STEP_PREFIX}{key}",
default=self.config_entry.options.get(
f"{CONF_STEP_PREFIX}{key}", DEFAULT_STEPS[key]
),
): selector(
{"number": {"min": 0.0001, "max": 100.0, "step": 0.001, "mode": "box"}}
)
for key in DEFAULT_STEPS
}

options_schema = vol.Schema(
{
vol.Required(
Expand All @@ -168,6 +182,7 @@ async def async_step_init(
CONF_OUTPUT_RANGE_MAX,
default=current_output_max,
): vol.Coerce(float),
**step_fields,
}
)

Expand Down
13 changes: 13 additions & 0 deletions custom_components/simple_pid_controller/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,16 @@
DEFAULT_INPUT_RANGE_MAX = 100.0
DEFAULT_OUTPUT_RANGE_MIN = 0.0
DEFAULT_OUTPUT_RANGE_MAX = 100.0

CONF_STEP_PREFIX = "step_"

DEFAULT_STEPS: dict[str, float] = {
"kp": 0.0001,
"ki": 0.0001,
"kd": 0.0001,
"sample_time": 0.01,
"setpoint": 0.01,
"output_min": 1.0,
"output_max": 1.0,
"starting_output": 1.0,
}
12 changes: 9 additions & 3 deletions custom_components/simple_pid_controller/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
CONF_INPUT_RANGE_MAX,
CONF_OUTPUT_RANGE_MIN,
CONF_OUTPUT_RANGE_MAX,
CONF_STEP_PREFIX,
DEFAULT_INPUT_RANGE_MIN,
DEFAULT_INPUT_RANGE_MAX,
DEFAULT_OUTPUT_RANGE_MIN,
DEFAULT_OUTPUT_RANGE_MAX,
DEFAULT_STEPS,
)

# Coordinator is used to centralize the data updates
Expand Down Expand Up @@ -129,7 +131,9 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, desc: dict) -> None:
self._attr_native_unit_of_measurement = desc["unit"]
self._attr_native_min_value = desc["min"]
self._attr_native_max_value = desc["max"]
self._attr_native_step = desc["step"]
self._attr_native_step = (entry.options or {}).get(
f"{CONF_STEP_PREFIX}{desc['key']}", DEFAULT_STEPS[desc["key"]]
)
self._attr_native_value = desc["default"]
self._attr_entity_category = desc["entity_category"]

Expand Down Expand Up @@ -162,7 +166,6 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, desc: dict) -> None:
self._attr_icon = "mdi:ray-vertex"
self._attr_mode = "box"
self._attr_native_unit_of_measurement = desc["unit"]
self._attr_native_step = desc["step"]
self._attr_native_value = desc["default"]
self._attr_entity_category = desc["entity_category"]
self._key = desc["key"]
Expand Down Expand Up @@ -208,7 +211,10 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, desc: dict) -> None:

self._attr_native_min_value = min_val
self._attr_native_max_value = max_val
self._attr_native_step = desc.get("step", 1.0)
self._attr_native_step = opts.get(
f"{CONF_STEP_PREFIX}{self._key}",
DEFAULT_STEPS.get(self._key, desc.get("step", 1.0)),
)

# Initialize current value
if self._key == "setpoint":
Expand Down
20 changes: 19 additions & 1 deletion custom_components/simple_pid_controller/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,25 @@
"input_range_min": "Minimum Input Range",
"input_range_max": "Maximum Input Range",
"output_range_min": "Minimum Output Range",
"output_range_max": "Maximum Output Range"
"output_range_max": "Maximum Output Range",
"step_kp": "Kp Step Size",
"step_ki": "Ki Step Size",
"step_kd": "Kd Step Size",
"step_sample_time": "Sample Time Step Size",
"step_setpoint": "Setpoint Step Size",
"step_output_min": "Output Min Step Size",
"step_output_max": "Output Max Step Size",
"step_starting_output": "Startup Value Step Size"
},
"data_description": {
"step_kp": "Increment size for the Kp parameter.",
"step_ki": "Increment size for the Ki parameter.",
"step_kd": "Increment size for the Kd parameter.",
"step_sample_time": "Increment size for the sample time parameter.",
"step_setpoint": "Increment size for the setpoint picker.",
"step_output_min": "Increment size for the output minimum.",
"step_output_max": "Increment size for the output maximum.",
"step_starting_output": "Increment size for the startup value."
}
}
}
Expand Down
62 changes: 40 additions & 22 deletions custom_components/simple_pid_controller/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,33 @@
"input_range_min": "Minimum Input Range",
"input_range_max": "Maximum Input Range",
"output_range_min": "Minimum Output Range",
"output_range_max": "Maximum Output Range"
"output_range_max": "Maximum Output Range",
"step_kp": "Kp Step Size",
"step_ki": "Ki Step Size",
"step_kd": "Kd Step Size",
"step_sample_time": "Sample Time Step Size",
"step_setpoint": "Setpoint Step Size",
"step_output_min": "Output Min Step Size",
"step_output_max": "Output Max Step Size",
"step_starting_output": "Startup Value Step Size"
},
"data_description": {
"step_kp": "Increment size for the Kp parameter.",
"step_ki": "Increment size for the Ki parameter.",
"step_kd": "Increment size for the Kd parameter.",
"step_sample_time": "Increment size for the sample time parameter.",
"step_setpoint": "Increment size for the setpoint picker.",
"step_output_min": "Increment size for the output minimum.",
"step_output_max": "Increment size for the output maximum.",
"step_starting_output": "Increment size for the startup value."
}
}
},
"error": {
"range_min_max": "Minimum must be lower than maximum."
}
},
"entity": {
"entity": {
"number": {
"kp": {
"name": "Kp"
Expand Down Expand Up @@ -75,23 +93,23 @@
"current_value": {
"name": "Current Value"
}
}
}
,
"services": {
"set_output": {
"name": "Set PID output",
"description": "Set or reset the PID controller output.",
"fields": {
"preset": {
"name": "Preset",
"description": "Use a preset output: zero_start, last_known_value or startup_value."
},
"value": {
"name": "Value",
"description": "Manual output value between output_min and output_max."
}
}
}
}
}
}
}
,
"services": {
"set_output": {
"name": "Set PID output",
"description": "Set or reset the PID controller output.",
"fields": {
"preset": {
"name": "Preset",
"description": "Use a preset output: zero_start, last_known_value or startup_value."
},
"value": {
"name": "Value",
"description": "Manual output value between output_min and output_max."
}
}
}
}
}
20 changes: 19 additions & 1 deletion custom_components/simple_pid_controller/translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,25 @@
"input_range_min": "Minimum Input Bereik",
"input_range_max": "Maximum Input Bereik",
"output_range_min": "Minimum Output Bereik",
"output_range_max": "Maximum Output Bereik"
"output_range_max": "Maximum Output Bereik",
"step_kp": "Kp Stapgrootte",
"step_ki": "Ki Stapgrootte",
"step_kd": "Kd Stapgrootte",
"step_sample_time": "Steektijd Stapgrootte",
"step_setpoint": "Stapgrootte doelwaarde",
"step_output_min": "Stapgrootte min. uitvoer",
"step_output_max": "Stapgrootte max. uitvoer",
"step_starting_output": "Stapgrootte startwaarde"
},
"data_description": {
"step_kp": "Stapgrootte voor de Kp-parameter.",
"step_ki": "Stapgrootte voor de Ki-parameter.",
"step_kd": "Stapgrootte voor de Kd-parameter.",
"step_sample_time": "Stapgrootte voor de steektijd.",
"step_setpoint": "Stapgrootte voor de setpoint-kiezer.",
"step_output_min": "Stapgrootte voor de minimale uitvoer.",
"step_output_max": "Stapgrootte voor de maximale uitvoer.",
"step_starting_output": "Stapgrootte voor de startwaarde."
}
}
},
Expand Down
78 changes: 76 additions & 2 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import pytest


from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.data_entry_flow import FlowResultType, InvalidData

from custom_components.simple_pid_controller.const import (
CONF_STEP_PREFIX,
DOMAIN,
CONF_NAME,
CONF_SENSOR_ENTITY_ID,
Expand All @@ -15,6 +17,7 @@
DEFAULT_INPUT_RANGE_MAX,
DEFAULT_OUTPUT_RANGE_MIN,
DEFAULT_OUTPUT_RANGE_MAX,
DEFAULT_STEPS,
)
from custom_components.simple_pid_controller.config_flow import (
PIDControllerFlowHandler,
Expand Down Expand Up @@ -184,7 +187,78 @@ async def test_options_flow(hass, config_entry, new_options, expected_errors):
assert result2.get("errors") == expected_errors
else:
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2.get("data") == new_options
# Step defaults are added by voluptuous; check submitted fields are present
assert new_options.items() <= result2.get("data", {}).items()


async def test_options_flow_with_custom_steps(hass, config_entry):
"""Test that step options round-trip correctly through the options flow."""
custom_steps = {f"{CONF_STEP_PREFIX}{key}": 0.5 for key in DEFAULT_STEPS}
new_options = {
CONF_SENSOR_ENTITY_ID: "sensor.new",
CONF_INPUT_RANGE_MIN: 1.0,
CONF_INPUT_RANGE_MAX: 10.0,
CONF_OUTPUT_RANGE_MIN: 1.0,
CONF_OUTPUT_RANGE_MAX: 10.0,
**custom_steps,
}

init_result = await hass.config_entries.options.async_init(config_entry.entry_id)
result2 = await hass.config_entries.options.async_configure(
init_result["flow_id"], user_input=new_options
)

assert result2["type"] == FlowResultType.CREATE_ENTRY
for key, val in custom_steps.items():
assert result2["data"][key] == val


_BASE_OPTIONS = {
CONF_SENSOR_ENTITY_ID: "sensor.new",
CONF_INPUT_RANGE_MIN: 1.0,
CONF_INPUT_RANGE_MAX: 10.0,
CONF_OUTPUT_RANGE_MIN: 1.0,
CONF_OUTPUT_RANGE_MAX: 10.0,
}


@pytest.mark.usefixtures("setup_integration")
@pytest.mark.parametrize("invalid_step", [0.0, -1.0, -0.0001])
async def test_options_flow_step_below_min_rejected(hass, config_entry, invalid_step):
"""Test that a step value below the selector minimum raises InvalidData."""
init_result = await hass.config_entries.options.async_init(config_entry.entry_id)
with pytest.raises(InvalidData):
await hass.config_entries.options.async_configure(
init_result["flow_id"],
user_input={**_BASE_OPTIONS, f"{CONF_STEP_PREFIX}kp": invalid_step},
)


@pytest.mark.usefixtures("setup_integration")
@pytest.mark.parametrize("invalid_step", [100.001, 200.0, 1000.0])
async def test_options_flow_step_above_max_rejected(hass, config_entry, invalid_step):
"""Test that a step value above the selector maximum raises InvalidData."""
init_result = await hass.config_entries.options.async_init(config_entry.entry_id)
with pytest.raises(InvalidData):
await hass.config_entries.options.async_configure(
init_result["flow_id"],
user_input={**_BASE_OPTIONS, f"{CONF_STEP_PREFIX}setpoint": invalid_step},
)


@pytest.mark.usefixtures("setup_integration")
@pytest.mark.parametrize("boundary_step", [0.0001, 100.0])
async def test_options_flow_step_boundary_values_accepted(
hass, config_entry, boundary_step
):
"""Test that step values at the exact selector boundaries are accepted."""
init_result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
init_result["flow_id"],
user_input={**_BASE_OPTIONS, f"{CONF_STEP_PREFIX}kp": boundary_step},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"][f"{CONF_STEP_PREFIX}kp"] == boundary_step


async def test_user_flow_duplicate_abort(hass):
Expand Down
Loading