diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 071de00..69f4b4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Get version id: version diff --git a/README.md b/README.md index 678091d..58b64c5 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - [Manual Tuning](#1-manual-trial--error) - [Ziegler–Nichols Method](#2-zieglernichols-method) - [PID Calculation Frequency and Sample Time](#pid-calculation-frequency-and-sample-time) +- [More details and extended documentation]{#more-details-and-extended-documentation) - [Example PID Graph](#example-pid-graph) - [Support & Development](#support--development) - [Service Actions](#service-actions) @@ -180,6 +181,14 @@ This integration recalculates the PID output at a fixed, user-configurable inter By using a single **Sample Time** for both scheduling and calculation—and understanding that each component tracks its own clock—you get predictable, evenly-spaced control updates while allowing Home Assistant’s event loop to manage timing drifts gracefully.``` +--- + +## 📚 More details and extended documentation + +The integration is based on simple-pid [https://pypi.org/project/simple-pid/](https://pypi.org/project/simple-pid/) + +Read the user guide here: [https://simple-pid.readthedocs.io/en/latest/user_guide.html#user-guide](https://simple-pid.readthedocs.io/en/latest/user_guide.html#user-guide) + --- ## 📈 Example PID Graph diff --git a/custom_components/simple_pid_controller/__init__.py b/custom_components/simple_pid_controller/__init__.py index abd6091..6d8826c 100644 --- a/custom_components/simple_pid_controller/__init__.py +++ b/custom_components/simple_pid_controller/__init__.py @@ -15,10 +15,14 @@ DOMAIN, CONF_NAME, CONF_SENSOR_ENTITY_ID, - CONF_RANGE_MIN, - CONF_RANGE_MAX, - DEFAULT_RANGE_MIN, - DEFAULT_RANGE_MAX, + CONF_INPUT_RANGE_MIN, + CONF_INPUT_RANGE_MAX, + CONF_OUTPUT_RANGE_MIN, + CONF_OUTPUT_RANGE_MAX, + DEFAULT_INPUT_RANGE_MIN, + DEFAULT_INPUT_RANGE_MAX, + DEFAULT_OUTPUT_RANGE_MIN, + DEFAULT_OUTPUT_RANGE_MAX, ) _LOGGER = logging.getLogger(__name__) @@ -39,11 +43,21 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self.hass = hass self.entry = entry self.name = entry.data.get(CONF_NAME) - self.range_min = entry.options.get( - CONF_RANGE_MIN, entry.data.get(CONF_RANGE_MIN, DEFAULT_RANGE_MIN) + self.input_range_min = entry.options.get( + CONF_INPUT_RANGE_MIN, + entry.data.get(CONF_INPUT_RANGE_MIN, DEFAULT_INPUT_RANGE_MIN), ) - self.range_max = entry.options.get( - CONF_RANGE_MAX, entry.data.get(CONF_RANGE_MAX, DEFAULT_RANGE_MAX) + self.input_range_max = entry.options.get( + CONF_INPUT_RANGE_MAX, + entry.data.get(CONF_INPUT_RANGE_MAX, DEFAULT_INPUT_RANGE_MAX), + ) + self.output_range_min = entry.options.get( + CONF_OUTPUT_RANGE_MIN, + entry.data.get(CONF_OUTPUT_RANGE_MIN, DEFAULT_OUTPUT_RANGE_MIN), + ) + self.output_range_max = entry.options.get( + CONF_OUTPUT_RANGE_MAX, + entry.data.get(CONF_OUTPUT_RANGE_MAX, DEFAULT_OUTPUT_RANGE_MAX), ) self.sensor_entity_id = entry.options.get( CONF_SENSOR_ENTITY_ID, entry.data.get(CONF_SENSOR_ENTITY_ID) diff --git a/custom_components/simple_pid_controller/config_flow.py b/custom_components/simple_pid_controller/config_flow.py index 899a446..b64a162 100644 --- a/custom_components/simple_pid_controller/config_flow.py +++ b/custom_components/simple_pid_controller/config_flow.py @@ -21,10 +21,14 @@ CONF_NAME, DEFAULT_NAME, CONF_SENSOR_ENTITY_ID, - CONF_RANGE_MIN, - CONF_RANGE_MAX, - DEFAULT_RANGE_MIN, - DEFAULT_RANGE_MAX, + CONF_INPUT_RANGE_MIN, + CONF_INPUT_RANGE_MAX, + CONF_OUTPUT_RANGE_MIN, + CONF_OUTPUT_RANGE_MAX, + DEFAULT_INPUT_RANGE_MIN, + DEFAULT_INPUT_RANGE_MAX, + DEFAULT_OUTPUT_RANGE_MIN, + DEFAULT_OUTPUT_RANGE_MAX, ) _LOGGER = logging.getLogger(__name__) @@ -54,12 +58,18 @@ async def async_step_user( vol.Required(CONF_SENSOR_ENTITY_ID): selector( {"entity": {"domain": "sensor"}} ), - vol.Optional(CONF_RANGE_MIN, default=DEFAULT_RANGE_MIN): vol.Coerce( - float - ), - vol.Optional(CONF_RANGE_MAX, default=DEFAULT_RANGE_MAX): vol.Coerce( - float - ), + vol.Optional( + CONF_INPUT_RANGE_MIN, default=DEFAULT_INPUT_RANGE_MIN + ): vol.Coerce(float), + vol.Optional( + CONF_INPUT_RANGE_MAX, default=DEFAULT_INPUT_RANGE_MAX + ): vol.Coerce(float), + vol.Optional( + CONF_OUTPUT_RANGE_MIN, default=DEFAULT_OUTPUT_RANGE_MIN + ): vol.Coerce(float), + vol.Optional( + CONF_OUTPUT_RANGE_MAX, default=DEFAULT_OUTPUT_RANGE_MAX + ): vol.Coerce(float), } ) @@ -67,11 +77,29 @@ async def async_step_user( self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) # Validate that range_min < range_max - min_val = user_input.get(CONF_RANGE_MIN) - max_val = user_input.get(CONF_RANGE_MAX) - if min_val is not None and max_val is not None and min_val >= max_val: + input_min_val = user_input.get(CONF_INPUT_RANGE_MIN) + input_max_val = user_input.get(CONF_INPUT_RANGE_MAX) + if ( + input_min_val is not None + and input_max_val is not None + and input_min_val >= input_max_val + ): + return self.async_show_form( + step_id="user", + data_schema=schema, + errors={"base": "input_range_min_max"}, + ) + output_min_val = user_input.get(CONF_OUTPUT_RANGE_MIN) + output_max_val = user_input.get(CONF_OUTPUT_RANGE_MAX) + if ( + output_min_val is not None + and output_max_val is not None + and output_min_val >= output_max_val + ): return self.async_show_form( - step_id="user", data_schema=schema, errors={"base": "range_min_max"} + step_id="user", + data_schema=schema, + errors={"base": "output_range_min_max"}, ) return self.async_create_entry( @@ -79,8 +107,10 @@ async def async_step_user( data={ CONF_NAME: user_input[CONF_NAME], CONF_SENSOR_ENTITY_ID: user_input[CONF_SENSOR_ENTITY_ID], - CONF_RANGE_MIN: user_input[CONF_RANGE_MIN], - CONF_RANGE_MAX: user_input[CONF_RANGE_MAX], + CONF_INPUT_RANGE_MIN: user_input[CONF_INPUT_RANGE_MIN], + CONF_INPUT_RANGE_MAX: user_input[CONF_INPUT_RANGE_MAX], + CONF_OUTPUT_RANGE_MIN: user_input[CONF_OUTPUT_RANGE_MIN], + CONF_OUTPUT_RANGE_MAX: user_input[CONF_OUTPUT_RANGE_MAX], }, ) @@ -103,8 +133,18 @@ async def async_step_init( current_sensor = self.config_entry.options.get( CONF_SENSOR_ENTITY_ID ) or self.config_entry.data.get(CONF_SENSOR_ENTITY_ID) - current_min = self.config_entry.options.get(CONF_RANGE_MIN, DEFAULT_RANGE_MIN) - current_max = self.config_entry.options.get(CONF_RANGE_MAX, DEFAULT_RANGE_MAX) + current_input_min = self.config_entry.options.get( + CONF_INPUT_RANGE_MIN, DEFAULT_INPUT_RANGE_MIN + ) + current_input_max = self.config_entry.options.get( + CONF_INPUT_RANGE_MAX, DEFAULT_INPUT_RANGE_MAX + ) + current_output_min = self.config_entry.options.get( + CONF_OUTPUT_RANGE_MIN, DEFAULT_OUTPUT_RANGE_MIN + ) + current_output_max = self.config_entry.options.get( + CONF_OUTPUT_RANGE_MAX, DEFAULT_OUTPUT_RANGE_MAX + ) options_schema = vol.Schema( { @@ -113,12 +153,20 @@ async def async_step_init( default=current_sensor, ): selector({"entity": {"domain": "sensor"}}), vol.Required( - CONF_RANGE_MIN, - default=current_min, + CONF_INPUT_RANGE_MIN, + default=current_input_min, + ): vol.Coerce(float), + vol.Required( + CONF_INPUT_RANGE_MAX, + default=current_input_max, + ): vol.Coerce(float), + vol.Required( + CONF_OUTPUT_RANGE_MIN, + default=current_output_min, ): vol.Coerce(float), vol.Required( - CONF_RANGE_MAX, - default=current_max, + CONF_OUTPUT_RANGE_MAX, + default=current_output_max, ): vol.Coerce(float), } ) @@ -126,13 +174,29 @@ async def async_step_init( # If the user has submitted the form, create the entry if user_input is not None: # Validate that range_min < range_max - min_val = user_input.get(CONF_RANGE_MIN) - max_val = user_input.get(CONF_RANGE_MAX) - if min_val is not None and max_val is not None and min_val >= max_val: + input_min_val = user_input.get(CONF_INPUT_RANGE_MIN) + input_max_val = user_input.get(CONF_INPUT_RANGE_MAX) + if ( + input_min_val is not None + and input_max_val is not None + and input_min_val >= input_max_val + ): + return self.async_show_form( + step_id="init", + data_schema=options_schema, + errors={"base": "input_range_min_max"}, + ) + output_min_val = user_input.get(CONF_OUTPUT_RANGE_MIN) + output_max_val = user_input.get(CONF_OUTPUT_RANGE_MAX) + if ( + output_min_val is not None + and output_max_val is not None + and output_min_val >= output_max_val + ): return self.async_show_form( step_id="init", data_schema=options_schema, - errors={"base": "range_min_max"}, + errors={"base": "output_range_min_max"}, ) return self.async_create_entry( diff --git a/custom_components/simple_pid_controller/const.py b/custom_components/simple_pid_controller/const.py index de37472..592b1b5 100644 --- a/custom_components/simple_pid_controller/const.py +++ b/custom_components/simple_pid_controller/const.py @@ -7,8 +7,12 @@ CONF_SENSOR_ENTITY_ID = "sensor_entity_id" -CONF_RANGE_MIN = "range_min" -CONF_RANGE_MAX = "range_max" +CONF_INPUT_RANGE_MIN = "input_range_min" +CONF_INPUT_RANGE_MAX = "input_range_max" +CONF_OUTPUT_RANGE_MIN = "output_range_min" +CONF_OUTPUT_RANGE_MAX = "output_range_max" -DEFAULT_RANGE_MIN = 0.0 -DEFAULT_RANGE_MAX = 100.0 +DEFAULT_INPUT_RANGE_MIN = 0.0 +DEFAULT_INPUT_RANGE_MAX = 100.0 +DEFAULT_OUTPUT_RANGE_MIN = 0.0 +DEFAULT_OUTPUT_RANGE_MAX = 100.0 diff --git a/custom_components/simple_pid_controller/diagnostics.py b/custom_components/simple_pid_controller/diagnostics.py index 204ff49..7cfa04b 100644 --- a/custom_components/simple_pid_controller/diagnostics.py +++ b/custom_components/simple_pid_controller/diagnostics.py @@ -18,7 +18,9 @@ async def async_get_config_entry_diagnostics( "data": { "name": handle.name, "sensor_entity_id": handle.sensor_entity_id, - "range_min": handle.range_min, - "range_max": handle.range_max, + "input_range_min": handle.input_range_min, + "input_range_max": handle.input_range_max, + "output_range_min": handle.output_range_min, + "output_range_max": handle.output_range_max, }, } diff --git a/custom_components/simple_pid_controller/number.py b/custom_components/simple_pid_controller/number.py index bf44c7b..7b02619 100644 --- a/custom_components/simple_pid_controller/number.py +++ b/custom_components/simple_pid_controller/number.py @@ -12,10 +12,14 @@ from .entity import BasePIDEntity from .const import ( - CONF_RANGE_MIN, - CONF_RANGE_MAX, - DEFAULT_RANGE_MIN, - DEFAULT_RANGE_MAX, + CONF_INPUT_RANGE_MIN, + CONF_INPUT_RANGE_MAX, + CONF_OUTPUT_RANGE_MIN, + CONF_OUTPUT_RANGE_MAX, + DEFAULT_INPUT_RANGE_MIN, + DEFAULT_INPUT_RANGE_MAX, + DEFAULT_OUTPUT_RANGE_MIN, + DEFAULT_OUTPUT_RANGE_MAX, ) # Coordinator is used to centralize the data updates @@ -29,7 +33,7 @@ "name": "Kp", "key": "kp", "unit": "", - "min": 0.0, + "min": -10.0, "max": 10.0, "step": 0.01, "default": 1.0, @@ -39,7 +43,7 @@ "name": "Ki", "key": "ki", "unit": "", - "min": 0.0, + "min": -10.0, "max": 10.0, "step": 0.01, "default": 0.1, @@ -49,7 +53,7 @@ "name": "Kd", "key": "kd", "unit": "", - "min": 0.0, + "min": -10.0, "max": 10.0, "step": 0.01, "default": 0.05, @@ -158,22 +162,39 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, desc: dict) -> None: # Compute range limits based on key opts = entry.options or {} data = entry.data or {} - range_min = opts.get( - CONF_RANGE_MIN, data.get(CONF_RANGE_MIN, DEFAULT_RANGE_MIN) + input_range_min = opts.get( + CONF_INPUT_RANGE_MIN, + data.get(CONF_INPUT_RANGE_MIN, DEFAULT_INPUT_RANGE_MIN), ) - range_max = opts.get( - CONF_RANGE_MAX, data.get(CONF_RANGE_MAX, DEFAULT_RANGE_MAX) + input_range_max = opts.get( + CONF_INPUT_RANGE_MAX, + data.get(CONF_INPUT_RANGE_MAX, DEFAULT_INPUT_RANGE_MAX), + ) + output_range_min = opts.get( + CONF_OUTPUT_RANGE_MIN, + data.get(CONF_OUTPUT_RANGE_MIN, DEFAULT_OUTPUT_RANGE_MIN), + ) + output_range_max = opts.get( + CONF_OUTPUT_RANGE_MAX, + data.get(CONF_OUTPUT_RANGE_MAX, DEFAULT_OUTPUT_RANGE_MAX), ) if self._key == "setpoint": - min_val, max_val = range_min, range_max + min_val, max_val = input_range_min, input_range_max elif self._key == "output_min": - min_val, max_val = -abs(range_max), range_min + min_val, max_val = output_range_min, output_range_max elif self._key == "output_max": - min_val, max_val = range_min, range_max + min_val, max_val = output_range_min, output_range_max else: - _LOGGER.error("Unexpected PID parameter key: %s", self._key) - min_val, max_val = DEFAULT_RANGE_MIN, DEFAULT_RANGE_MAX + _LOGGER.error( + "Unknown PID key '%s'. Using default values: input_min=%s, input_max=%s, output_min=%s, output_max=%s", + self._key, + DEFAULT_INPUT_RANGE_MIN, + DEFAULT_INPUT_RANGE_MAX, + DEFAULT_OUTPUT_RANGE_MIN, + DEFAULT_OUTPUT_RANGE_MAX, + ) + min_val, max_val = DEFAULT_INPUT_RANGE_MIN, DEFAULT_INPUT_RANGE_MAX self._attr_native_min_value = min_val self._attr_native_max_value = max_val @@ -181,13 +202,13 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, desc: dict) -> None: # Initialize current value if self._key == "setpoint": - self._attr_native_value = range_min + (range_max - range_min) * float( - desc["default"] - ) + self._attr_native_value = input_range_min + ( + input_range_max - input_range_min + ) * float(desc["default"]) elif self._key == "output_min": - self._attr_native_value = range_min + self._attr_native_value = output_range_min elif self._key == "output_max": - self._attr_native_value = range_max + self._attr_native_value = output_range_max else: _LOGGER.error("Unexpected error, unknown state in number.py") diff --git a/custom_components/simple_pid_controller/strings.json b/custom_components/simple_pid_controller/strings.json index 70a02c1..6bdd0a9 100644 --- a/custom_components/simple_pid_controller/strings.json +++ b/custom_components/simple_pid_controller/strings.json @@ -7,8 +7,11 @@ "description": "Enter a name and choose the sensor to drive the PID loop.", "data": { "name": "Name", - "sensor_entity_id": "Sensor Entity" - } + "sensor_entity_id": "Sensor Entity", + "input_range_min": "Minimum Input Range", + "input_range_max": "Maximum Input Range", + "output_range_min": "Minimum Output Range", + "output_range_max": "Maximum Output Range" } } }, "abort": { @@ -21,7 +24,11 @@ "title": "Edit PID Controller Options", "description": "Modify the sensor entity used by the controller.", "data": { - "sensor_entity_id": "Sensor Entity" + "sensor_entity_id": "Sensor Entity", + "input_range_min": "Minimum Input Range", + "input_range_max": "Maximum Input Range", + "output_range_min": "Minimum Output Range", + "output_range_max": "Maximum Output Range" } } } diff --git a/custom_components/simple_pid_controller/translations/en.json b/custom_components/simple_pid_controller/translations/en.json index 5b25d3c..da0dbb1 100644 --- a/custom_components/simple_pid_controller/translations/en.json +++ b/custom_components/simple_pid_controller/translations/en.json @@ -7,7 +7,11 @@ "description": "Enter a name and choose the sensor to drive the PID loop.", "data": { "name": "Name", - "sensor_entity_id": "Sensor Entity" + "sensor_entity_id": "Sensor Entity", + "input_range_min": "Minimum Input Range", + "input_range_max": "Maximum Input Range", + "output_range_min": "Minimum Output Range", + "output_range_max": "Maximum Output Range" } } }, @@ -16,7 +20,8 @@ }, "error": { "already_configured": "A configuration with this name already exists.", - "range_min_max": "Minimum must be lower than maximum." + "input_range_min_max": "Minimum must be lower than maximum.", + "output_range_min_max": "Minimum must be lower than maximum." } }, "options": { @@ -26,8 +31,10 @@ "description": "Modify the sensor entity and range used by the controller.", "data": { "sensor_entity_id": "Sensor Entity", - "range_min": "Minimum Range", - "range_max": "Maximum Range" + "input_range_min": "Minimum Input Range", + "input_range_max": "Maximum Input Range", + "output_range_min": "Minimum Output Range", + "output_range_max": "Maximum Output Range" } } }, diff --git a/custom_components/simple_pid_controller/translations/nl.json b/custom_components/simple_pid_controller/translations/nl.json index 1ff0624..8fa4b95 100644 --- a/custom_components/simple_pid_controller/translations/nl.json +++ b/custom_components/simple_pid_controller/translations/nl.json @@ -7,8 +7,11 @@ "description": "Voer een naam in en kies de sensor als input voor de PID.", "data": { "name": "Naam", - "sensor_entity_id": "Sensor naam" - } + "sensor_entity_id": "Sensor naam", + "input_range_min": "Minimum Input Bereik", + "input_range_max": "Maximum Input Bereik", + "output_range_min": "Minimum Output Bereik", + "output_range_max": "Maximum Output Bereik" } } }, "abort": { @@ -16,7 +19,8 @@ }, "error": { "already_configured": "Er bestaat al een configuratie met deze naam.", - "range_min_max": "Minimum moet lager zijn dan maximum." + "input_range_min_max": "Minimum moet lager zijn dan maximum.", + "output_range_min_max": "Minimum moet lager zijn dan maximum." } }, "options": { @@ -26,8 +30,10 @@ "description": "Wijzig de sensor en het bereik dat door de PID-regelaar wordt gebruikt.", "data": { "sensor_entity_id": "Sensor naam", - "range_min": "Minimum bereik", - "range_max": "Maximum bereik" + "input_range_min": "Minimum Input Bereik", + "input_range_max": "Maximum Input Bereik", + "output_range_min": "Minimum Output Bereik", + "output_range_max": "Maximum Output Bereik" } } }, diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 08f593f..1d28955 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -7,10 +7,14 @@ DOMAIN, CONF_NAME, CONF_SENSOR_ENTITY_ID, - CONF_RANGE_MIN, - CONF_RANGE_MAX, - DEFAULT_RANGE_MIN, - DEFAULT_RANGE_MAX, + CONF_INPUT_RANGE_MIN, + CONF_INPUT_RANGE_MAX, + CONF_OUTPUT_RANGE_MIN, + CONF_OUTPUT_RANGE_MAX, + DEFAULT_INPUT_RANGE_MIN, + DEFAULT_INPUT_RANGE_MAX, + DEFAULT_OUTPUT_RANGE_MIN, + DEFAULT_OUTPUT_RANGE_MAX, ) from custom_components.simple_pid_controller.config_flow import ( PIDControllerFlowHandler, @@ -28,15 +32,19 @@ { CONF_NAME: "My PID", CONF_SENSOR_ENTITY_ID: SENSOR_ENTITY, - CONF_RANGE_MIN: DEFAULT_RANGE_MIN, - CONF_RANGE_MAX: DEFAULT_RANGE_MAX, + CONF_INPUT_RANGE_MIN: DEFAULT_INPUT_RANGE_MIN, + CONF_INPUT_RANGE_MAX: DEFAULT_INPUT_RANGE_MAX, + CONF_OUTPUT_RANGE_MIN: DEFAULT_OUTPUT_RANGE_MIN, + CONF_OUTPUT_RANGE_MAX: DEFAULT_OUTPUT_RANGE_MAX, }, FlowResultType.CREATE_ENTRY, { CONF_NAME: "My PID", CONF_SENSOR_ENTITY_ID: SENSOR_ENTITY, - CONF_RANGE_MIN: DEFAULT_RANGE_MIN, - CONF_RANGE_MAX: DEFAULT_RANGE_MAX, + CONF_INPUT_RANGE_MIN: DEFAULT_INPUT_RANGE_MIN, + CONF_INPUT_RANGE_MAX: DEFAULT_INPUT_RANGE_MAX, + CONF_OUTPUT_RANGE_MIN: DEFAULT_OUTPUT_RANGE_MIN, + CONF_OUTPUT_RANGE_MAX: DEFAULT_OUTPUT_RANGE_MAX, }, None, ), @@ -45,15 +53,19 @@ { CONF_NAME: "My PID 2", CONF_SENSOR_ENTITY_ID: SENSOR_ENTITY, - CONF_RANGE_MIN: 1.0, - CONF_RANGE_MAX: 10.0, + CONF_INPUT_RANGE_MIN: 1.0, + CONF_INPUT_RANGE_MAX: 10.0, + CONF_OUTPUT_RANGE_MIN: 1.0, + CONF_OUTPUT_RANGE_MAX: 10.0, }, FlowResultType.CREATE_ENTRY, { CONF_NAME: "My PID 2", CONF_SENSOR_ENTITY_ID: SENSOR_ENTITY, - CONF_RANGE_MIN: 1.0, - CONF_RANGE_MAX: 10.0, + CONF_INPUT_RANGE_MIN: 1.0, + CONF_INPUT_RANGE_MAX: 10.0, + CONF_OUTPUT_RANGE_MIN: 1.0, + CONF_OUTPUT_RANGE_MAX: 10.0, }, None, ), @@ -62,12 +74,28 @@ { CONF_NAME: "Bad PID", CONF_SENSOR_ENTITY_ID: SENSOR_ENTITY, - CONF_RANGE_MIN: 10.0, - CONF_RANGE_MAX: 5.0, + CONF_INPUT_RANGE_MIN: 10.0, + CONF_INPUT_RANGE_MAX: 5.0, + CONF_OUTPUT_RANGE_MIN: 1.0, + CONF_OUTPUT_RANGE_MAX: 10.0, }, FlowResultType.FORM, None, - {"base": "range_min_max"}, + {"base": "input_range_min_max"}, + ), + # Invalid ranges (min >= max) + ( + { + CONF_NAME: "Bad PID", + CONF_SENSOR_ENTITY_ID: SENSOR_ENTITY, + CONF_INPUT_RANGE_MIN: 1.0, + CONF_INPUT_RANGE_MAX: 10.0, + CONF_OUTPUT_RANGE_MIN: 10.0, + CONF_OUTPUT_RANGE_MAX: 5.0, + }, + FlowResultType.FORM, + None, + {"base": "output_range_min_max"}, ), ], ) @@ -110,18 +138,32 @@ def test_async_get_options_flow(): ( { CONF_SENSOR_ENTITY_ID: "sensor.new", - CONF_RANGE_MIN: 1.0, - CONF_RANGE_MAX: 10.0, + CONF_INPUT_RANGE_MIN: 1.0, + CONF_INPUT_RANGE_MAX: 10.0, + CONF_OUTPUT_RANGE_MIN: 1.0, + CONF_OUTPUT_RANGE_MAX: 10.0, }, None, ), ( { CONF_SENSOR_ENTITY_ID: "sensor.new", - CONF_RANGE_MIN: 10.0, - CONF_RANGE_MAX: 5.0, + CONF_INPUT_RANGE_MIN: 10.0, + CONF_INPUT_RANGE_MAX: 5.0, + CONF_OUTPUT_RANGE_MIN: 1.0, + CONF_OUTPUT_RANGE_MAX: 10.0, + }, + {"base": "input_range_min_max"}, + ), + ( + { + CONF_SENSOR_ENTITY_ID: "sensor.new", + CONF_INPUT_RANGE_MIN: 1.0, + CONF_INPUT_RANGE_MAX: 10.0, + CONF_OUTPUT_RANGE_MIN: 10.0, + CONF_OUTPUT_RANGE_MAX: 5.0, }, - {"base": "range_min_max"}, + {"base": "output_range_min_max"}, ), ], ) @@ -150,8 +192,10 @@ async def test_user_flow_duplicate_abort(hass): user_input = { CONF_NAME: "Duplicate PID", CONF_SENSOR_ENTITY_ID: SENSOR_ENTITY, - CONF_RANGE_MIN: DEFAULT_RANGE_MIN, - CONF_RANGE_MAX: DEFAULT_RANGE_MAX, + CONF_INPUT_RANGE_MIN: DEFAULT_INPUT_RANGE_MIN, + CONF_INPUT_RANGE_MAX: DEFAULT_INPUT_RANGE_MAX, + CONF_OUTPUT_RANGE_MIN: DEFAULT_OUTPUT_RANGE_MIN, + CONF_OUTPUT_RANGE_MAX: DEFAULT_OUTPUT_RANGE_MAX, } # Create initial entry diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index 8bf9255..e0a9088 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -5,8 +5,10 @@ DOMAIN, CONF_SENSOR_ENTITY_ID, CONF_NAME, - DEFAULT_RANGE_MIN, - DEFAULT_RANGE_MAX, + DEFAULT_INPUT_RANGE_MIN, + DEFAULT_INPUT_RANGE_MAX, + DEFAULT_OUTPUT_RANGE_MIN, + DEFAULT_OUTPUT_RANGE_MAX, ) @@ -28,5 +30,7 @@ async def test_config_entry_diagnostics(hass, config_entry): data = result.get("data") assert data["name"] == config_entry.data[CONF_NAME] assert data["sensor_entity_id"] == config_entry.data[CONF_SENSOR_ENTITY_ID] - assert data["range_min"] == DEFAULT_RANGE_MIN - assert data["range_max"] == DEFAULT_RANGE_MAX + assert data["input_range_min"] == DEFAULT_INPUT_RANGE_MIN + assert data["input_range_max"] == DEFAULT_INPUT_RANGE_MAX + assert data["output_range_min"] == DEFAULT_OUTPUT_RANGE_MIN + assert data["output_range_max"] == DEFAULT_OUTPUT_RANGE_MAX diff --git a/tests/test_number.py b/tests/test_number.py index 7b30581..fc1d058 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -7,8 +7,10 @@ ControlParameterNumber, ) from custom_components.simple_pid_controller.const import ( - DEFAULT_RANGE_MIN, - DEFAULT_RANGE_MAX, + DEFAULT_INPUT_RANGE_MIN, + DEFAULT_INPUT_RANGE_MAX, + DEFAULT_OUTPUT_RANGE_MIN, + DEFAULT_OUTPUT_RANGE_MAX, ) @@ -131,9 +133,15 @@ def fake_write_state(): assert write_calls, "async_write_ha_state was not called" -@pytest.mark.parametrize("invalid_key", ["invalid1", "invalid2"]) +@pytest.mark.parametrize( + "invalid_key, expected_min, expected_max", + [ + ("input_invalid", DEFAULT_INPUT_RANGE_MIN, DEFAULT_INPUT_RANGE_MAX), + ("output_invalid", DEFAULT_OUTPUT_RANGE_MIN, DEFAULT_OUTPUT_RANGE_MAX), + ], +) async def test_controlparameter_number_unexpected_key( - hass, config_entry, caplog, invalid_key + hass, config_entry, caplog, invalid_key, expected_min, expected_max ): """Test that ControlParameterNumber logs error and uses default range for unexpected key.""" desc = { @@ -146,6 +154,6 @@ async def test_controlparameter_number_unexpected_key( } caplog.set_level(logging.ERROR) num = ControlParameterNumber(hass, config_entry, desc) - assert f"Unexpected PID parameter key: {invalid_key}" in caplog.text - assert num._attr_native_min_value == DEFAULT_RANGE_MIN - assert num._attr_native_max_value == DEFAULT_RANGE_MAX + assert f"Unknown PID key '{invalid_key}'. Using default values:" in caplog.text + assert num._attr_native_min_value == expected_min + assert num._attr_native_max_value == expected_max