Add min/max/step to MQTT number (#50869)
This commit is contained in:
parent
8c5c8ed153
commit
6e087039f4
3 changed files with 176 additions and 11 deletions
|
@ -75,6 +75,8 @@ ABBREVIATIONS = {
|
|||
"json_attr": "json_attributes",
|
||||
"json_attr_t": "json_attributes_topic",
|
||||
"json_attr_tpl": "json_attributes_template",
|
||||
"max": "max",
|
||||
"min": "min",
|
||||
"max_mirs": "max_mireds",
|
||||
"min_mirs": "min_mireds",
|
||||
"max_temp": "max_temp",
|
||||
|
@ -170,6 +172,7 @@ ABBREVIATIONS = {
|
|||
"stat_t": "state_topic",
|
||||
"stat_tpl": "state_template",
|
||||
"stat_val_tpl": "state_value_template",
|
||||
"step": "step",
|
||||
"stype": "subtype",
|
||||
"sup_feat": "supported_features",
|
||||
"sup_clrm": "supported_color_modes",
|
||||
|
|
|
@ -5,7 +5,12 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import number
|
||||
from homeassistant.components.number import NumberEntity
|
||||
from homeassistant.components.number import (
|
||||
DEFAULT_MAX_VALUE,
|
||||
DEFAULT_MIN_VALUE,
|
||||
DEFAULT_STEP,
|
||||
NumberEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
@ -28,15 +33,36 @@ from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_hel
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_MIN = "min"
|
||||
CONF_MAX = "max"
|
||||
CONF_STEP = "step"
|
||||
|
||||
DEFAULT_NAME = "MQTT Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
def validate_config(config):
|
||||
"""Validate that the configuration is valid, throws if it isn't."""
|
||||
if config.get(CONF_MIN) >= config.get(CONF_MAX):
|
||||
raise vol.Invalid(f"'{CONF_MAX}'' must be > '{CONF_MIN}'")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float),
|
||||
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=1e-3)
|
||||
),
|
||||
},
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema),
|
||||
validate_config,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
|
@ -67,6 +93,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
|
|||
|
||||
def __init__(self, config, config_entry, discovery_data):
|
||||
"""Initialize the MQTT Number."""
|
||||
self._config = config
|
||||
self._sub_state = None
|
||||
|
||||
self._current_number = None
|
||||
|
@ -89,12 +116,28 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
|
|||
"""Handle new MQTT messages."""
|
||||
try:
|
||||
if msg.payload.decode("utf-8").isnumeric():
|
||||
self._current_number = int(msg.payload)
|
||||
num_value = int(msg.payload)
|
||||
else:
|
||||
self._current_number = float(msg.payload)
|
||||
self.async_write_ha_state()
|
||||
num_value = float(msg.payload)
|
||||
except ValueError:
|
||||
_LOGGER.warning("We received <%s> which is not a Number", msg.payload)
|
||||
_LOGGER.warning(
|
||||
"Payload '%s' is not a Number",
|
||||
msg.payload.decode("utf-8", errors="ignore"),
|
||||
)
|
||||
return
|
||||
|
||||
if num_value < self.min_value or num_value > self.max_value:
|
||||
_LOGGER.error(
|
||||
"Invalid value for %s: %s (range %s - %s)",
|
||||
self.entity_id,
|
||||
num_value,
|
||||
self.min_value,
|
||||
self.max_value,
|
||||
)
|
||||
return
|
||||
|
||||
self._current_number = num_value
|
||||
self.async_write_ha_state()
|
||||
|
||||
if self._config.get(CONF_STATE_TOPIC) is None:
|
||||
# Force into optimistic mode.
|
||||
|
@ -118,6 +161,21 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
|
|||
if last_state:
|
||||
self._current_number = last_state.state
|
||||
|
||||
@property
|
||||
def min_value(self) -> float:
|
||||
"""Return the minimum value."""
|
||||
return self._config[CONF_MIN]
|
||||
|
||||
@property
|
||||
def max_value(self) -> float:
|
||||
"""Return the maximum value."""
|
||||
return self._config[CONF_MAX]
|
||||
|
||||
@property
|
||||
def step(self) -> float:
|
||||
"""Return the increment/decrement step."""
|
||||
return self._config[CONF_STEP]
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""Return the current value."""
|
||||
|
|
|
@ -5,7 +5,11 @@ from unittest.mock import patch
|
|||
import pytest
|
||||
|
||||
from homeassistant.components import number
|
||||
from homeassistant.components.mqtt.number import CONF_MAX, CONF_MIN
|
||||
from homeassistant.components.number import (
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_STEP,
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
|
@ -357,3 +361,103 @@ async def test_entity_debug_info_message(hass, mqtt_mock):
|
|||
await help_test_entity_debug_info_message(
|
||||
hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload=b"1"
|
||||
)
|
||||
|
||||
|
||||
async def test_min_max_step_attributes(hass, mqtt_mock):
|
||||
"""Test min/max/step attributes."""
|
||||
topic = "test/number"
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"number",
|
||||
{
|
||||
"number": {
|
||||
"platform": "mqtt",
|
||||
"state_topic": topic,
|
||||
"command_topic": topic,
|
||||
"name": "Test Number",
|
||||
"min": 5,
|
||||
"max": 110,
|
||||
"step": 20,
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("number.test_number")
|
||||
assert state.attributes.get(ATTR_MIN) == 5
|
||||
assert state.attributes.get(ATTR_MAX) == 110
|
||||
assert state.attributes.get(ATTR_STEP) == 20
|
||||
|
||||
|
||||
async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock):
|
||||
"""Test invalid min/max attributes."""
|
||||
topic = "test/number"
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"number",
|
||||
{
|
||||
"number": {
|
||||
"platform": "mqtt",
|
||||
"state_topic": topic,
|
||||
"command_topic": topic,
|
||||
"name": "Test Number",
|
||||
"min": 35,
|
||||
"max": 10,
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert f"'{CONF_MAX}'' must be > '{CONF_MIN}'" in caplog.text
|
||||
|
||||
|
||||
async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock):
|
||||
"""Test warning for MQTT payload which is not a number."""
|
||||
topic = "test/number"
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"number",
|
||||
{
|
||||
"number": {
|
||||
"platform": "mqtt",
|
||||
"state_topic": topic,
|
||||
"command_topic": topic,
|
||||
"name": "Test Number",
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_mqtt_message(hass, topic, "not_a_number")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Payload 'not_a_number' is not a Number" in caplog.text
|
||||
|
||||
|
||||
async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock):
|
||||
"""Test error when MQTT payload is out of min/max range."""
|
||||
topic = "test/number"
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"number",
|
||||
{
|
||||
"number": {
|
||||
"platform": "mqtt",
|
||||
"state_topic": topic,
|
||||
"command_topic": topic,
|
||||
"name": "Test Number",
|
||||
"min": 5,
|
||||
"max": 110,
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_mqtt_message(hass, topic, "115.5")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
"Invalid value for number.test_number: 115.5 (range 5.0 - 110.0)" in caplog.text
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue