mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0473e7419 | ||
|
|
3325de5e28 | ||
|
|
cc89a011e2 | ||
|
|
afffe3ba36 | ||
|
|
6bf3e07a74 | ||
|
|
c6c1fce2bf | ||
|
|
c76e9e1d62 | ||
|
|
4293d12008 | ||
|
|
7a3aac407f | ||
|
|
51819d40cb | ||
|
|
b0a1054e8d | ||
|
|
e7ad8f9c80 | ||
|
|
a8a8acb918 | ||
|
|
3ae5e1dfad | ||
|
|
ef79d48b6f | ||
|
|
94804cc741 | ||
|
|
879c5e5e14 |
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -168,6 +168,8 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
case BALLU_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -218,6 +218,8 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) {
|
||||
case 10:
|
||||
buf[i] = j;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -81,6 +81,8 @@ void Mcp4461Component::initialize_terminal_disabled(Mcp4461WiperIdx wiper, char
|
||||
case 'w':
|
||||
this->reg_[wiper_idx].terminal_w = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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() == []
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user