mirror of
https://github.com/esphome/esphome.git
synced 2026-09-10 23:07:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9442956f54 | ||
|
|
a952e7236b | ||
|
|
b3f0382d70 |
@@ -30,10 +30,53 @@ using modbus::ModbusFunctionCode;
|
||||
using modbus::ModbusRegisterType;
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
// Span overloads of the former modbus_controller helpers: read lambdas receive their payload as a
|
||||
// Remove before 2026.10.0 — these helpers have moved to modbus::helpers
|
||||
ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); }
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline FunctionCode modbus_register_read_function(modbus::EntityType reg_type) {
|
||||
return modbus::helpers::modbus_register_read_function(reg_type);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline FunctionCode modbus_register_write_function(modbus::EntityType reg_type) {
|
||||
return modbus::helpers::modbus_register_write_function(reg_type);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); }
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::byte_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::word_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::dword_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::qword_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
|
||||
return modbus::helpers::get_data<T>(data, buffer_offset);
|
||||
}
|
||||
|
||||
// Span overloads of the deprecated helpers below: read lambdas receive their payload as a
|
||||
// std::span<const uint8_t> (previously a const std::vector<uint8_t> &), and a span does not convert to
|
||||
// a vector, so existing lambdas calling these by name need an overload that accepts one. These carry
|
||||
// the 2026.8.0 deprecation window, since the span forms only exist from it.
|
||||
// this release's deprecation window, since the span forms only exist from it.
|
||||
// payload_to_number() deliberately has no such overload: one of its arguments is a modbus::helpers
|
||||
// type, so a span call already reaches the helper by argument-dependent lookup, and a forwarder here
|
||||
// would only make that call ambiguous.
|
||||
@@ -56,6 +99,33 @@ inline bool coil_from_vector(int coil, std::span<const uint8_t> data) {
|
||||
return modbus::helpers::bit_from_packed(coil, data);
|
||||
}
|
||||
|
||||
template<typename N>
|
||||
ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
N mask_and_shift_by_rightbit(N data, uint32_t mask) {
|
||||
return modbus::helpers::mask_and_shift_by_rightbit(data, mask);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
|
||||
modbus::helpers::number_to_payload(data, value, value_type);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
|
||||
uint32_t bitmask) {
|
||||
return modbus::helpers::payload_to_number(std::span<const uint8_t>(data), sensor_value_type, offset, bitmask)
|
||||
.value_or(0);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
|
||||
std::vector<uint16_t> data;
|
||||
modbus::helpers::float_to_payload(data, value, value_type);
|
||||
return data;
|
||||
}
|
||||
|
||||
class ModbusController;
|
||||
|
||||
/// How an item relates to the register range built just before it (same register type, address order).
|
||||
/// The numeric order doubles as the comparator tiebreak for items at the same address (see
|
||||
/// SensorItemsComparator): AUTO items form the shared range first, so a NEVER item comes last and
|
||||
|
||||
@@ -23,7 +23,6 @@ void ModbusNumber::parse_and_publish(std::span<const uint8_t> data) {
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Number new state : %.02f", result);
|
||||
// this->sensor_->raw_state = result;
|
||||
this->publish_state(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ void ModbusSensor::parse_and_publish(std::span<const uint8_t> data) {
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Sensor new state: %.02f", result);
|
||||
// this->sensor_->raw_state = result;
|
||||
this->publish_state(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,7 @@ const LogString *state_class_to_string(StateClass state_class) {
|
||||
return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0);
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
Sensor::Sensor() : state(NAN), raw_state(NAN) {}
|
||||
#pragma GCC diagnostic pop
|
||||
Sensor::Sensor() : state(NAN) {}
|
||||
|
||||
int8_t Sensor::get_accuracy_decimals() {
|
||||
if (this->sensor_flags_.has_accuracy_override)
|
||||
@@ -66,11 +63,8 @@ StateClass Sensor::get_state_class() {
|
||||
}
|
||||
|
||||
void Sensor::publish_state(float state) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
this->raw_state = state;
|
||||
#pragma GCC diagnostic pop
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
this->raw_state_ = state;
|
||||
this->raw_callback_.call(state);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -96,18 +96,20 @@ class Sensor : public EntityBase {
|
||||
|
||||
/// Getter-syntax for .state.
|
||||
float get_state() const { return this->state; }
|
||||
/// Getter-syntax for .raw_state
|
||||
/// Get the last state received by publish_state(), before any filters were applied.
|
||||
float get_raw_state() const {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return this->raw_state;
|
||||
#pragma GCC diagnostic pop
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
return this->raw_state_;
|
||||
#else
|
||||
return this->state; // No filters compiled in, raw == filtered
|
||||
#endif
|
||||
}
|
||||
|
||||
/** Publish a new state to the front-end.
|
||||
*
|
||||
* First, the new state will be assigned to the raw_value. Then it's passed through all filters
|
||||
* until it finally lands in the .value member variable and a callback is issued.
|
||||
* The value is passed through the filter chain (when filters are compiled in) before landing in
|
||||
* the `state` member and triggering the state callback. The pre-filter value is available via
|
||||
* get_raw_state().
|
||||
*
|
||||
* @param state The state as a floating point number.
|
||||
*/
|
||||
@@ -137,17 +139,11 @@ class Sensor : public EntityBase {
|
||||
*/
|
||||
float state;
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
/// @deprecated Use get_raw_state() instead. This member will be removed in ESPHome 2026.10.0.
|
||||
ESPDEPRECATED("Use get_raw_state() instead of .raw_state. Will be removed in 2026.10.0", "2026.4.0")
|
||||
float raw_state;
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
void internal_send_state_to_frontend(float state);
|
||||
|
||||
protected:
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
float raw_state_{NAN}; ///< The last state passed to publish_state(), before filters.
|
||||
LazyCallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
|
||||
#endif
|
||||
LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
esphome:
|
||||
name: test-sensor-raw-state
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Filters are compiled in for this config (USE_SENSOR_FILTER), so raw storage exists
|
||||
sensor:
|
||||
# No filters on this sensor: get_raw_state() must equal state
|
||||
- platform: template
|
||||
name: "No Filter Sensor"
|
||||
id: no_filter_sensor
|
||||
accuracy_decimals: 1
|
||||
|
||||
# Filtered sensor: get_raw_state() must be the pre-filter value
|
||||
- platform: template
|
||||
name: "With Filter Sensor"
|
||||
id: with_filter_sensor
|
||||
accuracy_decimals: 1
|
||||
filters:
|
||||
- multiply: 2.0
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test No Filter Button"
|
||||
id: test_no_filter_button
|
||||
on_press:
|
||||
- sensor.template.publish:
|
||||
id: no_filter_sensor
|
||||
state: 21.5
|
||||
- delay: 50ms
|
||||
- logger.log:
|
||||
format: "NO_FILTER: state=%.1f raw_state=%.1f"
|
||||
args:
|
||||
- id(no_filter_sensor).state
|
||||
- id(no_filter_sensor).get_raw_state()
|
||||
|
||||
- platform: template
|
||||
name: "Test With Filter Button"
|
||||
id: test_with_filter_button
|
||||
on_press:
|
||||
- sensor.template.publish:
|
||||
id: with_filter_sensor
|
||||
state: 21.5
|
||||
- delay: 50ms
|
||||
- logger.log:
|
||||
format: "WITH_FILTER: state=%.1f raw_state=%.1f"
|
||||
args:
|
||||
- id(with_filter_sensor).state
|
||||
- id(with_filter_sensor).get_raw_state()
|
||||
@@ -0,0 +1,31 @@
|
||||
esphome:
|
||||
name: test-sensor-raw-state-no-filter
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# No sensor in this config has filters, so USE_SENSOR_FILTER is not defined and
|
||||
# get_raw_state() falls back to state
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "No Filter Sensor"
|
||||
id: no_filter_sensor
|
||||
accuracy_decimals: 1
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test No Filter Button"
|
||||
id: test_no_filter_button
|
||||
on_press:
|
||||
- sensor.template.publish:
|
||||
id: no_filter_sensor
|
||||
state: 21.5
|
||||
- delay: 50ms
|
||||
- logger.log:
|
||||
format: "NO_FILTER: state=%.1f raw_state=%.1f"
|
||||
args:
|
||||
- id(no_filter_sensor).state
|
||||
- id(no_filter_sensor).get_raw_state()
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Integration tests for Sensor::get_raw_state().
|
||||
|
||||
Raw state storage only exists when filters are compiled in (USE_SENSOR_FILTER).
|
||||
Without it, get_raw_state() returns state, so both build configurations are covered:
|
||||
one fixture with a filtered sensor and one with no filters at all.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from aioesphomeapi import APIClient, EntityInfo
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
NO_FILTER_PATTERN = re.compile(r"NO_FILTER: state=([\d.]+) raw_state=([\d.]+)")
|
||||
WITH_FILTER_PATTERN = re.compile(r"WITH_FILTER: state=([\d.]+) raw_state=([\d.]+)")
|
||||
|
||||
|
||||
async def _press_and_read(
|
||||
client: APIClient,
|
||||
entities: list[EntityInfo],
|
||||
button_object_id: str,
|
||||
future: asyncio.Future[tuple[float, float]],
|
||||
label: str,
|
||||
) -> tuple[float, float]:
|
||||
button = next(
|
||||
(e for e in entities if button_object_id in e.object_id.lower()), None
|
||||
)
|
||||
assert button is not None, f"{button_object_id} not found"
|
||||
client.button_command(button.key)
|
||||
try:
|
||||
return await asyncio.wait_for(future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Timeout waiting for {label} log message")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_raw_state(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""With filters compiled in, raw state is stored separately from state."""
|
||||
loop = asyncio.get_running_loop()
|
||||
no_filter_future: asyncio.Future[tuple[float, float]] = loop.create_future()
|
||||
with_filter_future: asyncio.Future[tuple[float, float]] = loop.create_future()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
if not no_filter_future.done() and (match := NO_FILTER_PATTERN.search(line)):
|
||||
no_filter_future.set_result((float(match.group(1)), float(match.group(2))))
|
||||
if not with_filter_future.done() and (
|
||||
match := WITH_FILTER_PATTERN.search(line)
|
||||
):
|
||||
with_filter_future.set_result(
|
||||
(float(match.group(1)), float(match.group(2)))
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
state, raw_state = await _press_and_read(
|
||||
client, entities, "test_no_filter_button", no_filter_future, "NO_FILTER"
|
||||
)
|
||||
assert state == 21.5
|
||||
assert raw_state == 21.5
|
||||
|
||||
state, raw_state = await _press_and_read(
|
||||
client,
|
||||
entities,
|
||||
"test_with_filter_button",
|
||||
with_filter_future,
|
||||
"WITH_FILTER",
|
||||
)
|
||||
assert state == 43.0
|
||||
assert raw_state == 21.5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_raw_state_no_filter(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Without filters compiled in, get_raw_state() returns state."""
|
||||
loop = asyncio.get_running_loop()
|
||||
no_filter_future: asyncio.Future[tuple[float, float]] = loop.create_future()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
if not no_filter_future.done() and (match := NO_FILTER_PATTERN.search(line)):
|
||||
no_filter_future.set_result((float(match.group(1)), float(match.group(2))))
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
state, raw_state = await _press_and_read(
|
||||
client, entities, "test_no_filter_button", no_filter_future, "NO_FILTER"
|
||||
)
|
||||
assert state == 21.5
|
||||
assert raw_state == 21.5
|
||||
Reference in New Issue
Block a user