Service to change min/max for input_number?

Is it possible to adjust the min/max values of an input_number dynamically? Based on the input_number documentation, I don’t think it is but maybe someone knows of a sneaky way to update those values???

I am working on a configuration panel for OpenEVSE electric vehicle charger and depending on wether it is “L1” or “L2” mode the min/max charge current possible is 6-16 Amps or 10-80 Amps, respectively. I would rather not have to create an L1_charge_current and an L2_charge_current slider and hide/reveal which one depending on the service level.

I can’t think of a way, but perhaps you could create scripts and treat them like buttons, for example, a ‘min’ script would set it to 6 or 10, a ‘max’ script would set it to 16 or 80.

Other solution would be to have your slider from 1-100 and then do some math to convert to a 6-16 scale or 10-80 scale.

1 Like

That’s a good solution.

Thanks for that, scaling the input value is a good idea.

I’m actually trying to extend the built-in input_number.py as a custom component right now. It seems like it should be easy – famous last words.

You are going to have to be careful when changing ranges that the current set point is not outside the new range.

e.g. currently set to 80 and you change to a 6-16 range.

Quick and dirty custom component that allows min/max adjustments. Fully backwards compatible with the existing input_number. Seems to work great with Lovelace, I didn’t test it on State UI. It clamps the value to min/max as appropriate and refuses to set min>=max or max<=min.

To change min/max use:

Services

input_number.set_min
input_number.set_max

Data:

{
  "entity_id": "input_number.myinput",
  "value": 1
}

Place this into config/custom_components

input_number.py

"""
Component to offer a way to set a numeric value from a slider or text box.

For more details about this component, please refer to the documentation
at https://home-assistant.io/components/input_number/
"""
import logging

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
    ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE)
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'input_number'
ENTITY_ID_FORMAT = DOMAIN + '.{}'

CONF_INITIAL = 'initial'
CONF_MIN = 'min'
CONF_MAX = 'max'
CONF_STEP = 'step'

MODE_SLIDER = 'slider'
MODE_BOX = 'box'

ATTR_INITIAL = 'initial'
ATTR_VALUE = 'value'
ATTR_MIN = 'min'
ATTR_MAX = 'max'
ATTR_STEP = 'step'
ATTR_MODE = 'mode'

SERVICE_SET_VALUE = 'set_value'
SERVICE_SET_MIN = 'set_min'
SERVICE_SET_MAX = 'set_max'
SERVICE_INCREMENT = 'increment'
SERVICE_DECREMENT = 'decrement'

SERVICE_DEFAULT_SCHEMA = vol.Schema({
    vol.Optional(ATTR_ENTITY_ID): cv.entity_ids
})

SERVICE_SET_VALUE_SCHEMA = vol.Schema({
    vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
    vol.Required(ATTR_VALUE): vol.Coerce(float),
})

def _cv_input_number(cfg):
    """Configure validation helper for input number (voluptuous)."""
    minimum = cfg.get(CONF_MIN)
    maximum = cfg.get(CONF_MAX)
    if minimum >= maximum:
        raise vol.Invalid('Maximum ({}) is not greater than minimum ({})'
                          .format(minimum, maximum))
    state = cfg.get(CONF_INITIAL)
    if state is not None and (state < minimum or state > maximum):
        raise vol.Invalid('Initial value {} not in range {}-{}'
                          .format(state, minimum, maximum))
    return cfg


CONFIG_SCHEMA = vol.Schema({
    DOMAIN: cv.schema_with_slug_keys(
        vol.All({
            vol.Optional(CONF_NAME): cv.string,
            vol.Required(CONF_MIN): vol.Coerce(float),
            vol.Required(CONF_MAX): vol.Coerce(float),
            vol.Optional(CONF_INITIAL): vol.Coerce(float),
            vol.Optional(CONF_STEP, default=1):
                vol.All(vol.Coerce(float), vol.Range(min=1e-3)),
            vol.Optional(CONF_ICON): cv.icon,
            vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
            vol.Optional(CONF_MODE, default=MODE_SLIDER):
                vol.In([MODE_BOX, MODE_SLIDER]),
        }, _cv_input_number)
    )
}, required=True, extra=vol.ALLOW_EXTRA)


async def async_setup(hass, config):
    """Set up an input slider."""
    component = EntityComponent(_LOGGER, DOMAIN, hass)

    entities = []

    for object_id, cfg in config[DOMAIN].items():
        name = cfg.get(CONF_NAME)
        minimum = cfg.get(CONF_MIN)
        maximum = cfg.get(CONF_MAX)
        initial = cfg.get(CONF_INITIAL)
        step = cfg.get(CONF_STEP)
        icon = cfg.get(CONF_ICON)
        unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
        mode = cfg.get(CONF_MODE)

        entities.append(InputNumber(
            object_id, name, initial, minimum, maximum, step, icon, unit,
            mode))

    if not entities:
        return False

    component.async_register_entity_service(
        SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA,
        'async_set_value'
    )

    component.async_register_entity_service(
        SERVICE_SET_MIN, SERVICE_SET_VALUE_SCHEMA,
        'async_set_min'
    )

    component.async_register_entity_service(
        SERVICE_SET_MAX, SERVICE_SET_VALUE_SCHEMA,
        'async_set_max'
    )

    component.async_register_entity_service(
        SERVICE_INCREMENT, SERVICE_DEFAULT_SCHEMA,
        'async_increment'
    )

    component.async_register_entity_service(
        SERVICE_DECREMENT, SERVICE_DEFAULT_SCHEMA,
        'async_decrement'
    )

    await component.async_add_entities(entities)
    return True


