Add min/max/step to MQTT number (#50869)

This commit is contained in:
Shay Levy 2021-05-21 18:35:27 +03:00 committed by GitHub
parent 8c5c8ed153
commit 6e087039f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 176 additions and 11 deletions

View file

@ -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",

View file

@ -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."""

View file

@ -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
)