mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add min/max/step to MQTT number (#50869)
This commit is contained in:
parent
8c5c8ed153
commit
6e087039f4
@ -75,6 +75,8 @@ ABBREVIATIONS = {
|
|||||||
"json_attr": "json_attributes",
|
"json_attr": "json_attributes",
|
||||||
"json_attr_t": "json_attributes_topic",
|
"json_attr_t": "json_attributes_topic",
|
||||||
"json_attr_tpl": "json_attributes_template",
|
"json_attr_tpl": "json_attributes_template",
|
||||||
|
"max": "max",
|
||||||
|
"min": "min",
|
||||||
"max_mirs": "max_mireds",
|
"max_mirs": "max_mireds",
|
||||||
"min_mirs": "min_mireds",
|
"min_mirs": "min_mireds",
|
||||||
"max_temp": "max_temp",
|
"max_temp": "max_temp",
|
||||||
@ -170,6 +172,7 @@ ABBREVIATIONS = {
|
|||||||
"stat_t": "state_topic",
|
"stat_t": "state_topic",
|
||||||
"stat_tpl": "state_template",
|
"stat_tpl": "state_template",
|
||||||
"stat_val_tpl": "state_value_template",
|
"stat_val_tpl": "state_value_template",
|
||||||
|
"step": "step",
|
||||||
"stype": "subtype",
|
"stype": "subtype",
|
||||||
"sup_feat": "supported_features",
|
"sup_feat": "supported_features",
|
||||||
"sup_clrm": "supported_color_modes",
|
"sup_clrm": "supported_color_modes",
|
||||||
|
@ -5,7 +5,12 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import number
|
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.const import CONF_NAME, CONF_OPTIMISTIC
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_MIN = "min"
|
||||||
|
CONF_MAX = "max"
|
||||||
|
CONF_STEP = "step"
|
||||||
|
|
||||||
DEFAULT_NAME = "MQTT Number"
|
DEFAULT_NAME = "MQTT Number"
|
||||||
DEFAULT_OPTIMISTIC = False
|
DEFAULT_OPTIMISTIC = False
|
||||||
|
|
||||||
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
|
|
||||||
{
|
def validate_config(config):
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
"""Validate that the configuration is valid, throws if it isn't."""
|
||||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
if config.get(CONF_MIN) >= config.get(CONF_MAX):
|
||||||
}
|
raise vol.Invalid(f"'{CONF_MAX}'' must be > '{CONF_MIN}'")
|
||||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
|
||||||
|
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(
|
async def async_setup_platform(
|
||||||
@ -67,6 +93,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
|
|||||||
|
|
||||||
def __init__(self, config, config_entry, discovery_data):
|
def __init__(self, config, config_entry, discovery_data):
|
||||||
"""Initialize the MQTT Number."""
|
"""Initialize the MQTT Number."""
|
||||||
|
self._config = config
|
||||||
self._sub_state = None
|
self._sub_state = None
|
||||||
|
|
||||||
self._current_number = None
|
self._current_number = None
|
||||||
@ -89,12 +116,28 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
|
|||||||
"""Handle new MQTT messages."""
|
"""Handle new MQTT messages."""
|
||||||
try:
|
try:
|
||||||
if msg.payload.decode("utf-8").isnumeric():
|
if msg.payload.decode("utf-8").isnumeric():
|
||||||
self._current_number = int(msg.payload)
|
num_value = int(msg.payload)
|
||||||
else:
|
else:
|
||||||
self._current_number = float(msg.payload)
|
num_value = float(msg.payload)
|
||||||
self.async_write_ha_state()
|
|
||||||
except ValueError:
|
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:
|
if self._config.get(CONF_STATE_TOPIC) is None:
|
||||||
# Force into optimistic mode.
|
# Force into optimistic mode.
|
||||||
@ -118,6 +161,21 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
|
|||||||
if last_state:
|
if last_state:
|
||||||
self._current_number = last_state.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
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
"""Return the current value."""
|
"""Return the current value."""
|
||||||
|
@ -5,7 +5,11 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import number
|
from homeassistant.components import number
|
||||||
|
from homeassistant.components.mqtt.number import CONF_MAX, CONF_MIN
|
||||||
from homeassistant.components.number import (
|
from homeassistant.components.number import (
|
||||||
|
ATTR_MAX,
|
||||||
|
ATTR_MIN,
|
||||||
|
ATTR_STEP,
|
||||||
ATTR_VALUE,
|
ATTR_VALUE,
|
||||||
DOMAIN as NUMBER_DOMAIN,
|
DOMAIN as NUMBER_DOMAIN,
|
||||||
SERVICE_SET_VALUE,
|
SERVICE_SET_VALUE,
|
||||||
@ -357,3 +361,103 @@ async def test_entity_debug_info_message(hass, mqtt_mock):
|
|||||||
await help_test_entity_debug_info_message(
|
await help_test_entity_debug_info_message(
|
||||||
hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload=b"1"
|
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…
x
Reference in New Issue
Block a user