[homeassistant] Use input_number.set_value for input_number entities (#19965)

This commit is contained in:
Jesse Hills
2026-10-05 08:56:34 +13:00
committed by GitHub
parent aad7907c37
commit 4f539d3391
9 changed files with 137 additions and 4 deletions
@@ -7,19 +7,23 @@ from .. import (
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
homeassistant_ns,
setup_home_assistant_entity,
validate_entity_domain,
)
CODEOWNERS = ["@landonr"]
DEPENDENCIES = ["api"]
SUPPORTED_DOMAINS = ["input_number", "number"]
HomeassistantNumber = homeassistant_ns.class_(
"HomeassistantNumber", number.Number, cg.Component
)
CONFIG_SCHEMA = (
CONFIG_SCHEMA = cv.All(
number.number_schema(HomeassistantNumber)
.extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
validate_entity_domain("number", SUPPORTED_DOMAINS),
)
@@ -1,5 +1,7 @@
#include "homeassistant_number.h"
#include <cstring>
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
@@ -80,12 +82,19 @@ void HomeassistantNumber::control(float value) {
this->publish_state(value);
static constexpr auto SERVICE_NAME = StringRef::from_lit("number.set_value");
static constexpr auto NUMBER_SERVICE_NAME = StringRef::from_lit("number.set_value");
static constexpr auto INPUT_NUMBER_SERVICE_NAME = StringRef::from_lit("input_number.set_value");
static constexpr char INPUT_PREFIX[] = "input_";
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
api::HomeassistantActionRequest resp;
resp.service = SERVICE_NAME;
// input_number entities have their own set_value action
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = INPUT_NUMBER_SERVICE_NAME;
} else {
resp.service = NUMBER_SERVICE_NAME;
}
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
@@ -0,0 +1,32 @@
"""Tests for the homeassistant number platform."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.config import read_config
from esphome.core import CORE
VALID_CONFIG = "tests/component_tests/homeassistant/test_number.yaml"
INVALID_CONFIG = "tests/component_tests/homeassistant/test_number_invalid_domain.yaml"
def test_number_and_input_number_entities(
generate_main: Callable[[str | Path], str],
) -> None:
"""Both number and input_number entity IDs are accepted and passed to the C++ class."""
main_cpp = generate_main(VALID_CONFIG)
assert 'ha_number->set_entity_id("number.some_number");' in main_cpp
assert 'ha_input_number->set_entity_id("input_number.some_input");' in main_cpp
def test_number_rejects_unsupported_domain(
capsys: pytest.CaptureFixture[str],
) -> None:
"""An entity ID from another domain fails config validation."""
CORE.config_path = Path(INVALID_CONFIG)
assert read_config({}) is None
assert "is not supported by the number platform" in capsys.readouterr().out
@@ -0,0 +1,17 @@
---
esphome:
name: test
host:
logger:
api:
number:
- platform: homeassistant
entity_id: number.some_number
id: ha_number
- platform: homeassistant
entity_id: input_number.some_input
id: ha_input_number
@@ -0,0 +1,17 @@
---
esphome:
name: test
host:
logger:
api:
number:
- platform: homeassistant
entity_id: number.some_number
id: ha_number
- platform: homeassistant
entity_id: sensor.foo
id: ha_other_number
@@ -71,6 +71,9 @@ number:
- platform: homeassistant
entity_id: number.hello_world
id: ha_hello_world_number
- platform: homeassistant
entity_id: input_number.hello_world
id: ha_hello_world_input_number
sensor:
- platform: homeassistant
@@ -21,6 +21,12 @@ api:
- number.set:
id: ha_number
value: 42.5
- number.set:
id: ha_real_number
value: 12.5
- number.set:
id: ha_real_input_number
value: 7
- switch.turn_on: ha_switch
- switch.turn_off: ha_switch
- logger.log: "=== All tests completed ==="
@@ -129,6 +135,13 @@ text_sensor:
# Number component for testing HA number control
number:
# Real homeassistant numbers: the action used depends on the entity domain
- platform: homeassistant
id: ha_real_number
entity_id: number.test_real_number
- platform: homeassistant
id: ha_real_input_number
entity_id: input_number.test_real_input_number
- platform: template
name: "HA Controlled Number"
id: ha_number
@@ -86,6 +86,15 @@ async def test_api_homeassistant(
"nonexistent.action_for_error_test": loop.create_future(), # error_test_call
}
# Futures keyed by (service, entity_id) for the real homeassistant numbers
ha_real_number_futures = {
("number.set_value", "number.test_real_number"): loop.create_future(),
(
"input_number.set_value",
"input_number.test_real_input_number",
): loop.create_future(),
}
# Future for error message test
action_error_received_future = loop.create_future()
@@ -102,6 +111,10 @@ async def test_api_homeassistant(
if not future.done():
future.set_result(service_call)
key = (service_call.service, service_call.data.get("entity_id"))
if key in ha_real_number_futures and not ha_real_number_futures[key].done():
ha_real_number_futures[key].set_result(service_call)
# Immediately respond to the error test call so the test can proceed
# This needs to happen synchronously so ESPHome receives the response
# before logging "=== All tests completed ==="
@@ -201,6 +214,16 @@ async def test_api_homeassistant(
# Empty state with valid entity should work (use different entity to not interfere with test)
client.send_home_assistant_state("sensor.edge_case_empty_state", "", "")
# The real homeassistant numbers reject values outside min/max, which
# default to 0 until Home Assistant provides them
for entity in (
"number.test_real_number",
"input_number.test_real_input_number",
):
client.send_home_assistant_state(entity, "min", "0")
client.send_home_assistant_state(entity, "max", "100")
client.send_home_assistant_state(entity, "step", "0.5")
# List entities and services
_, services = await client.list_entities_services()
@@ -334,6 +357,21 @@ async def test_api_homeassistant(
# The value might be formatted with trailing zeros
assert float(number_call.data["value"]) == 42.5
# 7b. Real homeassistant numbers pick the action from the entity domain
real_number_call = await asyncio.wait_for(
ha_real_number_futures[("number.set_value", "number.test_real_number")],
timeout=2.0,
)
assert float(real_number_call.data["value"]) == 12.5
real_input_number_call = await asyncio.wait_for(
ha_real_number_futures[
("input_number.set_value", "input_number.test_real_input_number")
],
timeout=2.0,
)
assert float(real_input_number_call.data["value"]) == 7
# 8. HA Switch service calls
switch_on_call = await asyncio.wait_for(
service_call_futures["switch.turn_on"], timeout=2.0