class InputNumber(RestoreEntity):
    """Representation of a slider."""

    def __init__(self, object_id, name, initial, minimum, maximum, step, icon,
                 unit, mode):
        """Initialize an input number."""
        self.entity_id = ENTITY_ID_FORMAT.format(object_id)
        self._name = name
        self._current_value = initial
        self._initial = initial
        self._minimum = minimum
        self._maximum = maximum
        self._step = step
        self._icon = icon
        self._unit = unit
        self._mode = mode

    @property
    def should_poll(self):
        """If entity should be polled."""
        return False

    @property
    def name(self):
        """Return the name of the input slider."""
        return self._name

    @property
    def icon(self):
        """Return the icon to be used for this entity."""
        return self._icon

    @property
    def state(self):
        """Return the state of the component."""
        return self._current_value

    @property
    def unit_of_measurement(self):
        """Return the unit the value is expressed in."""
        return self._unit

    @property
    def state_attributes(self):
        """Return the state attributes."""
        return {
            ATTR_INITIAL: self._initial,
            ATTR_MIN: self._minimum,
            ATTR_MAX: self._maximum,
            ATTR_STEP: self._step,
            ATTR_MODE: self._mode,
        }

    async def async_added_to_hass(self):
        """Run when entity about to be added to hass."""
        await super().async_added_to_hass()
        if self._current_value is not None:
            return

        state = await self.async_get_last_state()
        value = state and float(state.state)

        # Check against None because value can be 0
        if value is not None and self._minimum <= value <= self._maximum:
            self._current_value = value
        else:
            self._current_value = self._minimum

    async def async_set_value(self, value):
        """Set new value."""
        num_value = float(value)
        if num_value < self._minimum or num_value > self._maximum:
            _LOGGER.warning("Invalid value: %s (range %s - %s)",
                            num_value, self._minimum, self._maximum)
            return
        self._current_value = num_value
        await self.async_update_ha_state()

    async def async_set_min(self, value):
        """Set new min value."""
        minimum = float(value)
        if minimum >= self._maximum:
            _LOGGER.warning("Invalid minimum: %s (must be lower than maximum %s)",
                            minimum, self._maximum)
            return
        # Adjust value to minimim if it is less than the new minimum
        if minimum > self._current_value:
            self._current_value = minimum
        self._minimum = minimum
        await self.async_update_ha_state()

    async def async_set_max(self, value):
        """Set new max value."""
        maximum = float(value)
        if self._minimum >= maximum:
            _LOGGER.warning("Invalid maximum: %s (must be higher than minimum %s)",
                            maximum, self._minimum)
            return
        if maximum < self._current_value:
            self._current_value = maximum
        self._maximum = maximum
        await self.async_update_ha_state()

    async def async_increment(self):
        """Increment value."""
        new_value = self._current_value + self._step
        if new_value > self._maximum:
            _LOGGER.warning("Invalid value: %s (range %s - %s)",
                            new_value, self._minimum, self._maximum)
            return
        self._current_value = new_value
        await self.async_update_ha_state()

    async def async_decrement(self):
        """Decrement value."""
        new_value = self._current_value - self._step
        if new_value < self._minimum:
            _LOGGER.warning("Invalid value: %s (range %s - %s)",
                            new_value, self._minimum, self._maximum)
            return
        self._current_value = new_value
        await self.async_update_ha_state()
1 Like

Updates here as I make improvements: input_number.py.

Not sure if this sort of change would be accepted as a PR, but I assume I’ll need to update documentation and tests before making a Pull Request.

Did you get anywhere with getting this merged? I have need for it as well, though I’ll just use your custom_component for now :slight_smile:

Oops, I kinda forgot about it. Never submitted it. The last few months it has been working OK, I haven’t needed to make any edits.

I will take a look at how to submit it as a PR.

No problem, it’s working great for me too (thanks!), just a bit worried in case they make a change down the line that breaks it I suppose. Having it officially supported would help with that I guess.

@a3a Is it just me or did this break in 0.92?

Yes and no… 0.92 doesn’t really break the custom-component per se, since there is not a code change needed, but it does change where the code belongs and what it is named in order for it to be loaded. See https://developers.home-assistant.io/blog/2019/04/12/new-integration-structure.html

I will have to rename and relocate the custom component.

As an aside, I am working on getting my build environment set up so I can build tests and then submit a pull-request to (hopefully) integrate this custom component into core.

Hmm, do they just mean /custom_components/input_number/input_number.py (and the init file)?

The new location is custom_components/input_number/__init__.py. I’ve updated my github repo with the required changes: input_number. You need to copy all of the files in the folder: __init__.py, manifest.json, and services.yaml. This approach does make it easier to specify documentation and other metadata, so it’s a good change… just takes a bit of time to update everything to work in the new world.

Gotcha, thanks :slight_smile: