Compare commits

...
Author SHA1 Message Date
J. Nick Koston f0473e7419 Merge branch 'dev' into ld2450_flash_strings 2026-10-21 18:02:58 -10:00
J. Nick Koston 3325de5e28 Avoid optional access in the select names test for clang-tidy 2026-10-21 17:50:06 -10:00
cc89a011e2 Bump clang-tidy + clang-format from 22.1.8 to 23.1.3 (#21296)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-21 17:45:36 -10:00
J. Nick Koston afffe3ba36 Test the baud rate and zone type name and enum paths 2026-10-21 17:32:14 -10:00
J. Nick Koston 6bf3e07a74 Note the select option order 2026-10-21 17:20:27 -10:00
J. Nick Koston c6c1fce2bf Read flash tables without memcpy and assert the table order 2026-10-21 17:13:04 -10:00
Craig Andrews c76e9e1d62 [epaper_spi] Support Good Display GDEY029Z95 2.9" 3-color e-paper (128x296, SSD1680) (#21218) 2026-10-21 17:09:44 -10:00
J. Nick Koston 4293d12008 Drive the selects by index 2026-10-21 17:04:06 -10:00
esphome[bot] 7a3aac407f Bump aioesphomeapi from 46.18.0 to 46.19.0 (#21345) 2026-10-21 17:03:34 -10:00
J. Nick Koston 51819d40cb Keep the baud rate table in flash 2026-10-21 17:01:47 -10:00
J. Nick Koston b0a1054e8d [ld2410] Publish queried number values directly (#21346) 2026-10-21 16:56:51 -10:00
J. Nick Koston e7ad8f9c80 [ld2450] Keep the baud rate, direction and zone type names in flash 2026-10-21 16:45:21 -10:00
a8a8acb918 [sensor] Add Relative Humidity Correction Filter (#18287)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-10-21 16:33:11 -10:00
Jesse Hills 3ae5e1dfad [core] Add unit tests for the smaller core modules (#21333) 2026-10-22 15:23:22 +13:00
Jonathan Swoboda ef79d48b6f [core] Add a default label to the switches clang-tidy 23 flags as not covering every path (#21338) 2026-10-22 02:22:32 +00:00
Jonathan Swoboda 94804cc741 [core] Drop IRAM_ATTR from interrupt handler declarations (#21335) 2026-10-21 16:13:18 -10:00
Jonathan Swoboda 879c5e5e14 [weikai][haier][zephyr_ble_server][wifi][improv_base] Fix the bugprone and diagnostic findings of clang-tidy 23 (#21334) 2026-10-22 02:12:43 +00:00
104 changed files with 2401 additions and 189 deletions
+8
View File
@@ -12,6 +12,7 @@ Checks: >-
-bugprone-multi-level-implicit-pointer-conversion,
-bugprone-narrowing-conversions,
-bugprone-tagged-union-member-count,
-bugprone-signed-bitwise,
-bugprone-signed-char-misuse,
-bugprone-switch-missing-default-case,
-cert-dcl50-cpp,
@@ -39,6 +40,7 @@ Checks: >-
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-explicit-constructor,
-cppcoreguidelines-init-variables,
-cppcoreguidelines-macro-to-enum,
-cppcoreguidelines-macro-usage,
@@ -84,6 +86,7 @@ Checks: >-
-llvm-use-ranges,
-llvmlibc-*,
-misc-const-correctness,
-misc-explicit-constructor,
-misc-include-cleaner,
-misc-multiple-inheritance,
-misc-no-recursion,
@@ -113,6 +116,7 @@ Checks: >-
-objc-*,
-performance-enum-size,
-portability-avoid-pragma-once,
-portability-no-assembler,
-portability-template-virtual-member-function,
-readability-ambiguous-smartptr-reset-call,
-readability-avoid-nested-conditional-operator,
@@ -129,9 +133,13 @@ Checks: >-
-readability-named-parameter,
-readability-redundant-casting,
-readability-redundant-inline-specifier,
-readability-redundant-lambda-parameter-list,
-readability-redundant-member-init,
-readability-redundant-nested-if,
-readability-redundant-parentheses,
-readability-redundant-typename,
-readability-trailing-comma,
-readability-trivial-switch,
-readability-uppercase-literal-suffix,
-readability-use-anyofallof,
-readability-use-std-min-max,
+1 -1
View File
@@ -38,7 +38,7 @@ repos:
- id: yamllint
exclude: ^(\.clang-format|\.clang-tidy)$
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v22.1.8
rev: v23.1.3
hooks:
- id: clang-format
types_or: [c, c++]
@@ -111,6 +111,8 @@ static const LogString *input_to_str(uint8_t input) {
return LOG_STR("VDD");
case NRF_SAADC_INPUT_VDDHDIV5:
return LOG_STR("VDDHDIV5");
default:
break;
}
return LOG_STR("undefined input");
}
+2
View File
@@ -261,6 +261,8 @@ void AirtonClimate::parse_state_frame_(const uint8_t *frame) {
case AIRTON_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
default:
break;
}
this->target_temperature = (frame[3] & 0b00001111) + AIRTON_TEMP_MIN;
+122
View File
@@ -25,6 +25,8 @@ void HelloRequest::decode_field(void *self, uint32_t tag, const uint8_t *data, p
msg.outgoing_connection_target = value.as_bool();
break;
#endif
default:
break;
}
}
uint8_t *HelloResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -51,6 +53,8 @@ void DisconnectRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
case proto_tag(1, WIRE_TYPE_VARINT):
msg.reason = static_cast<enums::DisconnectReason>(value.as_varint());
break;
default:
break;
}
}
uint8_t *DisconnectRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -396,6 +400,8 @@ void WizardInputSetRequest::decode_field(void *self, uint32_t tag, const uint8_t
case proto_tag(2, WIRE_TYPE_LENGTH_DELIMITED):
msg.entity_id = StringRef(value.data(), value.size());
break;
default:
break;
}
}
#endif
@@ -559,6 +565,8 @@ void CoverCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
case proto_tag(10, WIRE_TYPE_VARINT):
msg.stop_tilt = value.as_bool();
break;
default:
break;
}
}
#endif
@@ -679,6 +687,8 @@ void FanCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -885,6 +895,8 @@ void LightCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -1028,6 +1040,8 @@ void SwitchCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -1098,6 +1112,8 @@ void SubscribeLogsRequest::decode_field(void *self, uint32_t tag, const uint8_t
case proto_tag(2, WIRE_TYPE_VARINT):
msg.dump_config = value.as_bool();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -1127,6 +1143,8 @@ void NoiseEncryptionSetKeyRequest::decode_field(void *self, uint32_t tag, const
msg.key = value.data();
msg.key_len = value.size();
break;
default:
break;
}
}
uint8_t *NoiseEncryptionSetKeyResponse::encode_msg(const void *self,
@@ -1233,6 +1251,8 @@ void HomeassistantActionResponse::decode_field(void *self, uint32_t tag, const u
msg.response_data_len = value.size();
break;
#endif
default:
break;
}
}
#endif
@@ -1267,6 +1287,8 @@ void HomeAssistantStateResponse::decode_field(void *self, uint32_t tag, const ui
case proto_tag(3, WIRE_TYPE_LENGTH_DELIMITED):
msg.attribute = StringRef(value.data(), value.size());
break;
default:
break;
}
}
#endif
@@ -1292,6 +1314,8 @@ void DSTRule::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_
case proto_tag(6, WIRE_TYPE_VARINT):
msg.day_of_week = value.as_varint();
break;
default:
break;
}
}
void ParsedTimezone::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -1310,6 +1334,8 @@ void ParsedTimezone::decode_field(void *self, uint32_t tag, const uint8_t *data,
case proto_tag(4, WIRE_TYPE_LENGTH_DELIMITED):
value.decode_to_message(msg.dst_end);
break;
default:
break;
}
}
void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -1323,6 +1349,8 @@ void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data
value.decode_to_message(msg.parsed_timezone);
msg.has_parsed_timezone = true;
break;
default:
break;
}
}
#ifdef USE_API_USER_DEFINED_ACTIONS
@@ -1413,6 +1441,8 @@ void ExecuteServiceArgument::decode_field(void *self, uint32_t tag, const uint8_
case proto_tag(9, WIRE_TYPE_LENGTH_DELIMITED):
msg.string_array.push_back(value.as_string());
break;
default:
break;
}
}
void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
@@ -1447,6 +1477,8 @@ void ExecuteServiceRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.return_response = value.as_bool();
break;
#endif
default:
break;
}
}
void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
@@ -1541,6 +1573,8 @@ void CameraImageRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
case proto_tag(2, WIRE_TYPE_VARINT):
msg.stream = value.as_bool();
break;
default:
break;
}
}
#endif
@@ -1777,6 +1811,8 @@ void ClimateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -1905,6 +1941,8 @@ void WaterHeaterCommandRequest::decode_field(void *self, uint32_t tag, const uin
case proto_tag(8, WIRE_TYPE_FIXED32):
msg.target_temperature_high = value.as_float();
break;
default:
break;
}
}
#endif
@@ -1996,6 +2034,8 @@ void NumberCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2075,6 +2115,8 @@ void SelectCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2177,6 +2219,8 @@ void SirenCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2260,6 +2304,8 @@ void LockCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2309,6 +2355,8 @@ void ButtonCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2436,6 +2484,8 @@ void MediaPlayerCommandRequest::decode_field(void *self, uint32_t tag, const uin
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2448,6 +2498,8 @@ void SubscribeBluetoothLEAdvertisementsRequest::decode_field(void *self, uint32_
case proto_tag(1, WIRE_TYPE_VARINT):
msg.flags = value.as_varint();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -2506,6 +2558,8 @@ void BluetoothDeviceRequest::decode_field(void *self, uint32_t tag, const uint8_
case proto_tag(4, WIRE_TYPE_VARINT):
msg.address_type = value.as_varint();
break;
default:
break;
}
}
uint8_t *BluetoothDeviceConnectionResponse::encode_msg(const void *self,
@@ -2534,6 +2588,8 @@ void BluetoothGATTGetServicesRequest::decode_field(void *self, uint32_t tag, con
case proto_tag(1, WIRE_TYPE_VARINT):
msg.address = value.as_varint();
break;
default:
break;
}
}
uint8_t *BluetoothGATTDescriptor::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -2660,6 +2716,8 @@ void BluetoothGATTReadRequest::decode_field(void *self, uint32_t tag, const uint
case proto_tag(2, WIRE_TYPE_VARINT):
msg.handle = value.as_varint();
break;
default:
break;
}
}
uint8_t *BluetoothGATTReadResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -2695,6 +2753,8 @@ void BluetoothGATTWriteRequest::decode_field(void *self, uint32_t tag, const uin
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
void BluetoothGATTReadDescriptorRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -2708,6 +2768,8 @@ void BluetoothGATTReadDescriptorRequest::decode_field(void *self, uint32_t tag,
case proto_tag(2, WIRE_TYPE_VARINT):
msg.handle = value.as_varint();
break;
default:
break;
}
}
void BluetoothGATTWriteDescriptorRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -2725,6 +2787,8 @@ void BluetoothGATTWriteDescriptorRequest::decode_field(void *self, uint32_t tag,
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
void BluetoothGATTNotifyRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -2741,6 +2805,8 @@ void BluetoothGATTNotifyRequest::decode_field(void *self, uint32_t tag, const ui
case proto_tag(3, WIRE_TYPE_VARINT):
msg.enable = value.as_bool();
break;
default:
break;
}
}
uint8_t *BluetoothGATTNotifyDataResponse::encode_msg(const void *self,
@@ -2898,6 +2964,8 @@ void BluetoothScannerSetModeRequest::decode_field(void *self, uint32_t tag, cons
case proto_tag(1, WIRE_TYPE_VARINT):
msg.mode = static_cast<enums::BluetoothScannerMode>(value.as_varint());
break;
default:
break;
}
}
#endif
@@ -2913,6 +2981,8 @@ void SubscribeVoiceAssistantRequest::decode_field(void *self, uint32_t tag, cons
case proto_tag(2, WIRE_TYPE_VARINT):
msg.flags = value.as_varint();
break;
default:
break;
}
}
uint8_t *VoiceAssistantAudioSettings::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -2961,6 +3031,8 @@ void VoiceAssistantResponse::decode_field(void *self, uint32_t tag, const uint8_
case proto_tag(2, WIRE_TYPE_VARINT):
msg.error = value.as_bool();
break;
default:
break;
}
}
void VoiceAssistantEventData::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -2973,6 +3045,8 @@ void VoiceAssistantEventData::decode_field(void *self, uint32_t tag, const uint8
case proto_tag(2, WIRE_TYPE_LENGTH_DELIMITED):
msg.value = StringRef(value.data(), value.size());
break;
default:
break;
}
}
void VoiceAssistantEventResponse::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -2987,6 +3061,8 @@ void VoiceAssistantEventResponse::decode_field(void *self, uint32_t tag, const u
msg.data.emplace_back();
value.decode_to_message(msg.data.back());
break;
default:
break;
}
}
void VoiceAssistantAudio::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -3004,6 +3080,8 @@ void VoiceAssistantAudio::decode_field(void *self, uint32_t tag, const uint8_t *
msg.data2 = value.data();
msg.data2_len = value.size();
break;
default:
break;
}
}
uint8_t *VoiceAssistantAudio::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -3044,6 +3122,8 @@ void VoiceAssistantTimerEventResponse::decode_field(void *self, uint32_t tag, co
case proto_tag(6, WIRE_TYPE_VARINT):
msg.is_active = value.as_bool();
break;
default:
break;
}
}
void VoiceAssistantAnnounceRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -3063,6 +3143,8 @@ void VoiceAssistantAnnounceRequest::decode_field(void *self, uint32_t tag, const
case proto_tag(4, WIRE_TYPE_VARINT):
msg.start_conversation = value.as_bool();
break;
default:
break;
}
}
uint8_t *VoiceAssistantAnnounceFinished::encode_msg(const void *self,
@@ -3124,6 +3206,8 @@ void VoiceAssistantExternalWakeWord::decode_field(void *self, uint32_t tag, cons
case proto_tag(7, WIRE_TYPE_LENGTH_DELIMITED):
msg.url = StringRef(value.data(), value.size());
break;
default:
break;
}
}
void VoiceAssistantConfigurationRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -3135,6 +3219,8 @@ void VoiceAssistantConfigurationRequest::decode_field(void *self, uint32_t tag,
msg.external_wake_words.emplace_back();
value.decode_to_message(msg.external_wake_words.back());
break;
default:
break;
}
}
uint8_t *VoiceAssistantConfigurationResponse::encode_msg(const void *self,
@@ -3173,6 +3259,8 @@ void VoiceAssistantSetConfiguration::decode_field(void *self, uint32_t tag, cons
case proto_tag(1, WIRE_TYPE_LENGTH_DELIMITED):
msg.active_wake_words.push_back(value.as_string());
break;
default:
break;
}
}
#endif
@@ -3254,6 +3342,8 @@ void AlarmControlPanelCommandRequest::decode_field(void *self, uint32_t tag, con
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3333,6 +3423,8 @@ void TextCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3414,6 +3506,8 @@ void DateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3495,6 +3589,8 @@ void TimeCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3644,6 +3740,8 @@ void ValveCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3718,6 +3816,8 @@ void DateTimeCommandRequest::decode_field(void *self, uint32_t tag, const uint8_
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3807,6 +3907,8 @@ void UpdateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3819,6 +3921,8 @@ void ZWaveProxyFrame::decode_field(void *self, uint32_t tag, const uint8_t *data
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -3845,6 +3949,8 @@ void ZWaveProxyRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
uint8_t *ZWaveProxyRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -3940,6 +4046,8 @@ void InfraredRFTransmitRawTimingsRequest::decode_field(void *self, uint32_t tag,
case proto_tag(6, WIRE_TYPE_VARINT):
msg.modulation = value.as_varint();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -4056,6 +4164,8 @@ void SerialProxyConfigureRequest::decode_field(void *self, uint32_t tag, const u
case proto_tag(6, WIRE_TYPE_VARINT):
msg.data_size = value.as_varint();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -4084,6 +4194,8 @@ void SerialProxyWriteRequest::decode_field(void *self, uint32_t tag, const uint8
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
void SerialProxySetModemPinsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -4097,6 +4209,8 @@ void SerialProxySetModemPinsRequest::decode_field(void *self, uint32_t tag, cons
case proto_tag(2, WIRE_TYPE_VARINT):
msg.line_states = value.as_varint();
break;
default:
break;
}
}
void SerialProxyGetModemPinsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -4107,6 +4221,8 @@ void SerialProxyGetModemPinsRequest::decode_field(void *self, uint32_t tag, cons
case proto_tag(1, WIRE_TYPE_VARINT):
msg.instance = value.as_varint();
break;
default:
break;
}
}
uint8_t *SerialProxyGetModemPinsResponse::encode_msg(const void *self,
@@ -4135,6 +4251,8 @@ void SerialProxyRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
case proto_tag(2, WIRE_TYPE_VARINT):
msg.type = static_cast<enums::SerialProxyRequestType>(value.as_varint());
break;
default:
break;
}
}
uint8_t *SerialProxyRequestResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -4165,6 +4283,8 @@ void SerialProxySetModeRequest::decode_field(void *self, uint32_t tag, const uin
case proto_tag(2, WIRE_TYPE_VARINT):
msg.mode = static_cast<enums::SerialProxyMode>(value.as_varint());
break;
default:
break;
}
}
uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -4233,6 +4353,8 @@ void BluetoothSetConnectionParamsRequest::decode_field(void *self, uint32_t tag,
case proto_tag(5, WIRE_TYPE_VARINT):
msg.timeout = value.as_varint();
break;
default:
break;
}
}
uint8_t *BluetoothSetConnectionParamsResponse::encode_msg(const void *self,
+1 -1
View File
@@ -301,7 +301,7 @@ constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
/// Generated encode body: writes the fields at pos, returns the cursor past them.
using ProtoEncodeFn = uint8_t *(*) (const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
using ProtoEncodeFn = uint8_t *(*)(const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
/// Static encode helpers for the generated encode bodies. Each takes the write cursor by value and
/// returns it advanced, so outlined calls at -Os chain through the return register instead of a
+2
View File
@@ -168,6 +168,8 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
case BALLU_AUTO:
this->mode = climate::CLIMATE_MODE_AUTO;
break;
default:
break;
}
}
+4 -5
View File
@@ -111,10 +111,9 @@ static const uint8_t BL0906_USR_WRPROT = 0x9E;
// Reset Register
static const uint8_t BL0906_SOFT_RESET = 0x9F;
const uint8_t BL0906_INIT[2][6] = {
// Reset to default
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
// Enable User Operation Write
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
const uint8_t BL0906_INIT[2][6] = {// Reset to default
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
// Enable User Operation Write
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
} // namespace esphome::bl0906
@@ -268,6 +268,7 @@ int BluedroidGattClient::discover_services() {
case SearchState::REPORT_PENDING:
return 0; // One completion is already owed to this claimant.
case SearchState::NONE:
default:
break;
}
int err = this->check_and_log_error_("esp_ble_gattc_search_service",
+7 -3
View File
@@ -123,7 +123,7 @@ void BMI160Component::internal_setup_(int stage) {
bool BMI160Component::setup_stage_(int stage) {
switch (stage) {
case 0:
case 0: {
uint8_t chipid;
if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) {
return false;
@@ -138,7 +138,7 @@ bool BMI160Component::setup_stage_(int stage) {
// min 5ms, 10ms
this->set_timeout(10, [this]() { this->internal_setup_(1); });
break;
}
case 1:
ESP_LOGV(TAG, " Bringing gyroscope out of sleep");
if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::GYRO_SET_PMU_MODE | (uint8_t) GyroPmuMode::NORMAL)) {
@@ -149,7 +149,7 @@ bool BMI160Component::setup_stage_(int stage) {
this->set_timeout(GYRO_WAKEUP_TIMEOUT_MS, [this]() { this->internal_setup_(2); });
break;
case 2:
case 2: {
ESP_LOGV(TAG, " Setting up Gyro Config");
uint8_t gyro_config = (uint8_t) GyroBandwidth::OSR4 | (uint8_t) GyroOuputDataRate::HZ_25;
ESP_LOGV(TAG, " Output gyro_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_config));
@@ -178,6 +178,10 @@ bool BMI160Component::setup_stage_(int stage) {
}
this->setup_complete_ = true;
break;
}
default:
break;
}
return true;
}
+1 -1
View File
@@ -108,7 +108,7 @@ class CC1101Component final : public Component,
// GDO pin for packet reception
InternalGPIOPin *gdo0_pin_{nullptr};
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
static void gpio_intr(CC1101Component *arg);
// Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
+2
View File
@@ -175,6 +175,8 @@ uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) {
case POWER_PAC:
coeff = this->data_.coefficient[POWER_PAC] * scale;
return coeff ? 0x80000000 / coeff : 0;
default:
break;
}
return 0;
}
@@ -156,6 +156,8 @@ static const char *regout0_to_str(uint32_t value) {
return "3.0V";
case (UICR_REGOUT0_VOUT_3V3):
return "3.3V";
default:
break;
}
return "???V";
}
@@ -337,6 +339,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
return "QFxx - 6x6 48-pin QFN";
case 0x2005:
return "CKxx - 3.544 x 3.607 WLCSP";
default:
break;
}
return "Unspecified";
};
+2
View File
@@ -143,6 +143,8 @@ bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
case DELONGHI_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
default:
break;
}
this->publish_state();
return true;
+2
View File
@@ -250,6 +250,8 @@ void DFPlayer::loop() {
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
default:
break;
}
break;
case 0x41:
@@ -0,0 +1,12 @@
#include "epaper_goodisplay_3c.h"
namespace esphome::epaper_spi {
void EPaperGoodDisplay3C::power_on() {}
void EPaperGoodDisplay3C::refresh_screen(bool partial) {
EPaperWeAct3C::refresh_screen(partial);
this->next_delay_ = REFRESH_START_MS;
}
} // namespace esphome::epaper_spi
@@ -0,0 +1,28 @@
#pragma once
#include "epaper_weact_3c.h"
namespace esphome::epaper_spi {
/**
* Good Display 3-color e-paper displays (SSD1680 controller), e.g. GDEY029Z95.
*
* Driven as EPaperWeAct3C, except that there is no separate power-on: the full refresh (0xF7) powers the panel
* up itself. Busy then only goes high a little after the refresh starts, so the next state waits before
* checking it.
*/
class EPaperGoodDisplay3C : public EPaperWeAct3C {
public:
using EPaperWeAct3C::EPaperWeAct3C;
protected:
// Time from master activation (0x20) until busy can be relied on
static constexpr uint16_t REFRESH_START_MS = 50;
// With EPaperWeAct3C's power-on (0x22 0xF8) first, this panel's refresh ends almost at once and the image
// does not change.
void power_on() override;
void refresh_screen(bool partial) override;
};
} // namespace esphome::epaper_spi
@@ -4,11 +4,17 @@ Supported models:
- weact-2.13in-3c: 122x250 pixels (2.13" display)
- weact-2.9in-3c: 128x296 pixels (2.9" display)
- weact-4.2in-3c: 400x300 pixels (4.2" display)
- goodisplay-gdey029z95-2.9: 128x296 pixels (Good Display GDEY029Z95 2.9" display)
These displays use SSD1680 or SSD1683 controller and require a specific initialization
sequence. The DRV_OUT_CTL command is calculated from the display height.
The Good Display panel is driven as the WeAct ones, except that it must not get the
separate power-on before the refresh, so it has its own driver class, EPaperGoodDisplay3C.
"""
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN
from . import EpaperModel
@@ -17,8 +23,10 @@ class WeActBWR(EpaperModel):
source_files = ("epaper_weact_3c.cpp",)
def __init__(self, name, **defaults):
super().__init__(name, "EPaperWeAct3C", **defaults)
def __init__(
self, name: str, class_name: str = "EPaperWeAct3C", **defaults: object
) -> None:
super().__init__(name, class_name, **defaults)
def get_init_sequence(self, config):
"""Generate initialization sequence for WeAct BWR displays.
@@ -75,3 +83,27 @@ weact_4p2in3c = WeActBWR(
data_rate="10MHz",
minimum_update_interval="10s",
)
class GoodDisplayBWR(WeActBWR):
"""Good Display Black/White/Red displays using SSD1680 controller."""
source_files = ("epaper_weact_3c.cpp", "epaper_goodisplay_3c.cpp")
def __init__(self, name: str, **defaults: object) -> None:
super().__init__(name, "EPaperGoodDisplay3C", **defaults)
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
# Only a hardware reset wakes the panel from deep sleep, and refreshes are timed by busy
if name in (CONF_RESET_PIN, CONF_BUSY_PIN):
return cv.Required(name)
return super().option(name, fallback)
# Model: Good Display GDEY029Z95 2.9" BWR - 128x296 pixels, SSD1680 controller
GoodDisplayBWR(
"goodisplay-gdey029z95-2.9",
width=128,
height=296,
minimum_update_interval="10s",
)
+2
View File
@@ -175,6 +175,8 @@ void EzoPMP::read_command_result_() {
third_parameter_buffer[position_in_parameter_buffer] = current_char;
third_parameter_buffer[position_in_parameter_buffer + 1] = '\0';
break;
default:
break;
}
position_in_parameter_buffer++;
}
@@ -194,6 +194,8 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
ESP_LOGE(TAG, "Could not find fingerprint features");
this->finger_scan_invalid_callback_.call();
break;
default:
break;
}
return send_result;
}
+6 -5
View File
@@ -318,20 +318,21 @@ void HaierClimateBase::process_protocol_reset() {
bool HaierClimateBase::prepare_pending_action() {
if (this->action_request_.has_value()) {
switch (this->action_request_.value().action) {
PendingAction &request = this->action_request_.value();
switch (request.action) {
case ActionRequest::SEND_CUSTOM_COMMAND:
return true;
case ActionRequest::TURN_POWER_ON:
this->action_request_.value().message = this->get_power_message(true);
request.message = this->get_power_message(true);
return true;
case ActionRequest::TURN_POWER_OFF:
this->action_request_.value().message = this->get_power_message(false);
request.message = this->get_power_message(false);
return true;
case ActionRequest::TOGGLE_POWER:
this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
request.message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
return true;
default:
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action);
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) request.action);
this->action_request_.reset();
return false;
}
+4
View File
@@ -1071,6 +1071,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
case (uint8_t) hon_protocol::FanMode::FAN_HIGH:
this->fan_mode = CLIMATE_FAN_HIGH;
break;
default:
break;
}
should_publish = should_publish || (!old_fan_mode.has_value()) ||
(old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON));
@@ -1144,6 +1146,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
case (uint8_t) hon_protocol::ConditioningMode::AUTO:
this->mode = CLIMATE_MODE_HEAT_COOL;
break;
default:
break;
}
}
should_publish = should_publish || (old_mode != this->mode);
@@ -40,6 +40,8 @@ void decode_mitsubishi_heavy_zmp(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENG
case MITSUBISHI_HEAVY_ZMP_ECONO:
climate.preset = climate::CLIMATE_PRESET_ECO;
break;
default:
break;
}
uint8_t swing_h = frame[BYTE_SWING_H] & HORIZONTAL_SWING_MASK;
@@ -260,6 +260,8 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) {
case HITACHI_AC344_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
default:
break;
}
} else {
this->mode = climate::CLIMATE_MODE_OFF;
@@ -294,6 +296,8 @@ bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) {
case HITACHI_AC344_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
default:
break;
}
return true;
}
@@ -255,6 +255,8 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) {
case HITACHI_AC424_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
default:
break;
}
} else {
this->mode = climate::CLIMATE_MODE_OFF;
@@ -289,6 +291,8 @@ bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) {
case HITACHI_AC424_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
default:
break;
}
return true;
}
@@ -69,7 +69,7 @@ size_t ImprovBase::get_formatted_next_url_(char *buffer, size_t buffer_size) {
void ImprovBase::add_next_url_(improv::RpcResponseBuilder &builder, size_t max_len) {
// The builder rejects strings above 254 bytes, so anything longer than this
// buffer could never be sent anyway
char url_buffer[256];
char url_buffer[256] = {};
size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
if (len == 0) {
return;
+2
View File
@@ -61,6 +61,8 @@ void Kuntze::on_read_holding_registers(uint16_t start_address, std::span<const u
if (this->oci_sensor_ != nullptr)
this->oci_sensor_->publish_state(value);
break;
default:
break;
}
}
+9 -16
View File
@@ -407,12 +407,10 @@ void LD2410Component::handle_periodic_data_() {
}
#ifdef USE_NUMBER
std::function<void()> set_number_value(number::Number *n, float value) {
void set_number_value(number::Number *n, float value) {
if (n != nullptr && (!n->has_state() || n->state != value)) {
n->state = value;
return [n, value]() { n->publish_state(value); };
n->publish_state(value);
}
return []() {};
}
#endif
@@ -560,29 +558,24 @@ bool LD2410Component::handle_ack_data_() {
Moving distance range: 13th byte
Still distance range: 14th byte
*/
std::vector<std::function<void()>> updates;
updates.push_back(set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]));
updates.push_back(set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]));
set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]);
set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]);
/*
Moving Sensitivities: 15~23th bytes
*/
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) {
updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]));
for (size_t i = 0; i < TOTAL_GATES; i++) {
set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]);
}
/*
Still Sensitivities: 24~32th bytes
*/
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) {
updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]));
for (size_t i = 0; i < TOTAL_GATES; i++) {
set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]);
}
/*
None Duration: 33~34th bytes
*/
updates.push_back(
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32])));
for (auto &update : updates) {
update();
}
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32]));
#endif
break;
}
+37 -76
View File
@@ -9,6 +9,7 @@
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cinttypes>
@@ -19,23 +20,6 @@ namespace esphome::ld2450 {
ESPHOME_LOG_TAG(TAG, "ld2450");
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
enum ZoneType : uint8_t {
ZONE_DISABLED = 0,
ZONE_DETECTION = 1,
ZONE_FILTER = 2,
};
enum PeriodicData : uint8_t {
TARGET_X = 4,
TARGET_Y = 6,
@@ -54,61 +38,20 @@ enum AckData : uint8_t {
COMMAND_STATUS = 7,
};
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
const uint8_t value;
};
// Index + 1 is the BaudRate value; the select options use the same order
PROGMEM_STRING_TABLE(BaudRateStrings, "9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800");
static_assert(BAUD_RATE_9600 == 1 && BAUD_RATE_460800 == BaudRateStrings::COUNT, "BaudRate is index + 1");
// Index is the Direction value
PROGMEM_STRING_TABLE(DirectionStrings, "Approaching", "Moving away", "Stationary", "NA");
static_assert(DIRECTION_APPROACHING == 0 && DIRECTION_MOVING_AWAY == 1 && DIRECTION_STATIONARY == 2 &&
DIRECTION_NA == 3,
"Direction is the table index");
// Index is the ZoneType value; the select options use the same order
PROGMEM_STRING_TABLE(ZoneTypeStrings, "Disabled", "Detection", "Filter");
static_assert(ZONE_DISABLED == 0 && ZONE_DETECTION == 1 && ZONE_FILTER == 2, "ZoneType is the table index");
struct Uint8ToString {
const uint8_t value;
const char *str;
};
constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
};
constexpr Uint8ToString DIRECTION_BY_UINT[] = {
{DIRECTION_APPROACHING, "Approaching"},
{DIRECTION_MOVING_AWAY, "Moving away"},
{DIRECTION_STATIONARY, "Stationary"},
{DIRECTION_NA, "NA"},
};
constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
{ZONE_DISABLED, "Disabled"},
{ZONE_DETECTION, "Detection"},
{ZONE_FILTER, "Filter"},
};
constexpr StringToUint8 ZONE_TYPE_BY_STR[] = {
{"Disabled", ZONE_DISABLED},
{"Detection", ZONE_DETECTION},
{"Filter", ZONE_FILTER},
};
// Baud rates in the same order as BAUD_RATES_BY_STR for index-based lookup
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// Helper functions for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
for (const auto &entry : arr) {
if (str == entry.str)
return entry.value;
}
return 0xFF; // Not found
}
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
for (const auto &entry : arr) {
if (value == entry.value)
return entry.str;
}
return ""; // Not found
}
// Baud rates in the same order as BaudRateStrings for index-based lookup
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// LD2450 UART Serial Commands
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
@@ -546,7 +489,12 @@ void LD2450Component::handle_periodic_data_() {
if (this->direction_dedup_[index].next(direction)) {
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) {
tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
char direction_str[DirectionStrings::MAX_LENGTH + 1];
ESPHOME_strncpy_P(
direction_str,
reinterpret_cast<ESPHOME_PGM_P>(DirectionStrings::get_progmem_str(direction, DirectionStrings::LAST_INDEX)),
sizeof(direction_str));
tsd->publish_state(direction_str);
}
}
#endif
@@ -864,8 +812,15 @@ void LD2450Component::set_bluetooth(bool enable) {
// Set Baud rate
void LD2450Component::set_baud_rate(const char *state) {
const int16_t index = progmem_string_table_find<BaudRateStrings>(state, strlen(state));
if (index >= 0) {
this->set_baud_rate(static_cast<BaudRate>(index + 1));
}
}
void LD2450Component::set_baud_rate(BaudRate rate) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
const uint8_t cmd_value[2] = {rate, 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); });
}
@@ -873,8 +828,14 @@ void LD2450Component::set_baud_rate(const char *state) {
// Set Zone Type - one of: Disabled, Detection, Filter
void LD2450Component::set_zone_type(const char *state) {
ESP_LOGV(TAG, "Set zone type: %s", state);
uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state);
this->zone_type_ = zone_type;
const int16_t index = progmem_string_table_find<ZoneTypeStrings>(state, strlen(state));
if (index >= 0) {
this->set_zone_type(static_cast<ZoneType>(index));
}
}
void LD2450Component::set_zone_type(ZoneType type) {
this->zone_type_ = type;
this->send_set_zone_command_();
}
@@ -882,7 +843,7 @@ void LD2450Component::set_zone_type(const char *state) {
void LD2450Component::publish_zone_type() {
#ifdef USE_SELECT
if (this->zone_type_select_ != nullptr) {
this->zone_type_select_->publish_state(find_str(ZONE_TYPE_BY_UINT, this->zone_type_));
this->zone_type_select_->publish_state(static_cast<size_t>(this->zone_type_));
}
#endif
}
+19
View File
@@ -55,6 +55,23 @@ enum Direction : uint8_t {
DIRECTION_UNDEFINED = 4,
};
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
enum ZoneType : uint8_t {
ZONE_DISABLED = 0,
ZONE_DETECTION = 1,
ZONE_FILTER = 2,
};
// Target coordinate struct
struct Target {
int16_t x;
@@ -121,7 +138,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
void set_bluetooth(bool enable);
void set_multi_target(bool enable);
void set_baud_rate(const char *state);
void set_baud_rate(BaudRate rate);
void set_zone_type(const char *state);
void set_zone_type(ZoneType type);
void publish_zone_type();
void factory_reset();
#ifdef USE_TEXT_SENSOR
@@ -35,6 +35,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
selects = select.sub_selects(config, parent=hub)
# Option lists follow the order of the C++ enums; the hub maps by index
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
@@ -4,7 +4,7 @@ namespace esphome::ld2450 {
void BaudRateSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_baud_rate(this->option_at(index));
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
}
} // namespace esphome::ld2450
@@ -4,7 +4,7 @@ namespace esphome::ld2450 {
void ZoneTypeSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_zone_type(this->option_at(index));
this->parent_->set_zone_type(static_cast<ZoneType>(index));
}
} // namespace esphome::ld2450
+11 -3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <memory>
@@ -24,10 +25,17 @@
namespace esphome::ld24xx {
// Helper to find index of value in constexpr array
template<size_t N> optional<size_t> find_index(const uint32_t (&arr)[N], uint32_t value) {
// Index of value in a constant table that may live in flash, or none
template<typename T, size_t N> optional<size_t> find_index(const T (&arr)[N], T value) {
static_assert(sizeof(T) == 1 || sizeof(T) == 4, "flash tables are read as bytes or aligned 32 bit words");
for (size_t i = 0; i < N; i++) {
if (arr[i] == value)
T entry;
if constexpr (sizeof(T) == 1) {
entry = progmem_read_byte(reinterpret_cast<const uint8_t *>(&arr[i]));
} else {
entry = arr[i]; // an aligned 32 bit read from flash is safe on ESP8266
}
if (entry == value)
return i;
}
return {};
+2
View File
@@ -218,6 +218,8 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) {
case 10:
buf[i] = j;
break;
default:
break;
}
}
} else {
+1 -1
View File
@@ -38,7 +38,7 @@ class MCP23016 final : public Component, public i2c::I2CDevice, public gpio_expa
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(MCP23016 *arg);
static void gpio_intr(MCP23016 *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -10,7 +10,7 @@ enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANG
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
// ISR in flash.
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg);
void mcp23xxx_gpio_intr(Component *arg);
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
public:
+2
View File
@@ -81,6 +81,8 @@ void Mcp4461Component::initialize_terminal_disabled(Mcp4461WiperIdx wiper, char
case 'w':
this->reg_[wiper_idx].terminal_w = false;
break;
default:
break;
}
}
+2 -2
View File
@@ -244,9 +244,9 @@ class MipiSpi : public display::Display,
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
// Don't spam the log after setup
if (this->init_sequence_len_ == 0) {
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
esph_log_v(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
} else {
esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
esph_log_d(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
}
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->enable();
+2
View File
@@ -254,6 +254,8 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
case IRNoblexMode::IR_NOBLEX_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
default:
break;
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ class PCA6416AComponent final : public Component,
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(PCA6416AComponent *arg);
static void gpio_intr(PCA6416AComponent *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
+1 -1
View File
@@ -29,7 +29,7 @@ class PCA9505Component final : public Component,
void dump_config() override;
protected:
static void IRAM_ATTR gpio_intr(PCA9505Component *arg);
static void gpio_intr(PCA9505Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
+1 -1
View File
@@ -38,7 +38,7 @@ class PCA9554Component final : public Component,
#endif
protected:
static void IRAM_ATTR gpio_intr(PCA9554Component *arg);
static void gpio_intr(PCA9554Component *arg);
bool read_inputs_();
bool write_register_(uint8_t reg, uint16_t value) { return this->write_address_(reg * this->reg_width_, value); }
+1 -1
View File
@@ -33,7 +33,7 @@ class PCF8574Component final : public Component,
void dump_config() override;
protected:
static void IRAM_ATTR gpio_intr(PCF8574Component *arg);
static void gpio_intr(PCF8574Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -25,7 +25,7 @@ class PI4IOE5V6408Component final : public Component,
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(PI4IOE5V6408Component *arg);
static void gpio_intr(PI4IOE5V6408Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
+2
View File
@@ -656,6 +656,8 @@ void Pipsolar::handle_qpiws_(const char *message) {
this->publish_binary_sensor_(enabled, this->warning_battery_equalization_);
value_warnings_present |= enabled.value_or(false);
break;
default:
break;
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ class QMC5883LComponent final : public PollingComponent, public i2c::I2CDevice {
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
protected:
static void IRAM_ATTR gpio_intr(QMC5883LComponent *arg);
static void gpio_intr(QMC5883LComponent *arg);
void read_sensor_();
QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ};
+21
View File
@@ -39,13 +39,16 @@ from esphome.const import (
CONF_REFERENCE_TEMPERATURE,
CONF_SEND_EVERY,
CONF_SEND_FIRST_AT,
CONF_SENSOR_ID,
CONF_STATE_CLASS,
CONF_TEMPERATURE,
CONF_TEMPERATURE_OFFSET,
CONF_TIMEOUT,
CONF_TO,
CONF_TRIGGER_ID,
CONF_TYPE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_USE_FAHRENHEIT,
CONF_VALUE,
CONF_WEB_SERVER,
CONF_WINDOW_SIZE,
@@ -310,6 +313,7 @@ OrFilter = sensor_ns.class_("OrFilter", Filter)
CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
RHCorrectionFilter = sensor_ns.class_("RHCorrectionFilter", Filter)
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
@@ -1281,6 +1285,23 @@ async def calibrate_ntc_temperature_filter_to_code(config, filter_id):
)
@FILTER_REGISTRY.register(
"rh_correction",
RHCorrectionFilter,
cv.Schema(
{
cv.Required(CONF_TEMPERATURE_OFFSET): cv.templatable(cv.float_),
cv.Required(CONF_SENSOR_ID): cv.use_id(Sensor),
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}
),
)
async def rh_correction_filter_to_code(config: ConfigType, filter_id: ID) -> MockObj:
sensor = await cg.get_variable(config[CONF_SENSOR_ID])
offset = await cg.templatable(config[CONF_TEMPERATURE_OFFSET], [], cg.float_)
return cg.new_Pvariable(filter_id, offset, sensor, config[CONF_USE_FAHRENHEIT])
def _mean(xs):
return sum(xs) / len(xs)
+42
View File
@@ -493,6 +493,48 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
return temp;
}
// RHCorrectionFilter
static constexpr float MAGNUS_B = 17.67f, MAGNUS_C = 243.5f;
static bool rh_correction_temp_in_band(float t) { return t >= -40.0f && t <= 50.0f; }
optional<float> RHCorrectionFilter::new_value(float value) {
if (!this->temperature_sensor_->has_state()) {
if (!this->warned_) {
ESP_LOGW(TAG, "RHCorrectionFilter(%p): no temperature yet, passing humidity through uncorrected", this);
this->warned_ = true;
}
return value;
}
float offset = this->temperature_offset_.value();
float tc = this->temperature_sensor_->get_state();
if (this->use_fahrenheit_) {
tc = fahrenheit_to_celsius(tc);
offset /= 1.8f;
}
const float tnc = tc - offset;
// A NaN temperature or offset is a faulted input: keep the last corrected value
if (std::isnan(tnc)) {
if (!this->warned_) {
ESP_LOGW(TAG, "RHCorrectionFilter(%p): temperature or offset is NaN, dropping humidity updates", this);
this->warned_ = true;
}
return {};
}
if (!rh_correction_temp_in_band(tc) || !rh_correction_temp_in_band(tnc)) {
if (!this->warned_) {
ESP_LOGW(TAG, "RHCorrectionFilter(%p): Temperature out of range -40..50 °C. Tc=%f Tnc=%f", this, tc, tnc);
this->warned_ = true;
}
return value;
}
this->warned_ = false;
const float rh =
clamp(value * std::exp(MAGNUS_B * MAGNUS_C * (tnc - tc) / ((tnc + MAGNUS_C) * (tc + MAGNUS_C))), 0.0f, 100.0f);
ESP_LOGVV(TAG, "RHCorrectionFilter(%p)::new_value(%f) -> %f", this, value, rh);
return rh;
}
// StreamingFilter (base class)
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
: window_size_(window_size), send_first_at_(send_first_at) {}
+20
View File
@@ -647,6 +647,26 @@ class ToNTCTemperatureFilter : public Filter {
double c_;
};
/** RH Temperature correction filter
*
* Allows for Relative Humidity Correction when a temperature offset is applied to the temperature sensor.
* Uses the Magnus formula, rh = rh_nc * exp(b*(t_nc/(c+t_nc) - t/(c+t))) with b = 17.67, c = 243.5 °C;
* see https://en.wikipedia.org/wiki/Dew_point
*/
class RHCorrectionFilter : public Filter {
public:
RHCorrectionFilter(TemplatableFn<float> offset, const Sensor *temperature_sensor, bool use_fahrenheit)
: temperature_offset_(offset), temperature_sensor_(temperature_sensor), use_fahrenheit_(use_fahrenheit) {}
optional<float> new_value(float value) override;
protected:
TemplatableFn<float> temperature_offset_;
const Sensor *temperature_sensor_;
bool use_fahrenheit_;
bool warned_{false};
};
/** Base class for streaming filters (batch windows where window_size == send_every).
*
* When window_size equals send_every, we don't need a sliding window.
@@ -48,6 +48,8 @@ void SmlTextSensor::publish_val(const ObisInfo &obis_info) {
publish_state(reinterpret_cast<const char *>(obis_info.value.begin()), obis_info.value.size());
break;
}
default:
break;
}
}
+1 -1
View File
@@ -106,7 +106,7 @@ class ST25R : public PollingComponent, public nfc::Nfcc {
void finish_scan_();
void finalize_scan_();
void remove_tag_();
static void IRAM_ATTR isr(ST25R *arg);
static void isr(ST25R *arg);
GPIOPin *reset_pin_{nullptr};
InternalGPIOPin *irq_pin_{nullptr};
+1 -1
View File
@@ -107,7 +107,7 @@ class SX126x final : public Component,
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
protected:
static void IRAM_ATTR gpio_intr(SX126x *arg);
static void gpio_intr(SX126x *arg);
void configure_fsk_ook_();
void configure_lora_();
void set_packet_params_(uint8_t payload_length);
+1 -1
View File
@@ -91,7 +91,7 @@ class SX127x final : public Component,
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
protected:
static void IRAM_ATTR gpio_intr(SX127x *arg);
static void gpio_intr(SX127x *arg);
void configure_fsk_ook_();
void configure_lora_();
void set_mode_(uint8_t modulation, uint8_t mode);
+1 -1
View File
@@ -58,7 +58,7 @@ class TCA8418Component : public key_provider::KeyProvider, public Component, pub
#endif
/// Wakes the loop so queued events are read. Runs in interrupt context.
static void IRAM_ATTR interrupt_handler(TCA8418Component *arg);
static void interrupt_handler(TCA8418Component *arg);
protected:
/// Write the matrix / individual-input pin configuration. Returns false on I2C failure.
+1 -1
View File
@@ -27,7 +27,7 @@ class TCA9555Component final : public Component,
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(TCA9555Component *arg);
static void gpio_intr(TCA9555Component *arg);
// Virtual methods from GpioExpander base class — names come from base
bool digital_read_hw(uint8_t pin) override; // NOLINT(readability-identifier-naming)
bool digital_read_cache(uint8_t pin) override; // NOLINT(readability-identifier-naming)
+2
View File
@@ -191,6 +191,8 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) {
case TCL112_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
default:
break;
}
}
auto temp = TCL112_TEMP_MAX - remote_state[7];
+4
View File
@@ -39,6 +39,8 @@ void TEM3200Component::setup() {
ESP_LOGE(TAG, "STALE data. Data has not been updated since last fetch");
this->status_set_warning();
break;
default:
break;
}
}
@@ -127,6 +129,8 @@ void TEM3200Component::update() {
ESP_LOGE(TAG, "Warning: STALE data. Data has not been updated since last fetch");
this->status_set_warning();
return;
default:
break;
}
float temperature = convert_temperature(raw_temperature);
+13 -13
View File
@@ -1043,7 +1043,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
if (is_on) {
parse_cstr_param_(
request, ESPHOME_F("effect"), call,
static_cast<light::LightCall &(light::LightCall::*) (const char *, size_t)>(&decltype(call)::set_effect));
static_cast<light::LightCall &(light::LightCall::*)(const char *, size_t)>(&decltype(call)::set_effect));
}
DEFER_ACTION(call, call.perform());
@@ -1404,7 +1404,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
parse_cstr_param_(
request, ESPHOME_F("value"), call,
static_cast<text::TextCall &(text::TextCall::*) (const char *, size_t)>(&decltype(call)::set_value));
static_cast<text::TextCall &(text::TextCall::*)(const char *, size_t)>(&decltype(call)::set_value));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1464,7 +1464,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
auto call = obj->make_call();
parse_cstr_param_(
request, ESPHOME_F("option"), call,
static_cast<select::SelectCall &(select::SelectCall::*) (const char *, size_t)>(&decltype(call)::set_option));
static_cast<select::SelectCall &(select::SelectCall::*)(const char *, size_t)>(&decltype(call)::set_option));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1522,26 +1522,26 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
// Parse string mode parameters
parse_cstr_param_(
request, ESPHOME_F("mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(&decltype(call)::set_mode));
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(&decltype(call)::set_mode));
parse_cstr_param_(request, ESPHOME_F("fan_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
&decltype(call)::set_fan_mode));
parse_cstr_param_(request, ESPHOME_F("swing_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
&decltype(call)::set_swing_mode));
parse_cstr_param_(request, ESPHOME_F("preset"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
&decltype(call)::set_preset));
// Parse temperature parameters
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
using ClimateCall = decltype(call);
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_high));
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_low));
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1818,8 +1818,8 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
auto call = obj->make_call();
parse_cstr_param_(
request, ESPHOME_F("code"), call,
static_cast<alarm_control_panel::AlarmControlPanelCall &(
alarm_control_panel::AlarmControlPanelCall::*) (const char *, size_t)>(&decltype(call)::set_code));
static_cast<alarm_control_panel::AlarmControlPanelCall &(alarm_control_panel::AlarmControlPanelCall::*)(
const char *, size_t)>(&decltype(call)::set_code));
static constexpr ActionEntry<alarm_control_panel::AlarmControlPanelCall> ACTIONS[] PROGMEM = {
{"disarm", &alarm_control_panel::AlarmControlPanelCall::disarm},
@@ -1893,7 +1893,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
// Parse mode parameter
parse_cstr_param_(
request, ESPHOME_F("mode"), base_call,
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*) (const char *, size_t)>(
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*)(const char *, size_t)>(
&water_heater::WaterHeaterCall::set_mode));
// Parse temperature parameters
+6 -3
View File
@@ -450,9 +450,12 @@ uart::UARTFlushResult WeikaiChannel::flush() {
size_t WeikaiChannel::xfer_fifo_to_buffer_() {
size_t total = 0;
size_t to_transfer;
size_t free;
while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) {
while (true) {
size_t to_transfer = this->rx_in_fifo_();
size_t free = this->receive_buffer_.free();
if (to_transfer == 0 || free == 0) {
break;
}
// while bytes in fifo and some room in the buffer we transfer
if (to_transfer > XFER_MAX_SIZE)
to_transfer = XFER_MAX_SIZE; // we can only do so much
@@ -309,6 +309,8 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
case WHIRLPOOL_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
default:
break;
}
}
+3 -7
View File
@@ -16,6 +16,7 @@
#endif
#include "esphome/core/string_ref.h"
#include <algorithm>
#include <atomic>
#include <limits>
#include <span>
@@ -813,13 +814,8 @@ class WiFiComponent final : public Component {
}
bool all_networks_hidden_() const {
if (this->sta_.empty())
return false;
for (const auto &ap : this->sta_) {
if (!ap.get_hidden())
return false;
}
return true;
return !this->sta_.empty() &&
std::all_of(this->sta_.begin(), this->sta_.end(), [](const WiFiAP &ap) { return ap.get_hidden(); });
}
bool wifi_loop_();
@@ -62,7 +62,6 @@ static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid
#ifdef USE_WIFI_AP
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#endif // USE_WIFI_AP
static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -1037,7 +1036,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
s_sta_started = true;
// re-apply power save mode
wifi_apply_power_save_();
#ifdef SOC_WIFI_SUPPORT_5G
@@ -1046,7 +1044,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) {
ESP_LOGV(TAG, "STA stop");
s_sta_started = false;
s_sta_connecting = false;
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
@@ -166,8 +166,9 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
case CYW43_LINK_NONET:
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
default:
return WiFiSTAConnectStatus::IDLE;
}
return WiFiSTAConnectStatus::IDLE;
}
int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) {
@@ -5,7 +5,7 @@ namespace esphome::xiaomi_hhccpot002 {
ESPHOME_LOG_TAG(TAG, "xiaomi_hhccpot002");
void XiaomiHHCCPOT002 ::dump_config() {
void XiaomiHHCCPOT002::dump_config() {
ESP_LOGCONFIG(TAG, "Xiaomi HHCCPOT002");
LOG_SENSOR(" ", "Moisture", this->moisture_);
LOG_SENSOR(" ", "Conductivity", this->conductivity_);
@@ -40,8 +40,9 @@ static void advertise(k_work *work) {
truncate_name_to(name, sizeof(name), app_name.c_str(), app_name.length(),
App.is_name_add_mac_suffix_enabled() ? MAC_ADDRESS_SUFFIX_WITH_SEPARATOR_LEN : 0);
const uint8_t name_type = name_len < app_name.length() ? BT_DATA_NAME_SHORTENED : BT_DATA_NAME_COMPLETE;
static constexpr uint8_t FLAGS[] = {BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR};
const bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_FLAGS, FLAGS, sizeof(FLAGS)),
BT_DATA(name_type, name, static_cast<uint8_t>(name_len)),
};
@@ -255,6 +256,8 @@ static const char *role_str(uint8_t role) {
return "Central";
case BT_CONN_ROLE_PERIPHERAL:
return "Peripheral";
default:
break;
}
return "Unknown";
@@ -80,6 +80,8 @@ static const char *swap_type_str(uint8_t type) {
return "revert";
case BOOT_SWAP_TYPE_FAIL:
return "fail";
default:
break;
}
return "unknown";
+4 -2
View File
@@ -59,13 +59,15 @@ template<typename T> class PlainAtomic {
constexpr PlainAtomic(T value) : value_(value) {}
T load(std::memory_order order = std::memory_order_seq_cst) const {
T value = value_;
if (order != std::memory_order_relaxed)
if (order != std::memory_order_relaxed) {
ESPHOME_LFQ_COMPILER_BARRIER(); // acquire: later reads may not hoist above this load
}
return value;
}
void store(T value, std::memory_order order = std::memory_order_seq_cst) {
if (order != std::memory_order_relaxed)
if (order != std::memory_order_relaxed) {
ESPHOME_LFQ_COMPILER_BARRIER(); // release: earlier writes may not sink below this store
}
value_ = value;
}
T fetch_add(T amount, std::memory_order /*order*/ = std::memory_order_seq_cst) {
+1 -1
View File
@@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.2.0
esptool==5.6.0
click==8.5.0
aioesphomeapi==46.18.0
aioesphomeapi==46.19.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.5
puremagic==2.2.0
+2 -2
View File
@@ -1,4 +1,4 @@
# Useful stuff when working in a development environment
clang-format==22.1.8 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
clang-tidy==22.1.8
clang-format==23.1.3 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
clang-tidy==23.1.3
yamllint==1.38.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
+2
View File
@@ -2711,6 +2711,8 @@ def build_message_type(
o += " const ProtoFieldValue value(data, scalar);\n"
o += " switch (tag) {\n"
o += indent("\n".join(decode), " ").replace("this->", "msg.") + "\n"
o += " default:\n"
o += " break;\n"
o += " }\n"
o += "}\n"
cpp += o
+8 -14
View File
@@ -319,21 +319,15 @@ def run_tidy(
disabled_checks = []
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx")):
# MMIO peripheral access on these bare-metal platforms is all
# fixed-address.
disabled_checks.append("-clang-analyzer-core.FixedAddressDereference")
# fixed-address. clang-tidy 23 moved the checker under optin.
disabled_checks += [
"-clang-analyzer-core.FixedAddressDereference",
"-clang-analyzer-optin.core.FixedAddressDereference",
]
if args.environment.startswith("host"):
# glibc's assert() expands to a call the check mistakes for a
# constant condition.
disabled_checks += ["-misc-static-assert", "-cert-dcl03-c"]
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx", "host")):
# bugprone-pointer-arithmetic-on-polymorphic-object (and its
# cert-ctr56-cpp alias) crashes clang-tidy 22 with infinite matcher
# recursion on lvgl_esphome.h under the RP2 defines and on the
# all-include file under the host defines.
disabled_checks += [
"-bugprone-pointer-arithmetic-on-polymorphic-object",
"-cert-ctr56-cpp",
]
if os.path.normpath(path) in ANALYZER_SKIPPED_FILES:
disabled_checks.append("-clang-analyzer-*")
if disabled_checks:
@@ -523,7 +517,7 @@ def main():
failed_files = []
timings = []
try:
executable = get_binary("clang-tidy", 22)
executable = get_binary("clang-tidy", 23)
task_queue = queue.Queue(args.jobs)
lock = threading.Lock()
for _ in range(args.jobs):
@@ -571,13 +565,13 @@ def main():
try:
try:
subprocess.call(
["clang-apply-replacements-22", tmpdir], close_fds=False
["clang-apply-replacements-23", tmpdir], close_fds=False
)
except FileNotFoundError:
subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False)
except FileNotFoundError:
print(
"Error please install clang-apply-replacements-22 or clang-apply-replacements.\n",
"Error please install clang-apply-replacements-23 or clang-apply-replacements.\n",
file=sys.stderr,
)
except:
@@ -621,6 +621,69 @@ def test_uc8279_width_must_be_a_multiple_of_8(
)
GDEY029Z95 = "goodisplay-gdey029z95-2.9"
def _gdey029z95_config(**overrides: Any) -> ConfigType:
config: ConfigType = {
"id": "test_display",
"model": GDEY029Z95,
CONF_DC_PIN: 21,
CONF_RESET_PIN: 22,
CONF_BUSY_PIN: 23,
}
config.update(overrides)
return config
def test_gdey029z95_uses_the_good_display_driver() -> None:
"""The GDEY029Z95 uses EPaperGoodDisplay3C with the WeAct init sequence."""
model = MODELS[GDEY029Z95.upper()]
assert model.class_name == "EPaperGoodDisplay3C"
assert model.get_init_sequence({}) == (
(0x01, 0x27, 0x01, 0x00),
(0x11, 0x03),
(0x3C, 0x05),
(0x18, 0x80),
(0x21, 0x00, 0x80),
)
@pytest.mark.parametrize("pin", [CONF_RESET_PIN, CONF_BUSY_PIN])
def test_gdey029z95_requires_reset_and_busy_pins(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
pin: str,
) -> None:
"""Only a hardware reset wakes the panel from deep sleep, and refreshes are timed by busy."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = _gdey029z95_config()
del config[pin]
with pytest.raises(cv.Invalid, match=pin):
run_schema_validation(config)
def test_gdey029z95_has_no_partial_update(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
run_schema_validation(_gdey029z95_config())
with pytest.raises(cv.Invalid, match="does not support partial update"):
run_schema_validation(_gdey029z95_config(full_update_every=2))
def test_uc8279_x4pro_code_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
@@ -0,0 +1,24 @@
esphome:
name: test
esp32:
board: esp32dev
sensor:
- platform: template
id: temperature
filters:
- offset: -7.3
- platform: template
id: constant_offset
filters:
- rh_correction:
sensor_id: temperature
temperature_offset: -7.3
- platform: template
id: templated_offset
filters:
- rh_correction:
sensor_id: temperature
temperature_offset: !lambda return -7.3;
use_fahrenheit: true
@@ -0,0 +1,60 @@
"""The rh_correction filter: schema and the constructor call it generates."""
from collections.abc import Callable
from pathlib import Path
import re
import pytest
from esphome.components.sensor import validate_filters
import esphome.config_validation as cv
@pytest.mark.parametrize(
"filters",
[
pytest.param([{"rh_correction": {"sensor_id": "t"}}], id="no_offset"),
pytest.param([{"rh_correction": {"temperature_offset": -7.3}}], id="no_sensor"),
pytest.param(
[
{
"rh_correction": {
"sensor_id": "t",
"temperature_offset": -7.3,
"use_fahrenheit": "maybe",
}
}
],
id="bad_fahrenheit",
),
],
)
def test_rh_correction_rejects_incomplete_config(filters: list[dict]) -> None:
with pytest.raises(cv.Invalid):
validate_filters(filters)
def test_rh_correction_defaults_to_celsius() -> None:
config = validate_filters(
[{"rh_correction": {"sensor_id": "t", "temperature_offset": -7.3}}]
)
assert config[0]["rh_correction"]["use_fahrenheit"] is False
def test_rh_correction_codegen(
generate_main: Callable[[str | Path], str],
) -> None:
main_cpp = generate_main("tests/component_tests/sensor/rh_correction.yaml")
# A constant offset is wrapped in a stateless lambda so the filter stores a function pointer
assert re.search(
r"sensor::RHCorrectionFilter\(\[\]\(\) -> float \{\s*return -7\.3f;\s*\}, "
r"temperature, false\);\s*constant_offset->add_filter\(",
main_cpp,
)
# A templated offset keeps the user's lambda body and passes use_fahrenheit through
assert re.search(
r"sensor::RHCorrectionFilter\(\[\]\(\) -> float \{[^}]*return -7\.3;[^}]*\}, "
r"temperature, true\);\s*templated_offset->add_filter\(",
main_cpp,
)
@@ -0,0 +1,164 @@
#include <gtest/gtest.h>
#include <utility>
#include <vector>
#include "../common.h"
#include "esphome/components/epaper_spi/epaper_goodisplay_3c.h"
namespace esphome::epaper_spi::testing {
namespace {
class TestableGoodDisplay3C : public EPaperGoodDisplay3C {
public:
TestableGoodDisplay3C(uint16_t width, uint16_t height) : EPaperGoodDisplay3C("test", width, height, nullptr, 0) {}
void install(spi::SPIDelegate *delegate) {
this->delegate_ = delegate;
this->set_dc_pin(&this->dc);
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
}
void draw(int x, int y, Color color) { this->draw_pixel_at(x, y, color); }
/// Both planes of one push; returns how many calls it took.
int run_push() {
int calls = 1;
while (!this->transfer_data())
calls++;
return calls;
}
/// A whole update with the frame already drawn, run through loop() as the main loop would.
void run_update() {
this->set_auto_clear(false);
this->state_ = EPaperState::UPDATE;
const uint32_t deadline = millis() + 2000;
while (this->state_ != EPaperState::IDLE) {
ASSERT_LT(millis(), deadline) << "update did not finish";
this->loop();
}
}
using EPaperGoodDisplay3C::REFRESH_START_MS;
RecordingPin dc;
};
/// Also notes when each command byte was sent.
class TimedCommandDelegate : public RecordingDelegate {
public:
using RecordingDelegate::RecordingDelegate;
void write_array(const uint8_t *ptr, size_t length) override {
if (!this->dc_->level) {
for (size_t i = 0; i != length; i++)
this->sent.emplace_back(ptr[i], millis());
}
RecordingDelegate::write_array(ptr, length);
}
/// When the n-th (from 0) copy of a command was sent.
uint32_t time_of(uint8_t command, int n = 0) const {
for (const auto &[byte, time] : this->sent) {
if (byte == command && n-- == 0)
return time;
}
ADD_FAILURE() << "command " << int(command) << " not sent";
return 0;
}
std::vector<std::pair<uint8_t, uint32_t>> sent;
};
using Bytes = std::vector<uint8_t>;
const Color RED(255, 0, 0);
/// One row of 16 pixels: white, black, red, then white. The red plane goes first as 1=red, then the black/white
/// plane as 1=white with red pixels sent as 0.
TEST(EPaperGoodDisplay3CTransfer, PixelsReachThePanel) {
TestableGoodDisplay3C display(16, 1);
RecordingDelegate bus(&display.dc);
display.install(&bus);
display.fill(Color::WHITE);
display.draw(1, 0, Color::BLACK);
display.draw(2, 0, RED);
EXPECT_EQ(display.run_push(), 1);
EXPECT_EQ(bus.commands, (Bytes{0x44, 0x45, 0x4E, 0x4F, 0x26, 0x24}));
EXPECT_EQ(bus.data[0x44], (Bytes{0x00, 0x01}));
EXPECT_EQ(bus.data[0x45], (Bytes{0x00, 0x00, 0x00, 0x00}));
EXPECT_EQ(bus.data[0x24], (Bytes{0x9F, 0xFF}));
EXPECT_EQ(bus.data[0x26], (Bytes{0x20, 0x00}));
}
/// A push that yields to the main loop after every chunk sends the same commands and bytes as one that runs to
/// completion in one call, and a second push starts again from the window setup.
TEST(EPaperGoodDisplay3CTransfer, ResumedPushMatches) {
// 128x20 pixels is 320 bytes per plane, so each plane takes three chunks of up to MAX_TRANSFER_SIZE.
constexpr int width = 128;
constexpr int height = 20;
const Color colors[] = {Color::WHITE, Color::BLACK, RED};
TestableGoodDisplay3C reference(width, height);
RecordingDelegate reference_bus(&reference.dc);
reference.install(&reference_bus);
TestableGoodDisplay3C display(width, height);
// Every data write takes longer than MAX_TRANSFER_TIME, so the push yields after each chunk.
RecordingDelegate bus(&display.dc, MAX_TRANSFER_TIME + 1);
display.install(&bus);
for (int y = 0; y != height; y++) {
for (int x = 0; x != width; x++) {
reference.draw(x, y, colors[(x * 7 + y * 3) % 3]);
display.draw(x, y, colors[(x * 7 + y * 3) % 3]);
}
}
EXPECT_EQ(reference.run_push(), 1);
EXPECT_GT(display.run_push(), 2);
EXPECT_EQ(bus.commands, reference_bus.commands);
EXPECT_EQ(bus.data[0x24].size(), 320u);
EXPECT_EQ(bus.data[0x24], reference_bus.data[0x24]);
EXPECT_EQ(bus.data[0x26], reference_bus.data[0x26]);
bus.clear();
display.run_push();
EXPECT_EQ(bus.commands, reference_bus.commands);
}
/// A whole update sends no power-on (0x22 0xF8) before the refresh (0x22 0xF7): with it, this panel does not
/// refresh. Power off (0x83) and deep sleep follow.
TEST(EPaperGoodDisplay3CTransfer, UpdateHasNoSeparatePowerOn) {
TestableGoodDisplay3C display(16, 1);
RecordingDelegate bus(&display.dc);
display.install(&bus);
display.fill(RED);
display.run_update();
EXPECT_EQ(bus.commands, (Bytes{0x44, 0x45, 0x4E, 0x4F, 0x26, 0x24, 0x4E, 0x4F, 0x22, 0x20, 0x22, 0x20, 0x10}));
EXPECT_EQ(bus.data[0x26], (Bytes{0xFF, 0xFF}));
EXPECT_EQ(bus.data[0x24], (Bytes{0x00, 0x00}));
EXPECT_EQ(bus.data[0x22], (Bytes{0xF7, 0x83}));
EXPECT_EQ(bus.data[0x10], (Bytes{0x01}));
}
/// Busy only goes high a little after master activation (0x20), so power off is not sent until REFRESH_START_MS
/// after the refresh starts.
TEST(EPaperGoodDisplay3CTransfer, WaitsForBusyAfterTheRefreshStarts) {
TestableGoodDisplay3C display(16, 1);
TimedCommandDelegate bus(&display.dc);
display.install(&bus);
display.fill(RED);
display.run_update();
// The first 0x20 starts the refresh; the second 0x22 selects power off.
EXPECT_GE(bus.time_of(0x22, 1) - bus.time_of(0x20, 0), TestableGoodDisplay3C::REFRESH_START_MS);
}
} // namespace
} // namespace esphome::epaper_spi::testing
@@ -209,6 +209,27 @@ display:
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# Good Display GDEY029Z95 2.9" 3-color e-paper (128x296, SSD1680)
- platform: epaper_spi
spi_id: spi_bus
model: goodisplay-gdey029z95-2.9
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# WeAct 4.2" 3-color e-paper (400x300, SSD1683)
- platform: epaper_spi
spi_id: spi_bus
+1
View File
@@ -30,6 +30,7 @@ class TestableLD2450 : public LD2450Component {
using LD2450Component::buffer_pos_;
using LD2450Component::readline_;
using LD2450Component::zone_config_;
using LD2450Component::zone_type_;
void feed(const std::vector<uint8_t> &data) {
for (uint8_t byte : data) {
@@ -0,0 +1,85 @@
#include <algorithm>
#include "common.h"
namespace esphome::ld2450::testing {
namespace {
class CapturingUARTComponent : public MockUARTComponent {
public:
void write_array(const uint8_t *data, size_t len) override {
this->written.insert(this->written.end(), data, data + len);
}
std::vector<uint8_t> written;
};
// Header, length 4, command 0xA1 (set baud rate): the rate byte and a zero follow.
constexpr uint8_t SET_BAUD_RATE_PREFIX[] = {0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xA1, 0x00};
// Header, length 28, command 0xC2 (set zone): the zone type byte and a zero follow.
constexpr uint8_t SET_ZONE_PREFIX[] = {0xFD, 0xFC, 0xFB, 0xFA, 0x1C, 0x00, 0xC2, 0x00};
// The two payload bytes after the first frame with this prefix, or -1 when it was not written.
template<size_t N> std::pair<int, int> payload_after(const std::vector<uint8_t> &w, const uint8_t (&prefix)[N]) {
auto it = std::search(w.begin(), w.end(), std::begin(prefix), std::end(prefix));
if (it == w.end() || static_cast<size_t>(w.end() - it) < N + 2)
return {-1, -1};
return {*(it + N), *(it + N + 1)};
}
} // namespace
class LD2450SelectNamesTest : public ::testing::Test {
protected:
void SetUp() override { this->ld2450_.set_uart_parent(&this->uart_); }
CapturingUARTComponent uart_;
TestableLD2450 ld2450_;
};
TEST_F(LD2450SelectNamesTest, BaudRateNamesSendTheirDeviceValue) {
static constexpr const char *NAMES[] = {"9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800"};
for (uint8_t i = 0; i < 8; i++) {
this->uart_.written.clear();
this->ld2450_.set_baud_rate(NAMES[i]);
const auto payload = payload_after(this->uart_.written, SET_BAUD_RATE_PREFIX);
EXPECT_EQ(payload.first, i + 1) << NAMES[i];
EXPECT_EQ(payload.second, 0) << NAMES[i];
}
}
TEST_F(LD2450SelectNamesTest, BaudRateEnumMatchesTheSelectOrder) {
this->ld2450_.set_baud_rate(BAUD_RATE_256000);
EXPECT_EQ(payload_after(this->uart_.written, SET_BAUD_RATE_PREFIX).first, 7);
}
TEST_F(LD2450SelectNamesTest, UnknownBaudRateNameWritesNothing) {
this->ld2450_.set_baud_rate("1200");
EXPECT_TRUE(this->uart_.written.empty());
}
TEST_F(LD2450SelectNamesTest, ZoneTypeNamesSetTheTypeAndSendIt) {
static constexpr const char *NAMES[] = {"Disabled", "Detection", "Filter"};
for (uint8_t i = 0; i < 3; i++) {
this->uart_.written.clear();
this->ld2450_.set_zone_type(NAMES[i]);
EXPECT_EQ(this->ld2450_.zone_type_, i) << NAMES[i];
const auto payload = payload_after(this->uart_.written, SET_ZONE_PREFIX);
EXPECT_EQ(payload.first, i) << NAMES[i];
EXPECT_EQ(payload.second, 0) << NAMES[i];
}
}
TEST_F(LD2450SelectNamesTest, ZoneTypeEnumMatchesTheSelectOrder) {
this->ld2450_.set_zone_type(ZONE_FILTER);
EXPECT_EQ(this->ld2450_.zone_type_, 2);
this->ld2450_.set_zone_type(ZONE_DETECTION);
EXPECT_EQ(this->ld2450_.zone_type_, 1);
}
TEST_F(LD2450SelectNamesTest, UnknownZoneTypeNameChangesNothing) {
this->ld2450_.set_zone_type(ZONE_DETECTION);
this->uart_.written.clear();
this->ld2450_.set_zone_type("Bogus");
EXPECT_EQ(this->ld2450_.zone_type_, 1);
EXPECT_TRUE(this->uart_.written.empty());
}
} // namespace esphome::ld2450::testing
+11
View File
@@ -0,0 +1,11 @@
import esphome.codegen as cg
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code_testing(config: ConfigType) -> None:
# filter.cpp only compiles with the define the real to_code emits for configured filters
cg.add_define("USE_SENSOR_FILTER")
manifest.to_code = to_code_testing
+18
View File
@@ -254,3 +254,21 @@ sensor:
lambda: return 42.0;
update_interval: 1s
state_class: "measurement_angle"
# Testing rh_correction filter
- platform: copy
source_id: source_sensor
name: "RH Correction Filter Fahrenheit"
filters:
- rh_correction:
sensor_id: source_sensor
temperature_offset: -10.0
use_fahrenheit: true
- platform: copy
source_id: source_sensor
name: "RH Correction Filter Template"
filters:
- rh_correction:
sensor_id: source_sensor
temperature_offset: !lambda return -10.0;
+66
View File
@@ -0,0 +1,66 @@
#include <gtest/gtest.h>
#include <cmath>
#include "esphome/components/sensor/filter.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::sensor::testing {
// 40 % read 7.3 °C warm at a true 25 °C, in single precision Magnus arithmetic
static constexpr float CORRECTED = 61.13f;
struct RhFixture {
Sensor temperature;
RHCorrectionFilter filter;
RhFixture(float (*offset)(), bool use_fahrenheit = false) : filter(offset, &this->temperature, use_fahrenheit) {}
};
TEST(RHCorrectionFilter, CorrectsForTheOffset) {
RhFixture f([]() -> float { return -7.3f; });
f.temperature.publish_state(25.0f);
EXPECT_NEAR(f.filter.new_value(40.0f).value_or(NAN), CORRECTED, 0.05f);
}
TEST(RHCorrectionFilter, PositiveOffsetLowersTheHumidity) {
RhFixture f([]() -> float { return 7.3f; });
f.temperature.publish_state(25.0f);
EXPECT_LT(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
}
TEST(RHCorrectionFilter, FahrenheitReadingsGiveTheSameResult) {
// 77 °F is 25 °C and -13.14 °F is -7.3 °C of offset
RhFixture f([]() -> float { return -13.14f; }, true);
f.temperature.publish_state(77.0f);
EXPECT_NEAR(f.filter.new_value(40.0f).value_or(NAN), CORRECTED, 0.05f);
}
TEST(RHCorrectionFilter, ClampsToOneHundredPercent) {
RhFixture f([]() -> float { return -7.3f; });
f.temperature.publish_state(25.0f);
EXPECT_FLOAT_EQ(f.filter.new_value(90.0f).value_or(NAN), 100.0f);
}
TEST(RHCorrectionFilter, PassesThroughWithoutAUsableTemperature) {
RhFixture f([]() -> float { return -7.3f; });
// No temperature reading yet
EXPECT_FLOAT_EQ(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
// Out of the -40..50 °C band
f.temperature.publish_state(60.0f);
EXPECT_FLOAT_EQ(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
// The band also covers the reconstructed enclosure temperature
f.temperature.publish_state(45.0f);
EXPECT_FLOAT_EQ(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
}
TEST(RHCorrectionFilter, DropsTheUpdateOnNaN) {
RhFixture f([]() -> float { return -7.3f; });
f.temperature.publish_state(NAN);
EXPECT_FALSE(f.filter.new_value(40.0f).has_value());
RhFixture nan_offset([]() -> float { return NAN; });
nan_offset.temperature.publish_state(25.0f);
EXPECT_FALSE(nan_offset.filter.new_value(40.0f).has_value());
}
} // namespace esphome::sensor::testing
@@ -0,0 +1,102 @@
esphome:
name: test-rh-correction
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: DEBUG
sensor:
- platform: template
name: "Temperature C"
id: temperature_c
accuracy_decimals: 1
- platform: template
name: "Temperature F"
id: temperature_f
accuracy_decimals: 1
- platform: template
name: "Humidity Source"
id: humidity_source
accuracy_decimals: 1
- platform: copy
source_id: humidity_source
name: "RH Celsius"
id: rh_celsius
accuracy_decimals: 2
filters:
- rh_correction:
sensor_id: temperature_c
temperature_offset: -7.3
- platform: copy
source_id: humidity_source
name: "RH Fahrenheit"
id: rh_fahrenheit
accuracy_decimals: 2
filters:
- rh_correction:
sensor_id: temperature_f
temperature_offset: -13.14
use_fahrenheit: true
script:
- id: test_rh_correction
then:
# Both temperature sensors read 25 °C after a -7.3 °C (-13.14 °F) offset
- sensor.template.publish:
id: temperature_c
state: 25.0
- sensor.template.publish:
id: temperature_f
state: 77.0
- sensor.template.publish:
id: humidity_source
state: 40.0
- delay: 20ms
# Clamped to 100 %
- sensor.template.publish:
id: humidity_source
state: 90.0
- delay: 20ms
# Out of band: passes through unchanged
- sensor.template.publish:
id: temperature_c
state: 60.0
- sensor.template.publish:
id: temperature_f
state: 140.0
- sensor.template.publish:
id: humidity_source
state: 40.0
- delay: 20ms
# NaN temperature: the update is dropped
- sensor.template.publish:
id: temperature_c
state: !lambda "return NAN;"
- sensor.template.publish:
id: temperature_f
state: !lambda "return NAN;"
- sensor.template.publish:
id: humidity_source
state: 50.0
- delay: 20ms
- sensor.template.publish:
id: temperature_c
state: 25.0
- sensor.template.publish:
id: temperature_f
state: 77.0
- sensor.template.publish:
id: humidity_source
state: 40.0
button:
- platform: template
name: "Test RH Correction"
on_press:
- script.execute: test_rh_correction
@@ -0,0 +1,55 @@
"""Test the sensor rh_correction filter numerically."""
from __future__ import annotations
from aioesphomeapi import ButtonInfo
import pytest
from .state_utils import InitialStateHelper, SensorStateCollector, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
from .wait_utils import wait_or_fail
# 40 % at 25 °C, measured 7.3 °C warmer, in single precision Magnus arithmetic
CORRECTED = pytest.approx(61.13, abs=0.05)
# Corrected, clamped, passed through out of band, corrected again; the NaN step is dropped
EXPECTED = [CORRECTED, 100.0, 40.0, CORRECTED]
@pytest.mark.asyncio
async def test_sensor_filters_rh_correction(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
collector = SensorStateCollector(sensor_names=["rh_celsius", "rh_fahrenheit"])
async with (
run_compiled(yaml_config),
api_client_connected() as client,
):
entities, _ = await client.list_entities_services()
collector.build_key_mapping(entities)
initial_state_helper = InitialStateHelper(entities)
client.subscribe_states(
initial_state_helper.on_state_wrapper(collector.on_state)
)
await initial_state_helper.wait_for_initial_states()
done = collector.add_waiter(
lambda: all(
len(values) == len(EXPECTED)
for values in collector.sensor_states.values()
)
)
button = require_entity(entities, "test_rh_correction", ButtonInfo, exact=True)
client.button_command(button.key)
await wait_or_fail(
done,
2.0,
"Timed out waiting for the corrected humidity values",
values=collector.sensor_states,
)
assert collector.sensor_states["rh_celsius"] == EXPECTED
# The same readings in °F give the same corrected humidity
assert collector.sensor_states["rh_fahrenheit"] == EXPECTED
@@ -2,10 +2,13 @@
# pylint: disable=protected-access
import ctypes
import json
import logging
import os
from pathlib import Path
import shlex
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
@@ -128,6 +131,65 @@ def test_pick_entry_prefers_esphome_tu() -> None:
assert idedata._pick_entry(entries)["file"].endswith("app.cpp")
def test_pick_entry_without_cxx_tu_raises() -> None:
"""A compile DB holding only C files has no usable translation unit."""
entries = [_entry(f"{ABS}build", f"{ABS}build/src/a.c", "gcc -c a.c")]
with pytest.raises(ValueError, match="no C\\+\\+ translation unit"):
idedata._pick_entry(entries)
def test_expand_response_files_absolute_path(tmp_path: Path) -> None:
"""An absolute response file path is read as is, not joined to the directory."""
rsp = tmp_path / "flags.rsp"
rsp.write_text("-DFROM_RSP=1")
other = tmp_path / "elsewhere"
other.mkdir()
assert idedata._expand_response_files([f"@{rsp}"], other) == ["-DFROM_RSP=1"]
def test_parse_entry_drops_bare_input_files() -> None:
"""A source file given without -c is not kept as a compiler flag."""
entry = _entry(
f"{ABS}build",
f"{ABS}build/src/esphome/x.cpp",
"g++ -DFOO x.cpp -std=gnu++20",
)
_, _, _, cxx_flags = idedata.parse_entry(entry)
assert cxx_flags == ["-std=gnu++20"]
@pytest.mark.parametrize(
("cxx_path", "expected"),
[
pytest.param("g++", "gcc", id="bare"),
pytest.param("/usr/bin/g++", "/usr/bin/gcc", id="path"),
pytest.param("xtensa-esp32-elf-g++.exe", "xtensa-esp32-elf-gcc.exe", id="exe"),
pytest.param("C:\\tools\\g++.exe", "C:\\tools\\gcc.exe", id="backslash"),
pytest.param("/usr/bin/clang++", "/usr/bin/clang++", id="clang"),
],
)
def test_cc_path_from_cxx(cxx_path: str, expected: str) -> None:
"""Only a g++ program name is rewritten to gcc."""
assert idedata._cc_path_from_cxx(cxx_path) == expected
@pytest.mark.parametrize(
("token", "expected"),
[
pytest.param("-includefoo.h", True, id="joined"),
pytest.param("-include", False, id="separate"),
pytest.param("-include-pch", False, id="pch"),
pytest.param("-DFOO", False, id="other"),
],
)
def test_is_joined_include(token: str, expected: bool) -> None:
"""Only the joined ``-includefoo.h`` spelling counts."""
assert idedata.is_joined_include(token) is expected
def test_pick_entry_falls_back_to_any_cxx_tu() -> None:
"""With no ``/src/esphome/`` TU present, the first C++ entry is the fallback."""
entries = [
@@ -345,6 +407,48 @@ def test_split_command_empty_returns_empty() -> None:
assert idedata._split_command(" ") == []
@pytest.fixture(name="fake_windows_argv")
def fixture_fake_windows_argv(monkeypatch: pytest.MonkeyPatch) -> list[bool]:
"""Run ``_split_command`` down its Windows branch on any host.
``CommandLineToArgvW`` is replaced by a shlex based stand-in; the returned
list records whether ``LocalFree`` released the argv block.
"""
freed: list[bool] = []
def command_line_to_argv(command: str, argc_ref: object) -> list[str]:
tokens = shlex.split(command)
argc_ref._obj.value = len(tokens) # type: ignore[attr-defined]
return tokens
windll = SimpleNamespace(
shell32=SimpleNamespace(CommandLineToArgvW=command_line_to_argv),
kernel32=SimpleNamespace(LocalFree=lambda _ptr: freed.append(True)),
)
monkeypatch.setattr(ctypes, "windll", windll, raising=False)
monkeypatch.setattr(idedata.os, "name", "nt")
return freed
def test_split_command_windows_branch(fake_windows_argv: list[bool]) -> None:
"""On Windows the command is tokenized by the OS parser and freed after."""
assert idedata._split_command("g++ -DFOO=1 -c x.cpp") == [
"g++",
"-DFOO=1",
"-c",
"x.cpp",
]
assert fake_windows_argv == [True]
def test_split_command_windows_branch_blank_command(
fake_windows_argv: list[bool],
) -> None:
"""A blank command yields no tokens without calling the OS parser."""
assert idedata._split_command(" ") == []
assert fake_windows_argv == []
@pytest.mark.skipif(os.name != "nt", reason="Windows argv tokenization")
def test_parse_entry_normalizes_windows_cxx_path() -> None:
"""A backslash compiler path is emitted forward-slashed; define unescaped."""
@@ -7,8 +7,10 @@ from typing import Any
import pytest
from esphome import final_validate as fv
import esphome.codegen as cg
from esphome.components import button, number, select, switch
from esphome.config import Config
from esphome.config_validation import Invalid
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -28,6 +30,7 @@ from esphome.core.entity_helpers import (
entity_duplicate_validator,
finalize_entity_strings,
get_base_entity_object_id,
inherit_property_from,
register_device_class,
register_icon,
register_unit_of_measurement,
@@ -1607,3 +1610,59 @@ async def test_sub_entities_create_from_whole_config(
assert str(var) == "rf_sw"
assert calls == [("rf_sw", "<esphome__rf_sw__fn>")]
assert setup_test_environment == ["hub->set_rf_switch(rf_sw)"]
def test_entity_duplicate_validator_without_name() -> None:
"""A config with no name is returned untouched and not tracked."""
validator = entity_duplicate_validator("sensor")
config = {CONF_ID: "no_name"}
assert validator(config) is config
assert not CORE.unique_ids
def test_entity_duplicate_validator_testing_mode_allows_duplicates() -> None:
validator = entity_duplicate_validator("sensor")
config = {CONF_NAME: "Temperature"}
validator(dict(config))
CORE.testing_mode = True
try:
assert validator(dict(config)) == config
finally:
CORE.testing_mode = False
def _inherit_config() -> tuple[Config, ID, dict[str, Any]]:
parent_id = ID("parent", is_declaration=True)
child_id = ID("child", is_declaration=True)
parent = {CONF_ID: parent_id, CONF_ICON: "mdi:parent"}
child = {CONF_ID: child_id, "source": ID("parent"), "inner": {"x": 1}}
config = Config()
config["sensor"] = [parent, child]
config.declare_ids.append((parent_id, ["sensor", 0, CONF_ID]))
config.declare_ids.append((child_id, ["sensor", 1, CONF_ID]))
return config, child_id, child
def test_inherit_property_from_path_inherits_value() -> None:
config, child_id, child = _inherit_config()
validator = inherit_property_from(["inner", CONF_ICON], "source")
token = fv.full_config.set(config)
try:
assert validator(child) is child
finally:
fv.full_config.reset(token)
assert config["sensor"][1]["inner"][CONF_ICON] == "mdi:parent"
def test_inherit_property_from_path_missing_is_ignored() -> None:
config, _, child = _inherit_config()
validator = inherit_property_from(["absent", CONF_ICON], "source")
token = fv.full_config.set(config)
try:
assert validator(child) is child
finally:
fv.full_config.reset(token)
assert "absent" not in config["sensor"][1]
+79
View File
@@ -22,6 +22,7 @@ from esphome.bundle import (
_add_bytes_to_tar,
_default_target_dir,
_find_used_secret_keys,
_validate_tar_members,
add_bundle_file,
add_secret_scan_dir,
extract_bundle,
@@ -1872,3 +1873,81 @@ def test_bundle_round_trip_with_secrets(tmp_path: Path) -> None:
manifest = read_bundle_manifest(bundle_path)
assert manifest.has_secrets is True
def test_add_secret_scan_dir_relative_path(tmp_path: Path) -> None:
"""A relative scan directory is taken as relative to the config directory."""
config_dir = _setup_config_dir(tmp_path)
(config_dir / "secrets.yaml").write_text("pw: hunter2\nunused: nope\n")
package_dir = config_dir / "pkg"
package_dir.mkdir()
(package_dir / "base.yml").write_text("password: !secret pw\n")
add_secret_scan_dir(Path("pkg"))
result = ConfigBundleCreator({}).create_bundle()
assert result.manifest[ManifestKey.HAS_SECRETS] is True
with tarfile.open(fileobj=io.BytesIO(result.data), mode="r:gz") as tar:
secrets_data = tar.extractfile("secrets.yaml").read().decode()
assert "hunter2" in secrets_data
assert "unused" not in secrets_data
def test_discover_files_skips_system_includes_c(tmp_path: Path) -> None:
"""System includes in includes_c are not local files."""
_setup_config_dir(tmp_path)
config: dict[str, Any] = {"esphome": {"includes_c": ["<system.h>"]}}
files = ConfigBundleCreator(config).discover_files()
assert [f.path for f in files] == ["test.yaml"]
def test_discover_files_ignores_missing_path_and_scalars(tmp_path: Path) -> None:
"""Absolute Path values that are not files and non-path values are ignored."""
_setup_config_dir(tmp_path)
config: dict[str, Any] = {
"missing": tmp_path / "does_not_exist.bin",
"number": 5,
"flag": True,
}
files = ConfigBundleCreator(config).discover_files()
assert [f.path for f in files] == ["test.yaml"]
def test_build_filtered_secrets_skips_files_outside_config_dir(
tmp_path: Path,
) -> None:
_setup_config_dir(tmp_path)
outside = tmp_path / "outside_secrets.yaml"
outside.write_text("k: v\n")
creator = ConfigBundleCreator({})
creator._secrets_paths.add(outside.resolve())
assert creator._build_filtered_secrets({"k"}) == {}
def test_validate_tar_members_rejects_escape_through_symlinked_dir(
tmp_path: Path,
) -> None:
"""A member resolving outside the target through an existing symlink is refused."""
target = tmp_path / "out"
target.mkdir()
outside = tmp_path / "outside"
outside.mkdir()
try:
(target / "link").symlink_to(outside, target_is_directory=True)
except (OSError, NotImplementedError):
pytest.skip("symlinks not available")
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tar:
_add_bytes_to_tar(tar, "link/file.txt", b"data")
buf.seek(0)
with (
tarfile.open(fileobj=buf, mode="r") as tar,
pytest.raises(EsphomeError, match="outside target"),
):
_validate_tar_members(tar, target.resolve())
+37
View File
@@ -6,6 +6,8 @@ from unittest.mock import patch
import pytest
from esphome.config_helpers import (
Extend,
Remove,
filter_source_files_from_defines,
filter_source_files_from_platform,
frameworks_for_platforms,
@@ -172,3 +174,38 @@ def test_filter_source_files_from_defines() -> None:
mock_core.defines = set()
assert sorted(filter_func()) == ["automation.cpp", "filter.cpp"]
def test_extend_str_repr_and_equality() -> None:
ext = Extend("my_id")
assert str(ext) == "!extend my_id"
assert repr(ext) == "Extend(my_id)"
assert ext == Extend("my_id")
assert ext != Extend("other")
assert ext != "my_id"
def test_remove_str_repr_and_equality() -> None:
rem = Remove("my_id")
assert str(rem) == "!remove my_id"
assert repr(rem) == "Remove(my_id)"
assert rem == Remove("my_id")
assert rem != Remove("other")
assert rem != "my_id"
def test_filter_source_files_from_platform_unknown_pair() -> None:
"""A target platform/framework pair with no PlatformFramework yields no exclusions."""
mock_core_data = {
KEY_CORE: {
KEY_TARGET_PLATFORM: "not_a_platform",
KEY_TARGET_FRAMEWORK: "arduino",
}
}
with patch("esphome.config_helpers.CORE.data", mock_core_data):
func = filter_source_files_from_platform(
{"a.cpp": {PlatformFramework.ESP32_ARDUINO}}
)
assert func() == []
+83 -1
View File
@@ -1,10 +1,18 @@
"""Tests for the coroutine module."""
from collections.abc import AsyncGenerator, Generator
import contextvars
from typing import Any
import pytest
from esphome.coroutine import CoroPriority, FakeEventLoop, coroutine_with_priority
from esphome.coroutine import (
CoroPriority,
FakeEventLoop,
_Task,
coroutine,
coroutine_with_priority,
)
def test_coro_priority_enum_values() -> None:
@@ -262,3 +270,77 @@ def test_context_inherits_ambient_value_at_schedule_time() -> None:
my_var.reset(token)
assert seen == {"value": "ambient"}
def test_coroutine_does_not_rewrap_esphome_coroutine() -> None:
@coroutine
def wrapped() -> Generator[None, None, None]:
yield
assert coroutine(wrapped) is wrapped
def test_coroutine_rejects_async_generator() -> None:
async def agen() -> AsyncGenerator[int, None]:
yield 1
with pytest.raises(ValueError, match="Async generator functions are not allowed"):
coroutine(agen)
def test_coroutine_flattens_awaitable_and_generator_yields() -> None:
results: list[Any] = []
async def native() -> int:
return 3
def inner() -> Generator[None, None, int]:
yield
return 4
@coroutine
def outer() -> Generator[Any, Any, None]:
results.append((yield native()))
results.append((yield inner()))
loop = FakeEventLoop()
loop.add_job(outer)
loop.flush_tasks()
assert results == [3, 4]
def test_task_equality_and_ordering() -> None:
ctx = contextvars.copy_context()
first = _Task(0.0, 0, iter(()), None, ctx)
same = first.with_priority(0.0)
higher = _Task(1.0, 1, iter(()), None, ctx)
assert first == same
assert (first != same) is False
assert first != higher
assert higher < first
def test_add_job_rejects_coroutine_object() -> None:
async def native() -> None:
"""Never awaited; only used to build a coroutine object."""
coro_obj = native()
try:
with pytest.raises(ValueError, match="not coroutine objects"):
FakeEventLoop().add_job(coro_obj)
finally:
coro_obj.close()
def test_flush_tasks_detects_deadlock() -> None:
def forever() -> Generator[None, None, None]:
while True:
yield
loop = FakeEventLoop()
loop.add_job(forever)
with pytest.raises(RuntimeError, match="Circular dependency"):
loop.flush_tasks()
+41
View File
@@ -1,5 +1,6 @@
from collections.abc import Awaitable, Callable
import importlib.util
import inspect
import logging
from pathlib import Path
import sys
@@ -362,3 +363,43 @@ def test_add_optional_skips_none(monkeypatch: pytest.MonkeyPatch) -> None:
assert ch.add_optional(setter, None) is None
setter.assert_not_called()
add_mock.assert_not_called()
def _register_component_mocks(monkeypatch: pytest.MonkeyPatch) -> tuple[Mock, Mock]:
base = Mock()
base.__str__ = lambda self: "foo.bar"
base.type = Mock()
base.type.__str__ = lambda self: "foo::Bar"
var = Mock(base=base)
app_mock = Mock(register_component_=Mock(return_value=var))
monkeypatch.setattr(ch, "App", app_mock)
monkeypatch.setattr(ch, "CORE", Mock(component_ids=["foo.bar"], data={}))
monkeypatch.setattr(ch, "add", Mock())
return var, app_mock
@pytest.mark.asyncio
async def test_register_component_stack_error_warns_and_skips_source(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
var, app_mock = _register_component_mocks(monkeypatch)
monkeypatch.setattr(inspect, "stack", Mock(side_effect=IndexError("no stack")))
with caplog.at_level(logging.WARNING):
assert await ch.register_component(var, {}) is var
assert "Error while finding name of component" in caplog.text
app_mock.register_component_.assert_called_once_with(var)
@pytest.mark.asyncio
async def test_register_component_without_module_has_no_source(
monkeypatch: pytest.MonkeyPatch,
) -> None:
var, app_mock = _register_component_mocks(monkeypatch)
monkeypatch.setattr(inspect, "stack", Mock(return_value=[(None,), (None,)]))
monkeypatch.setattr(inspect, "getmodule", lambda _: None)
assert await ch.register_component(var, {}) is var
app_mock.register_component_.assert_called_once_with(var)
+50
View File
@@ -367,6 +367,56 @@ def test_generate_cmakelists_txt_references_project_managed_components_variable(
assert "${ESPHOME_PROJECT_MANAGED_COMPONENTS}" in content
def test_generate_cmakelists_txt_uses_declared_src_dir(
tmp_component: IDFComponent,
) -> None:
"""A library.json srcDir is used as is instead of probing src/Src/."""
lib_dir = tmp_component.path / "lib"
lib_dir.mkdir()
(lib_dir / "main.c").write_text("int main() {}")
tmp_component.data = {"build": {"srcDir": "lib"}}
content = generate_cmakelists_txt(tmp_component)
assert "SRCS" in content
assert "main.c" in content
assert "INCLUDE_DIRS" in content
def test_generate_cmakelists_txt_without_include_dirs(
tmp_component: IDFComponent,
) -> None:
"""No INCLUDE_DIRS line when none of the include directories exist."""
tmp_component.data = {"build": {"srcDir": "missing"}}
content = generate_cmakelists_txt(tmp_component)
assert "INCLUDE_DIRS" not in content
def test_generate_idf_component_yml_with_several_dependencies(
tmp_component: IDFComponent, tmp_path: Path
) -> None:
"""Every dependency lands under one shared dependencies section."""
deps = []
for name in ("dep_a", "dep_b"):
dep = IDFComponent(name, "1.0", source=URLSource("http://dummy.com"))
dep.path = tmp_path / name
deps.append(dep)
tmp_component.dependencies = deps
tmp_component.data = {}
result = generate_idf_component_yml(tmp_component)
assert result == (
"dependencies:\n"
" dep_a:\n"
f" override_path: {deps[0].path}\n"
" dep_b:\n"
f" override_path: {deps[1].path}\n"
)
def test_generate_idf_component_yml_basic(tmp_component):
tmp_component.data = {"description": "test", "repository": {"url": "http://aaa"}}
result = generate_idf_component_yml(tmp_component)
+25
View File
@@ -2055,3 +2055,28 @@ def test_perform_ota_tcp_send_network_error_unchanged(mock_socket: Mock) -> None
]
with pytest.raises(espota2.OTANetworkError, match="sending frame"):
espota2.perform_ota(mock_socket, None, io.BytesIO(b"x" * 100), "test.bin")
@pytest.mark.parametrize(
("dashboard", "expect_device_hint"), [(False, True), (True, False)]
)
def test_run_ota_impl_resolve_failure_hint(
mock_resolve_ip: Mock,
firmware_file: Path,
caplog: pytest.LogCaptureFixture,
monkeypatch: pytest.MonkeyPatch,
dashboard: bool,
expect_device_hint: bool,
) -> None:
"""The --device hint is only useful on the command line, not the dashboard."""
from esphome.core import CORE
monkeypatch.setattr(CORE, "dashboard", dashboard)
mock_resolve_ip.side_effect = EsphomeError("no such host")
with caplog.at_level(logging.ERROR), pytest.raises(espota2.OTAError):
espota2.run_ota_impl_("test.local", 3232, None, firmware_file)
assert "Is it connected to WiFi?" in caplog.text
assert ("try --device <IP>" in caplog.text) is expect_device_hint
assert "static IP address" in caplog.text
+28 -1
View File
@@ -2,7 +2,7 @@
import pytest
from esphome import final_validate as fv
from esphome import config_validation as cv, final_validate as fv
from esphome.config import Config
from esphome.const import CONF_ID
from esphome.core import ID
@@ -45,3 +45,30 @@ def test_non_dict_parent_raises_type_error() -> None:
config.declare_ids.append((list_id, ["listed", 0]))
with pytest.raises(TypeError):
fv.get_declaring_config(ID("in_list"), config)
@pytest.mark.parametrize("pre_built", [False, True])
def test_id_declaration_match_schema_validates_declaration(pre_built: bool) -> None:
config, hub_id, _ = _config()
schema = {cv.Required("mode"): cv.string}
if pre_built:
schema = cv.Schema(schema, extra=cv.ALLOW_EXTRA)
validator = fv.id_declaration_match_schema(schema)
token = fv.full_config.set(config)
try:
result = validator(ID("my_hub"))
finally:
fv.full_config.reset(token)
assert result["mode"] == "fast"
def test_id_declaration_match_schema_rejects_mismatch() -> None:
config, _, _ = _config()
validator = fv.id_declaration_match_schema({cv.Required("mode"): cv.one_of("slow")})
token = fv.full_config.set(config)
try:
with pytest.raises(cv.Invalid):
validator(ID("my_hub"))
finally:
fv.full_config.reset(token)
+69
View File
@@ -3233,3 +3233,72 @@ def test_resolve_symlink_stub_returns_none_when_read_bytes_raises(
result = git.resolve_symlink_stub(repo_dir, stub)
assert result is None
def test_git_file_git_url() -> None:
gf = git.GitFile("github.com", "esphome", "repo", "file.yaml")
assert gf.git_url == "https://github.com/esphome/repo.git"
@pytest.mark.parametrize(
("domain", "expected"),
[
(
"codeberg.org",
"https://codeberg.org/owner/repo/raw/commit/main/dir/file.yaml",
),
(
"github.com",
"https://raw.githubusercontent.com/owner/repo/main/dir/file.yaml",
),
("gitlab.com", "https://gitlab.com/owner/repo/-/raw/main/dir/file.yaml"),
],
)
def test_git_file_raw_url(domain: str, expected: str) -> None:
gf = git.GitFile(domain, "owner", "repo", "dir/file.yaml", ref="main")
assert gf.raw_url == expected
def test_git_file_raw_url_requires_ref() -> None:
gf = git.GitFile("github.com", "owner", "repo", "file.yaml")
with pytest.raises(ValueError, match="URL has no ref"):
_ = gf.raw_url
def test_git_file_raw_url_unsupported_domain() -> None:
gf = git.GitFile("example.com", "owner", "repo", "file.yaml", ref="main")
with pytest.raises(NotImplementedError, match="example.com not supported"):
_ = gf.raw_url
def test_git_file_from_shorthand_full() -> None:
gf = git.GitFile.from_shorthand("gitlab://owner/repo/sub/file.yaml@v1.0?query")
assert gf == git.GitFile(
"gitlab.com", "owner", "repo", "sub/file.yaml", ref="v1.0", query="query"
)
def test_git_file_from_shorthand_without_ref_or_query() -> None:
gf = git.GitFile.from_shorthand("codeberg://owner/repo/file.yaml")
assert gf == git.GitFile("codeberg.org", "owner", "repo", "file.yaml")
def test_git_file_from_shorthand_requires_string() -> None:
with pytest.raises(ValueError, match="must be a string"):
git.GitFile.from_shorthand(123)
def test_git_file_from_shorthand_bad_format() -> None:
with pytest.raises(ValueError, match="not in expected"):
git.GitFile.from_shorthand("not a url")
def test_git_file_from_shorthand_unknown_domain() -> None:
with pytest.raises(ValueError, match="Unknown git domain bitbucket"):
git.GitFile.from_shorthand("bitbucket://owner/repo/file.yaml")
+85 -1
View File
@@ -11,7 +11,7 @@ import time
import pytest
from esphome.core import CORE
from esphome.log import AnsiFore, AnsiStyle, color, setup_log
from esphome.log import AnsiFore, AnsiStyle, ESPHomeLogFormatter, color, setup_log
class _FakeTty(io.StringIO):
@@ -339,3 +339,87 @@ def test_setup_log_win32_always_imports_colorama(
monkeypatch.setattr(sys, "stderr", _FakeTty())
setup_log()
assert "colorama" in sys.modules
def _record(level: int, message: str) -> logging.LogRecord:
return logging.LogRecord("esphome", level, __file__, 1, message, None, None)
@pytest.mark.parametrize(
("level", "colour"),
[
(logging.DEBUG, AnsiFore.CYAN),
(logging.INFO, AnsiFore.GREEN),
(logging.WARNING, AnsiFore.YELLOW),
(logging.ERROR, AnsiFore.RED),
(logging.CRITICAL, AnsiFore.RED),
],
)
def test_formatter_colours_by_level(
monkeypatch: pytest.MonkeyPatch, level: int, colour: AnsiFore
) -> None:
monkeypatch.setattr(CORE, "dashboard", False)
formatter = ESPHomeLogFormatter(include_timestamp=False)
result = formatter.format(_record(level, "hello"))
name = logging.getLevelName(level)
assert result == f"{colour.value}{name} hello{AnsiStyle.RESET_ALL.value}"
def test_formatter_unknown_level_has_no_colour_prefix(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(CORE, "dashboard", False)
formatter = ESPHomeLogFormatter(include_timestamp=False)
result = formatter.format(_record(25, "hello"))
assert result == f"Level 25 hello{AnsiStyle.RESET_ALL.value}"
def test_formatter_includes_timestamp_when_requested(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(CORE, "dashboard", False)
formatter = ESPHomeLogFormatter(include_timestamp=True)
record = _record(logging.INFO, "hello")
result = formatter.format(record)
assert formatter.formatTime(record) in result
def test_formatter_dashboard_escapes_ansi(monkeypatch: pytest.MonkeyPatch) -> None:
"""The dashboard prints the escape character as literal text."""
monkeypatch.setattr(CORE, "dashboard", True)
formatter = ESPHomeLogFormatter(include_timestamp=False)
result = formatter.format(_record(logging.INFO, "hello"))
assert "\033" not in result
assert result.startswith("\\033[32mINFO hello")
def test_setup_log_debug_sets_verbose(
monkeypatch: pytest.MonkeyPatch, colorama_probe: None
) -> None:
monkeypatch.setattr(CORE, "dashboard", True)
logging.getLogger().handlers.clear()
setup_log(logging.DEBUG)
assert CORE.verbose is True
assert CORE.quiet is False
def test_setup_log_critical_sets_quiet(
monkeypatch: pytest.MonkeyPatch, colorama_probe: None
) -> None:
monkeypatch.setattr(CORE, "dashboard", True)
logging.getLogger().handlers.clear()
setup_log(logging.CRITICAL)
assert CORE.quiet is True
assert CORE.verbose is False
+193 -2
View File
@@ -1,13 +1,26 @@
"""Unit tests for esphome/pins.py."""
from collections.abc import Iterator
import logging
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from esphome import config_validation as cv, pins
import esphome.codegen as cg
from esphome.const import CONF_ADDRESS
from esphome.core import ID
from esphome.config import Config, path_context
from esphome.const import (
CONF_ADDRESS,
CONF_ALLOW_OTHER_USES,
CONF_ID,
CONF_IGNORE_STRAPPING_WARNING,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
KEY_CORE,
KEY_TARGET_PLATFORM,
)
from esphome.core import CORE, ID
from esphome.schema_extractors import SCHEMA_EXTRACT
SomeHub = cg.esphome_ns.class_("SomeHub")
@@ -166,3 +179,181 @@ async def test_new_parented_pin(args: tuple[str, ...]) -> None:
var.set_inverted.assert_called_once_with(True)
var.set_flags.assert_called_once()
assert add.call_count == 3
@pytest.fixture
def registry() -> pins.PinRegistry:
reg = pins.PinRegistry()
reg.register("hub", lambda conf: conf)(lambda conf: conf)
return reg
@pytest.fixture
def pin_path() -> Iterator[None]:
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
token = path_context.set(["pin"])
try:
yield
finally:
path_context.reset(token)
@pytest.mark.usefixtures("pin_path")
def test_registry_validate_tracks_usage_and_get_count(
registry: pins.PinRegistry,
) -> None:
assert registry.get_count("hub", "h1", 4) == 0
registry.validate({"hub": "h1", CONF_NUMBER: 4}, "hub")
assert registry.get_count("hub", "h1", 4) == 1
assert registry.get_count("hub", "h1", 5) == 0
@pytest.mark.usefixtures("pin_path")
def test_registry_validate_without_number_is_not_tracked(
registry: pins.PinRegistry,
) -> None:
assert registry.validate({"hub": "h1"}, "hub") == {"hub": "h1"}
assert not registry.pins_used
def test_registry_get_to_code(registry: pins.PinRegistry) -> None:
to_code = registry["hub"][0]
assert registry.get_to_code("hub") is to_code
assert registry.get_to_code({"hub": "h1"}) is to_code
def _final_config(client_id: ID) -> Config:
config = Config()
config["hub"] = [{CONF_ID: client_id}]
config.declare_ids.append((client_id, ["hub", 0, CONF_ID]))
return config
@pytest.mark.parametrize(
("uses", "allow_others", "testing_mode", "error"),
[
(2, False, False, "used in multiple places"),
(2, True, False, None),
(2, False, True, None),
(1, False, False, None),
(1, True, False, "incorrectly sets allow_other_uses"),
(1, True, True, None),
],
)
@pytest.mark.usefixtures("pin_path")
def test_registry_final_validate_reuse_rules(
registry: pins.PinRegistry,
monkeypatch: pytest.MonkeyPatch,
uses: int,
allow_others: bool,
testing_mode: bool,
error: str | None,
) -> None:
client_id = ID("h1", is_declaration=True)
extra = {CONF_ALLOW_OTHER_USES: True} if allow_others else {}
for _ in range(uses):
registry.validate({"hub": client_id, CONF_NUMBER: 4, **extra}, "hub")
monkeypatch.setattr(CORE, "testing_mode", testing_mode)
config = _final_config(client_id)
registry.final_validate(config)
if error is None:
assert not config.errors
else:
assert all(error in str(err) for err in config.errors)
assert config.errors
@pytest.mark.usefixtures("pin_path")
def test_registry_final_validate_runs_hook_with_parent_config(
monkeypatch: pytest.MonkeyPatch,
) -> None:
hook = MagicMock()
registry = pins.PinRegistry()
registry.register("hub", lambda conf: conf, hook)(lambda conf: conf)
client_id = ID("h1", is_declaration=True)
pin = registry.validate({"hub": client_id, CONF_NUMBER: 4}, "hub")
config = _final_config(client_id)
registry.final_validate(config)
hook.assert_called_once_with(pin, config["hub"][0])
assert not config.errors
@pytest.mark.parametrize(
("mode", "expected"),
[
("input", {"input": True}),
("INPUT_PULLUP", {"input": True, "pullup": True}),
("output_open_drain", {"output": True, "open_drain": True}),
],
)
def test_set_mode_parses_string_modes(mode: str, expected: dict[str, bool]) -> None:
assert pins._set_mode({CONF_NUMBER: 1, CONF_MODE: mode}, {}) == {
CONF_NUMBER: 1,
CONF_MODE: expected,
}
def test_set_mode_rejects_unknown_string_mode() -> None:
with pytest.raises(cv.Invalid, match="Unknown pin mode bogus"):
pins._set_mode({CONF_MODE: "bogus"}, {})
def test_set_mode_keeps_dict_mode_and_applies_default() -> None:
explicit = {CONF_MODE: {"input": True}}
assert pins._set_mode(explicit, {"output": True}) is explicit
assert pins._set_mode({}, {"output": True}) == {CONF_MODE: {"output": True}}
def test_gpio_flags_expr() -> None:
assert str(pins.gpio_flags_expr({"input": False})) == str(cg.gpio_Flags.FLAG_NONE)
flags = str(pins.gpio_flags_expr({"input": True, "pullup": True}))
assert "FLAG_INPUT" in flags
assert "FLAG_PULLUP" in flags
def test_gpio_validate_modes() -> None:
mode = {"input": True, "output": False}
assert pins.gpio_validate_modes(mode) is mode
with pytest.raises(cv.Invalid, match="Mode must be input or output"):
pins.gpio_validate_modes({"input": False, "output": False})
INPUT_MODE = {"input": True}
def test_gpio_base_schema_rejects_placeholder_number() -> None:
schema = pins.gpio_base_schema(SomeHub, cv.int_)
with pytest.raises(cv.Invalid, match="placeholder 'GPIOX'"):
schema({CONF_NUMBER: "GPIOX", CONF_MODE: INPUT_MODE})
assert schema({CONF_NUMBER: 3, CONF_MODE: INPUT_MODE})[CONF_NUMBER] == 3
@pytest.mark.parametrize("invertible", [True, False])
def test_gpio_base_schema_invertible(invertible: bool) -> None:
schema = pins.gpio_base_schema(SomeHub, cv.int_, invertible=invertible)
assert (
CONF_INVERTED in schema({CONF_NUMBER: 3, CONF_MODE: INPUT_MODE})
) is invertible
def test_check_strapping_pin(caplog: pytest.LogCaptureFixture) -> None:
logger = logging.getLogger("test_pins")
with caplog.at_level(logging.WARNING, logger="test_pins"):
pins.check_strapping_pin({CONF_NUMBER: 2}, {2}, logger)
pins.check_strapping_pin(
{CONF_NUMBER: 2, CONF_IGNORE_STRAPPING_WARNING: True}, {2}, logger
)
assert caplog.text.count("GPIO2 is a strapping pin") == 1
with pytest.raises(cv.Invalid, match="not a strapping pin"):
pins.check_strapping_pin(
{CONF_NUMBER: 3, CONF_IGNORE_STRAPPING_WARNING: True}, {2}, logger
)
+73 -1
View File
@@ -5,10 +5,12 @@ from __future__ import annotations
from collections.abc import Callable
import io
from pathlib import Path
import runpy
import sys
from types import ModuleType
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock
import platformio
from platformio.registry.client import RegistryClient
import pytest
@@ -146,3 +148,73 @@ def test_patch_registry_private_packages_skips_account_probe(
assert RegistryClient.allowed_private_packages() is False
assert RegistryClient().allowed_private_packages() is False
def test_patch_file_downloader_retries_when_session_cleanup_fails(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A failing close of the old session must not stop the retry."""
package_exception = type("PackageException", (Exception,), {})
attempts: list[int] = []
def flaky_init(self: object, *args: object, **kwargs: object) -> None:
attempts.append(1)
if len(attempts) < 2:
raise package_exception("502")
downloader = type("FileDownloader", (), {"__init__": flaky_init})
monkeypatch.setitem(
sys.modules,
"platformio.package.download",
SimpleNamespace(FileDownloader=downloader),
)
monkeypatch.setitem(
sys.modules,
"platformio.package.exception",
SimpleNamespace(PackageException=package_exception),
)
monkeypatch.setattr(runner.time, "sleep", lambda _delay: None)
runner.patch_file_downloader()
instance = object.__new__(downloader)
instance._http_response = Mock(close=Mock(side_effect=OSError("boom")))
downloader.__init__(instance)
assert len(attempts) == 2
instance._http_response.close.assert_called_once()
def test_runner_runs_main_when_executed_as_a_script(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Running the file as ``__main__`` exits with PlatformIO's return code."""
monkeypatch.setattr(sys, "argv", ["runner", "run"])
monkeypatch.setattr(sys, "stdout", io.StringIO())
monkeypatch.setattr(sys, "stderr", io.StringIO())
# The script re-imports and applies the PlatformIO patches on its own
# module namespace, so give it stand-ins rather than the real internals.
monkeypatch.setitem(sys.modules, "platformio.run", Mock())
monkeypatch.setitem(
sys.modules,
"platformio.package.download",
SimpleNamespace(FileDownloader=type("FileDownloader", (), {})),
)
monkeypatch.setitem(
sys.modules,
"platformio.package.exception",
SimpleNamespace(PackageException=Exception),
)
monkeypatch.setitem(
sys.modules,
"platformio.registry.client",
SimpleNamespace(RegistryClient=type("RegistryClient", (), {})),
)
pio_main = SimpleNamespace(main=lambda: 3)
monkeypatch.setitem(sys.modules, "platformio.__main__", pio_main)
monkeypatch.setattr(platformio, "__main__", pio_main, raising=False)
with pytest.raises(SystemExit) as exc_info:
runpy.run_path(str(Path(runner.__file__)), run_name="__main__")
assert exc_info.value.code == 3
@@ -112,6 +112,22 @@ def test_idedata_cc_path(setup_core: Path) -> None:
)
@pytest.mark.parametrize(
("raw_data", "expected"),
[
pytest.param(
{"defines": ["USE_ESP32", "FOO=1"]}, ["USE_ESP32", "FOO=1"], id="set"
),
pytest.param({}, [], id="missing"),
],
)
def test_idedata_defines(raw_data: dict[str, list[str]], expected: list[str]) -> None:
"""IDEData.defines returns the defines, or an empty list when absent."""
idedata = toolchain.IDEData({"prog_path": "/path/to/firmware.elf", **raw_data})
assert idedata.defines == expected
def test_flash_image_dataclass() -> None:
"""Test FlashImage dataclass stores path and offset correctly."""
image = FlashImage(path=Path("/path/to/image.bin"), offset="0x10000")
@@ -758,6 +774,17 @@ def test_copy_ccache_script(setup_core: Path) -> None:
assert dest.read_text() == source.read_text()
def test_copy_pch_script(setup_core: Path) -> None:
"""The shared PCH pre-script is copied into the build dir."""
CORE.build_path = setup_core / "build" / "test"
toolchain.copy_pch_script()
dest = setup_core / "build" / "test" / "pch.py"
source = Path(toolchain.__file__).parent / "pch.py.script"
assert dest.read_text() == source.read_text()
class _FakeSConsEnv(dict):
"""Just enough of a SCons construction environment for ccache.py."""
@@ -1015,6 +1042,20 @@ def test_run_platformio_cli_run_builds_command(
)
def test_run_platformio_cli_run_without_verbose(
setup_core: Path, mock_run_platformio_cli: Mock
) -> None:
"""Without verbose, no -v flag is passed to PlatformIO."""
CORE.build_path = str(setup_core / "build" / "test")
mock_run_platformio_cli.return_value = 0
toolchain.run_platformio_cli_run({"name": "test"}, False, "extra")
mock_run_platformio_cli.assert_called_once_with(
"run", "-d", CORE.build_path, "extra"
)
def test_run_compile(setup_core: Path, mock_run_platformio_cli_run: Mock) -> None:
"""Test run_compile with process limit."""
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME
+18
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
import importlib
import logging
import re
import socket
from unittest.mock import ANY, patch
@@ -10,6 +12,7 @@ from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError
from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr
import pytest
from esphome import resolver
from esphome.async_thread import AsyncDispatchTimeout
from esphome.core import EsphomeError
from esphome.resolver import RESOLVE_TIMEOUT, AsyncResolver
@@ -162,3 +165,18 @@ def test_async_resolver_mixed_addresses(
mock_resolve.assert_called_once_with(
["test.local", "192.168.1.100", "::1"], 6053, timeout=RESOLVE_TIMEOUT
)
def test_invalid_resolve_timeout_env_falls_back_to_default(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
"""A non-numeric ESPHOME_RESOLVE_TIMEOUT warns and uses the default."""
monkeypatch.setenv("ESPHOME_RESOLVE_TIMEOUT", "soon")
try:
with caplog.at_level(logging.WARNING, logger="esphome.resolver"):
importlib.reload(resolver)
assert resolver.RESOLVE_TIMEOUT == 20.0
assert "ESPHOME_RESOLVE_TIMEOUT='soon' is not a valid number" in caplog.text
finally:
monkeypatch.delenv("ESPHOME_RESOLVE_TIMEOUT")
importlib.reload(resolver)
@@ -0,0 +1,90 @@
"""Tests for esphome.schema_extractors."""
from collections.abc import Callable, Iterator
from typing import Any
import pytest
from esphome import schema_extractors as se
@pytest.fixture
def extraction_enabled() -> Iterator[None]:
"""Enable schema extraction and restore the module state afterwards."""
stores = (
se.extended_schemas,
se.list_schemas,
se.registry_schemas,
se.hidden_schemas,
se.typed_schemas,
)
saved = [dict(store) for store in stores]
previous = se.EnableSchemaExtraction
se.EnableSchemaExtraction = True
try:
yield
finally:
se.EnableSchemaExtraction = previous
for store, old in zip(stores, saved, strict=True):
store.clear()
store.update(old)
def _validator(value: Any) -> Any:
return value
def test_decorators_are_passthrough_when_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(se, "EnableSchemaExtraction", False)
assert se.schema_extractor("name")(_validator) is _validator
assert se.schema_extractor_registry(object())(_validator) is _validator
assert se.schema_extractor_extended(_validator) is _validator
assert se.schema_extractor_list(_validator) is _validator
assert se.schema_extractor_typed(_validator) is _validator
@pytest.mark.usefixtures("extraction_enabled")
def test_schema_extractor_records_hidden_schema() -> None:
assert se.schema_extractor("my_validator")(_validator) is _validator
assert se.hidden_schemas[repr(_validator)] == "my_validator"
@pytest.mark.usefixtures("extraction_enabled")
def test_schema_extractor_registry_records_registry() -> None:
registry = object()
assert se.schema_extractor_registry(registry)(_validator) is _validator
assert se.registry_schemas[repr(_validator)] is registry
@pytest.mark.parametrize(
("decorator", "store_name"),
[
(se.schema_extractor_extended, "extended_schemas"),
(se.schema_extractor_list, "list_schemas"),
],
)
@pytest.mark.usefixtures("extraction_enabled")
def test_args_recording_decorators(
decorator: Callable[[Callable[..., Any]], Callable[..., Any]], store_name: str
) -> None:
def build(*args: Any) -> list[Any]:
return list(args)
wrapped = decorator(build)
result = wrapped(1, 2)
assert result == [1, 2]
assert getattr(se, store_name)[repr(result)] == (1, 2)
@pytest.mark.usefixtures("extraction_enabled")
def test_schema_extractor_typed_records_args_and_kwargs() -> None:
def build(*args: Any, **kwargs: Any) -> list[Any]:
return [args, kwargs]
result = se.schema_extractor_typed(build)(1, key="x")
assert result == [(1,), {"key": "x"}]
assert se.typed_schemas[repr(result)] == ((1,), {"key": "x"})

Some files were not shown because too many files have changed in this diff Show More