mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 16:30:22 +00:00
Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter
This commit is contained in:
@@ -3,10 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
INTERRUPT_DEFINE = "USE_GPIO_BINARY_SENSOR_INTERRUPT"
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_basic_setup(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
@@ -69,3 +74,62 @@ def test_gpio_binary_sensor_explicit_polling_mode(
|
||||
)
|
||||
|
||||
assert "bs_polling->set_use_interrupt(false);" in main_cpp
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_interrupt_emits_define(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""
|
||||
An interrupt-mode sensor must emit the define that compiles the ISR code,
|
||||
since the platform ISR pin implementation is only built when needed
|
||||
"""
|
||||
generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml")
|
||||
|
||||
assert INTERRUPT_DEFINE in {d.name for d in CORE.defines}
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_polling_omits_define(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""
|
||||
A polling-only config must not emit the interrupt define, so the ISR code
|
||||
(and its reference to ISRInternalGPIOPin) is compiled out
|
||||
"""
|
||||
generate_main("tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml")
|
||||
|
||||
assert INTERRUPT_DEFINE not in {d.name for d in CORE.defines}
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_mixed_modes_emit_define(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""
|
||||
With one interrupt and one polling sensor, the define is emitted and the
|
||||
polling instance still opts out via its setter
|
||||
"""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/gpio/test_gpio_binary_sensor_mixed.yaml"
|
||||
)
|
||||
|
||||
assert INTERRUPT_DEFINE in {d.name for d in CORE.defines}
|
||||
assert "bs_polling->set_use_interrupt(false);" in main_cpp
|
||||
assert "bs_interrupt->set_use_interrupt" not in main_cpp
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_expander_pin_omits_define(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""
|
||||
An expander pin can't use interrupts: final validation falls back to
|
||||
polling and the interrupt define must not be emitted. This is the config
|
||||
that fails to link if the ISR code is compiled without an internal pin
|
||||
"""
|
||||
with caplog.at_level(logging.INFO):
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/gpio/test_gpio_binary_sensor_expander.yaml"
|
||||
)
|
||||
|
||||
assert "bs_expander->set_use_interrupt(false);" in main_cpp
|
||||
assert INTERRUPT_DEFINE not in {d.name for d in CORE.defines}
|
||||
assert "falling back to polling mode" in caplog.text
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
scl: 16
|
||||
sda: 17
|
||||
|
||||
ch422g:
|
||||
- id: ch422g_hub
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
name: "Expander Sensor"
|
||||
id: bs_expander
|
||||
pin:
|
||||
ch422g: ch422g_hub
|
||||
number: 1
|
||||
mode: INPUT
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin: 5
|
||||
name: "Interrupt Sensor"
|
||||
id: bs_interrupt
|
||||
|
||||
- platform: gpio
|
||||
pin: 4
|
||||
name: "Polling Sensor"
|
||||
id: bs_polling
|
||||
use_interrupt: false
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Schema-level config validation for custom_pdu and the deprecated custom_command alias.
|
||||
|
||||
custom_command took a raw frame with a leading device address byte; custom_pdu takes the PDU only.
|
||||
The old key is still accepted at the schema level and auto-migrated later in final validate (which a
|
||||
bare-schema test can't reach), so these tests only cover what the schema itself enforces: the two keys
|
||||
are mutually exclusive, and custom_pdu takes byte-sized values.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid, MultipleInvalid
|
||||
|
||||
from esphome.components.modbus_controller import ModbusItemBaseSchema
|
||||
from esphome.components.modbus_controller.const import (
|
||||
CONF_CUSTOM_COMMAND,
|
||||
CONF_CUSTOM_PDU,
|
||||
)
|
||||
|
||||
|
||||
def test_custom_command_accepted_at_schema_level() -> None:
|
||||
"""custom_command validates at the schema level; migration/rejection happens in final validate."""
|
||||
config = ModbusItemBaseSchema(
|
||||
{CONF_CUSTOM_COMMAND: [0x01, 0x03, 0x00, 0x2A, 0x00, 0x01]}
|
||||
)
|
||||
assert config[CONF_CUSTOM_COMMAND] == [0x01, 0x03, 0x00, 0x2A, 0x00, 0x01]
|
||||
|
||||
|
||||
def test_custom_pdu_and_custom_command_mutually_exclusive() -> None:
|
||||
"""Only one custom source may be given; supplying both is a schema error."""
|
||||
with pytest.raises((Invalid, MultipleInvalid)):
|
||||
ModbusItemBaseSchema(
|
||||
{
|
||||
CONF_CUSTOM_PDU: [0x03, 0x00, 0x2A, 0x00, 0x01],
|
||||
CONF_CUSTOM_COMMAND: [0x01, 0x03, 0x00, 0x2A, 0x00, 0x01],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_custom_pdu_accepted() -> None:
|
||||
"""The new key takes PDU bytes (function code + data, no address byte)."""
|
||||
config = ModbusItemBaseSchema({CONF_CUSTOM_PDU: [0x03, 0x00, 0x2A, 0x00, 0x01]})
|
||||
assert config[CONF_CUSTOM_PDU] == [0x03, 0x00, 0x2A, 0x00, 0x01]
|
||||
|
||||
|
||||
def test_custom_pdu_rejects_non_byte_values() -> None:
|
||||
"""PDU entries are bytes; a word-sized value is a sign the old raw format is being used."""
|
||||
with pytest.raises((Invalid, MultipleInvalid)):
|
||||
ModbusItemBaseSchema({CONF_CUSTOM_PDU: [0x0103, 0x002A]})
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
network:
|
||||
tcp_send_buffer: 32kB
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
tcp_send_buffer: 16384
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Tests for the ``network: tcp_send_buffer:`` option.
|
||||
|
||||
The option sets lwIP's per-socket TCP send buffer
|
||||
(CONFIG_LWIP_TCP_SND_BUF_DEFAULT) on ESP-IDF. The stock default (5744 bytes)
|
||||
stalls bursty senders such as a Bluetooth proxy streaming GATT notifications;
|
||||
until now the only way to raise it was the all-or-nothing
|
||||
``enable_high_performance`` bundle.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32.const import (
|
||||
KEY_SDKCONFIG_OPTIONS,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
)
|
||||
from esphome.components.network import (
|
||||
CONF_TCP_SEND_BUFFER,
|
||||
CONFIG_SCHEMA,
|
||||
TCP_SEND_BUFFER_MAX,
|
||||
TCP_SEND_BUFFER_MIN,
|
||||
)
|
||||
from esphome.const import KEY_ESP32, KEY_FRAMEWORK_VERSION, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _sdkconfig_option(name: str) -> int | None:
|
||||
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(name)
|
||||
|
||||
|
||||
def test_tcp_send_buffer_sets_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("tcp_send_buffer.yaml"))
|
||||
assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 32000
|
||||
|
||||
|
||||
def test_tcp_send_buffer_overrides_high_performance(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""An explicit size wins over the high performance bundle's 65534."""
|
||||
generate_main(component_config_path("tcp_send_buffer_high_perf.yaml"))
|
||||
assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 16384
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [TCP_SEND_BUFFER_MIN, TCP_SEND_BUFFER_MAX])
|
||||
def test_boundary_values_accepted(
|
||||
set_core_config: SetCoreConfigCallable, value: int
|
||||
) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
core_data={KEY_FRAMEWORK_VERSION: cv.Version(5, 5, 5)},
|
||||
platform_data={KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
assert CONFIG_SCHEMA({"tcp_send_buffer": value})[CONF_TCP_SEND_BUFFER] == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["1kB", "128kB"])
|
||||
def test_out_of_range_rejected(
|
||||
set_core_config: SetCoreConfigCallable, value: str
|
||||
) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
core_data={KEY_FRAMEWORK_VERSION: cv.Version(5, 5, 5)},
|
||||
platform_data={KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
with pytest.raises(Invalid):
|
||||
CONFIG_SCHEMA({"tcp_send_buffer": value})
|
||||
|
||||
|
||||
def test_rejected_on_esp8266(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP8266_ARDUINO,
|
||||
core_data={KEY_FRAMEWORK_VERSION: cv.Version(3, 1, 2)},
|
||||
)
|
||||
with pytest.raises(Invalid, match="esp32"):
|
||||
CONFIG_SCHEMA({"tcp_send_buffer": "32kB"})
|
||||
@@ -1,10 +0,0 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The scan list helper is header-only and needs none of the component's real
|
||||
# dependencies. Pulling them in breaks the host build: web_server_base
|
||||
# includes ESPAsyncWebServer.h and ota.web_server includes md5/md5.h, neither
|
||||
# of which exists there.
|
||||
manifest.dependencies = []
|
||||
manifest.auto_load = []
|
||||
@@ -108,6 +108,22 @@ select:
|
||||
return value;
|
||||
|
||||
sensor:
|
||||
# custom_pdu polls a ready-made PDU (function code + data - no device address byte, no CRC); covers
|
||||
# the set_custom_pdu codegen path and the custom-range polling constructor.
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller1
|
||||
id: modbus_sensor_custom_pdu
|
||||
name: Test Custom PDU Sensor
|
||||
custom_pdu: [0x03, 0x00, 0x2A, 0x00, 0x01]
|
||||
value_type: U_WORD
|
||||
# Deprecated custom_command (leading byte 0x02 == modbus_controller1's address) drives the
|
||||
# migrate_custom_command final-validate auto-migration path in CI.
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller1
|
||||
id: modbus_sensor_custom_command
|
||||
name: Test Custom Command Sensor
|
||||
custom_command: [0x02, 0x03, 0x00, 0x2B, 0x00, 0x01]
|
||||
value_type: U_WORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller1
|
||||
id: modbus_sensor1
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
tcp_send_buffer: 32kB
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
network:
|
||||
|
||||
api:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
# Angle-bracket name pins the explicit-timezone host codegen path
|
||||
# (setenv/tzset plus pre-parsed struct emission) with characters that
|
||||
# would break unescaped string interpolation.
|
||||
timezone: "<+07>-7"
|
||||
+6
-6
@@ -4,13 +4,13 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/captive_portal/scan_list.h"
|
||||
#include "esphome/components/wifi/scan_list.h"
|
||||
|
||||
namespace esphome::captive_portal::testing {
|
||||
namespace esphome::wifi::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// Stand-in for wifi::WiFiScanResult, which does not compile on the host.
|
||||
// Stand-in for WiFiScanResult, which does not compile on the host.
|
||||
struct Entry {
|
||||
std::string ssid;
|
||||
int8_t rssi;
|
||||
@@ -24,7 +24,7 @@ struct Entry {
|
||||
bool get_is_hidden() const { return this->is_hidden; }
|
||||
};
|
||||
|
||||
// One row as the portal would emit it.
|
||||
// One network as a consumer would emit it.
|
||||
struct Row {
|
||||
std::string ssid;
|
||||
int8_t rssi;
|
||||
@@ -33,7 +33,7 @@ struct Row {
|
||||
bool operator==(const Row &rhs) const { return ssid == rhs.ssid && rssi == rhs.rssi && lock == rhs.lock; }
|
||||
};
|
||||
|
||||
// Walk the results the way handle_config does and collect the rows that survive.
|
||||
// Walk the results the way the consumers do and collect the rows that survive.
|
||||
std::vector<Row> rows(const std::vector<Entry> &results) {
|
||||
std::vector<Row> out;
|
||||
for (size_t i = 0; i < results.size(); i++) {
|
||||
@@ -127,4 +127,4 @@ TEST(ScanList, EmptyListShowsNothing) {
|
||||
EXPECT_TRUE(rows(results).empty());
|
||||
}
|
||||
|
||||
} // namespace esphome::captive_portal::testing
|
||||
} // namespace esphome::wifi::testing
|
||||
@@ -0,0 +1,20 @@
|
||||
esphome:
|
||||
name: get-time-tz-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
|
||||
sensor:
|
||||
# Exposes the standard offset of the effective timezone so the test can
|
||||
# observe which GetTimeResponse messages changed it
|
||||
- platform: template
|
||||
name: "TZ Offset"
|
||||
id: tz_offset
|
||||
accuracy_decimals: 0
|
||||
update_interval: 100ms
|
||||
lambda: |-
|
||||
return time::get_global_tz().std_offset_seconds;
|
||||
+6
-5
@@ -1,5 +1,5 @@
|
||||
esphome:
|
||||
name: uart-mock-modbus-custom-command
|
||||
name: uart-mock-modbus-custom-pdu
|
||||
|
||||
host:
|
||||
api:
|
||||
@@ -69,13 +69,14 @@ sensor:
|
||||
address: 0x01
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
# Custom command: a raw frame {device address, function code, address hi, address lo,
|
||||
# count hi, count lo}; the CRC is appended by the hub. Reads holding register 0x0001,
|
||||
# count 1; the lambda parses the response payload (the register value, big-endian).
|
||||
# Custom PDU: read holding register 0x0001, count 1. The PDU is
|
||||
# {function code, address hi, address lo, count hi, count lo}; the device
|
||||
# address and CRC are added by the hub. The lambda parses the response payload
|
||||
# (the register value, big-endian).
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "custom_read"
|
||||
custom_command: [0x01, 0x03, 0x00, 0x01, 0x00, 0x01]
|
||||
custom_pdu: [0x03, 0x00, 0x01, 0x00, 0x01]
|
||||
lambda: |-
|
||||
if (data.size() < 2) return {};
|
||||
return (float) ((data[0] << 8) | data[1]);
|
||||
@@ -134,7 +134,8 @@ sensor:
|
||||
value_type: U_WORD
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
|
||||
# F - contiguous registers where the second asks for a slower rate.
|
||||
# F - contiguous registers that historically carried differing skip_updates rates and were split by
|
||||
# the rate merge; with per-range rates gone they group like any contiguous pair.
|
||||
- platform: modbus_controller
|
||||
name: "rate_first"
|
||||
address: 0x150
|
||||
@@ -146,7 +147,6 @@ sensor:
|
||||
address: 0x151
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
skip_updates: 5
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
|
||||
# B - a wide value and one of its halves share a start address, with a contiguous sensor after them.
|
||||
|
||||
@@ -51,9 +51,8 @@ modbus_controller:
|
||||
modbus_id: virtual_modbus_client
|
||||
id: ctl
|
||||
max_cmd_retries: 1
|
||||
# offline_skip_updates and the sensor's skip_updates deliberately share a period: offline
|
||||
# probing must follow the offline cadence alone, or phase combinations like this one can
|
||||
# leave the device never probing again.
|
||||
# offline_skip_updates: 1 -> once offline, the controller re-probes every second update cycle;
|
||||
# the test silences the device to force it offline, then answers again and checks it recovers.
|
||||
offline_skip_updates: 1
|
||||
update_interval: never
|
||||
on_offline:
|
||||
@@ -71,7 +70,6 @@ sensor:
|
||||
address: 0x03
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
skip_updates: 1
|
||||
# Mirrors the controller's online state so the test can await the transitions.
|
||||
- platform: template
|
||||
name: link_state
|
||||
|
||||
@@ -49,14 +49,10 @@ uart_mock:
|
||||
inject_rx: [0x01, 0x03, 0x02, 0x01, 0x41, 0x79, 0xE4] # 0x101 = 0x0141 = 321
|
||||
- expect_tx: [0x01, 0x03, 0x01, 0x03, 0x00, 0x01, 0x75, 0xF6] # Read holding 0x103 count 1
|
||||
inject_rx: [0x01, 0x03, 0x02, 0x01, 0xA5, 0x79, 0xAF] # 0x103 = 0x01A5 = 421
|
||||
# A widened shared-address range at 0x200 plus a sensor at 0x201 carrying its own skip_updates.
|
||||
# The sensor must keep its own range so the polling rates stay independent; if it were folded into
|
||||
# the widened range it would decode 0x201 from THAT response (2, not 777) and drag the range's
|
||||
# rate down to its own.
|
||||
# Two sensors sharing start address 0x200 (a word and a dword) widen the range to 2 registers
|
||||
# and both decode from the single response.
|
||||
- expect_tx: [0x01, 0x03, 0x02, 0x00, 0x00, 0x02, 0xC5, 0xB3] # Read holding 0x200 count 2
|
||||
inject_rx: [0x01, 0x03, 0x04, 0x01, 0x41, 0x00, 0x02, 0x2A, 0x1A] # 0x200=0x0141, 0x201=0x0002
|
||||
- expect_tx: [0x01, 0x03, 0x02, 0x01, 0x00, 0x01, 0xD4, 0x72] # Read holding 0x201 count 1
|
||||
inject_rx: [0x01, 0x03, 0x02, 0x03, 0x09, 0x78, 0xB2] # 0x201 = 0x0309 = 777
|
||||
|
||||
modbus:
|
||||
uart_id: virtual_uart_dev
|
||||
@@ -128,26 +124,17 @@ sensor:
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
# Shared address 0x200: the dword widens the range the word opened (or vice versa)
|
||||
- platform: modbus_controller
|
||||
name: "rate_word"
|
||||
name: "widen_word"
|
||||
address: 0x200
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
- platform: modbus_controller
|
||||
name: "rate_dword"
|
||||
name: "widen_dword"
|
||||
address: 0x200
|
||||
register_type: holding
|
||||
value_type: U_DWORD
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
# Inside the widened range but with its own skip_updates: must NOT be folded in, or the two sensors
|
||||
# above would silently drop to this sensor's polling rate
|
||||
- platform: modbus_controller
|
||||
name: "own_rate"
|
||||
address: 0x201
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
skip_updates: 100
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Integration test for GetTimeResponse parsed_timezone presence handling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aioesphomeapi import connection as api_connection
|
||||
from aioesphomeapi.api_pb2 import GetTimeResponse
|
||||
import pytest
|
||||
|
||||
from .state_utils import SensorTracker, build_key_to_entity_mapping
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# 2024-01-01 00:00:00 UTC
|
||||
EPOCH = 1704067200
|
||||
# POSIX offsets are positive west of UTC, so UTC+7 is -25200 and UTC-5 is 18000
|
||||
UTC_PLUS_7 = -25200
|
||||
UTC_MINUS_5 = 18000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_get_time_response_timezone(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A present parsed_timezone is applied even when all zero; an absent one is ignored."""
|
||||
# The client answers the device's own GetTimeRequest with the host timezone;
|
||||
# strip the parsed field from that reply so only the messages sent below
|
||||
# can change the device timezone.
|
||||
monkeypatch.setattr(api_connection, "_build_parsed_tz_proto", lambda tz: None)
|
||||
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
tracker = SensorTracker(["tz_offset"])
|
||||
tracker.key_to_sensor = build_key_to_entity_mapping(entities, ["tz_offset"])
|
||||
client.subscribe_states(tracker.on_state)
|
||||
|
||||
await tracker.await_change(tracker.expect_any("tz_offset"), "tz_offset")
|
||||
initial = tracker.sensor_states["tz_offset"][-1]
|
||||
# Pick a zone that differs from the codegen default so the change is visible
|
||||
target = UTC_PLUS_7 if initial != UTC_PLUS_7 else UTC_MINUS_5
|
||||
|
||||
# Present, non-zero: applied
|
||||
future = tracker.expect("tz_offset", target)
|
||||
resp = GetTimeResponse(epoch_seconds=EPOCH)
|
||||
resp.parsed_timezone.std_offset_seconds = target
|
||||
resp.parsed_timezone.dst_offset_seconds = target
|
||||
client._connection.send_messages((resp,))
|
||||
await tracker.await_change(future, "tz_offset")
|
||||
|
||||
# Absent (legacy client with only the deprecated string): ignored, and in
|
||||
# particular not mistaken for an all-zero UTC zone
|
||||
future = tracker.expect("tz_offset", 0)
|
||||
resp = GetTimeResponse(epoch_seconds=EPOCH, timezone="UTC0")
|
||||
client._connection.send_messages((resp,))
|
||||
await tracker.await_must_not_change(future, "tz_offset", timeout=1.0)
|
||||
assert tracker.sensor_states["tz_offset"][-1] == target
|
||||
# Retire the expectation so it cannot swallow the first matching state
|
||||
# meant for the next phase
|
||||
future.cancel()
|
||||
|
||||
# Present but all zero (genuine UTC): applied
|
||||
future = tracker.expect("tz_offset", 0)
|
||||
resp = GetTimeResponse(epoch_seconds=EPOCH)
|
||||
resp.parsed_timezone.SetInParent()
|
||||
client._connection.send_messages((resp,))
|
||||
await tracker.await_change(future, "tz_offset")
|
||||
@@ -675,9 +675,7 @@ async def test_uart_mock_modbus_shared_address(
|
||||
wide sensor's span keep polling separately, and that the sensor at the span's tail address does not
|
||||
anchor a re-use join on a mid-range predecessor (which would make it decode that sensor's bytes).
|
||||
|
||||
A sensor at 0x201 carrying skip_updates sits inside a widened shared-address range at 0x200 but
|
||||
keeps its own range, so polling rates stay independent; folding it in would also make it decode
|
||||
0x201 out of the shared response (2) instead of its own poll (777).
|
||||
A word and a dword sharing 0x200 widen that range to two registers and both decode from the one read.
|
||||
"""
|
||||
|
||||
line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback()
|
||||
@@ -693,9 +691,8 @@ async def test_uart_mock_modbus_shared_address(
|
||||
"wide_qword": 100,
|
||||
"inside_wide": 321,
|
||||
"tail_of_wide": 421,
|
||||
"rate_word": 321,
|
||||
"rate_dword": pytest.approx(21037058),
|
||||
"own_rate": 777,
|
||||
"widen_word": 321,
|
||||
"widen_dword": pytest.approx(21037058),
|
||||
}
|
||||
tracker = SensorTracker(list(expected_values.keys()))
|
||||
futures = tracker.expect_all(expected_values)
|
||||
@@ -710,18 +707,18 @@ async def test_uart_mock_modbus_shared_address(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_modbus_custom_command(
|
||||
async def test_uart_mock_modbus_custom_pdu(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test a custom_command sensor polling a register served by the mock server.
|
||||
"""Test a custom_pdu sensor reading a register served by the mock server.
|
||||
|
||||
The custom_command is a raw frame (device address + PDU); the hub appends the CRC and
|
||||
routes the response back to the polling command, whose sensor lambda parses the payload.
|
||||
Guards the custom polling wiring: the command must reference the sensor's custom_data and
|
||||
decode the real function code, or nothing is ever transmitted. A plain read on the same
|
||||
register anchors the bus.
|
||||
The custom_pdu is a raw read-holding PDU (function code + address + count); the
|
||||
controller prepends its own device address and appends the CRC, sends it, and the
|
||||
sensor's lambda parses the response payload. Confirms the custom PDU path decodes
|
||||
the function code and routes the response to the sensor (the gap that hid the
|
||||
step-2 raw-vs-PDU bug). A plain read on the same register anchors the bus.
|
||||
"""
|
||||
|
||||
line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback()
|
||||
@@ -754,9 +751,8 @@ async def test_uart_mock_modbus_offline(
|
||||
publishes. This pins the pooled non-response counter, can_send() gating, the
|
||||
offline retry cadence, and recovery - none of which the responding-path tests touch.
|
||||
|
||||
The fixture gives offline_skip_updates and the sensor's skip_updates the same period
|
||||
on purpose: offline probing must follow the offline cadence alone, since requiring
|
||||
both cadences to coincide leaves phase combinations where no probe ever goes out.
|
||||
Offline probing follows the offline cadence alone; regular every-update polling
|
||||
resumes once the device answers again.
|
||||
"""
|
||||
|
||||
tracker = SensorTracker(["link_state", "reg"])
|
||||
|
||||
Reference in New Issue
Block a user