Replace custom esphome::optional with std::optional

The custom optional implementation (from optional-bare, 2017) predates
C++17. All ESPHome platforms now compile with gnu++20, making
std::optional available everywhere.

The custom implementation had several issues:
- No emplace() support
- Always default-constructs value_ (wasteful for non-trivial types)
- reset() only flips a bool without destroying the value
- No move semantics
- Requires T to be default constructible

Replace with using aliases (using std::optional, using std::nullopt,
etc.) so all existing code using esphome::optional continues to work.

Also fix ~30 unsafe .value() calls across climate IR components that
relied on the custom optional's behavior of returning a
default-constructed value when empty. With std::optional, accessing
an empty optional is UB. These are replaced with value_or() using
appropriate defaults (CLIMATE_FAN_AUTO, CLIMATE_PRESET_NONE).
This commit is contained in:
J. Nick Koston
2026-02-27 16:09:19 -10:00
parent e7d4f2608b
commit 3bca31ba9f
24 changed files with 44 additions and 249 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ void BalluClimate::transmit_state() {
remote_state[11] = 0x1e;
// Fan speed
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_HIGH:
remote_state[4] |= BALLU_FAN_HIGH;
break;
@@ -79,7 +79,7 @@ void LgIrClimate::transmit_state() {
if (this->mode == climate::CLIMATE_MODE_OFF) {
remote_state |= FAN_AUTO;
} else {
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_HIGH:
remote_state |= FAN_MAX;
break;
+1 -1
View File
@@ -83,7 +83,7 @@ void CoolixClimate::transmit_state() {
this->fan_mode = climate::CLIMATE_FAN_AUTO;
remote_state |= COOLIX_FAN_MODE_AUTO_DRY;
} else {
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_HIGH:
remote_state |= COOLIX_FAN_MAX;
break;
+1 -1
View File
@@ -94,7 +94,7 @@ uint8_t DaikinClimate::operation_mode_() const {
uint16_t DaikinClimate::fan_speed_() const {
uint16_t fan_speed;
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_QUIET:
fan_speed = DAIKIN_FAN_SILENT << 8;
break;
+1 -1
View File
@@ -176,7 +176,7 @@ uint8_t DaikinArcClimate::operation_mode_() {
uint16_t DaikinArcClimate::fan_speed_() {
uint16_t fan_speed;
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
fan_speed = DAIKIN_FAN_1 << 8;
break;
+1 -1
View File
@@ -111,7 +111,7 @@ uint8_t DaikinBrcClimate::operation_mode_() {
uint8_t DaikinBrcClimate::fan_speed_swing_() {
uint16_t fan_speed;
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
fan_speed = DAIKIN_BRC_FAN_1;
break;
+1 -1
View File
@@ -64,7 +64,7 @@ uint8_t DelonghiClimate::operation_mode_() {
uint16_t DelonghiClimate::fan_speed_() {
uint16_t fan_speed;
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
fan_speed = DELONGHI_FAN_LOW;
break;
+1 -1
View File
@@ -28,7 +28,7 @@ uint8_t EmmetiClimate::set_mode_() {
}
uint8_t EmmetiClimate::set_fan_speed_() {
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
return EMMETI_FAN_1;
case climate::CLIMATE_FAN_MEDIUM:
@@ -141,7 +141,7 @@ void FujitsuGeneralClimate::transmit_state() {
}
// Set fan
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_HIGH:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH);
break;
+3 -3
View File
@@ -180,7 +180,7 @@ uint8_t GreeClimate::operation_mode_() {
uint8_t GreeClimate::fan_speed_() {
// YX1FF has 4 fan speeds -- we treat low as quiet and turbo as high
if (this->model_ == GREE_YX1FF) {
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_QUIET:
return GREE_FAN_1;
case climate::CLIMATE_FAN_LOW:
@@ -195,7 +195,7 @@ uint8_t GreeClimate::fan_speed_() {
}
}
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
return GREE_FAN_1;
case climate::CLIMATE_FAN_MEDIUM:
@@ -235,7 +235,7 @@ uint8_t GreeClimate::temperature_() {
uint8_t GreeClimate::preset_() {
// YX1FF has sleep preset
if (this->model_ == GREE_YX1FF) {
switch (this->preset.value()) {
switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) {
case climate::CLIMATE_PRESET_NONE:
return GREE_PRESET_NONE;
case climate::CLIMATE_PRESET_SLEEP:
+4 -2
View File
@@ -893,7 +893,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
} else {
this->preset = CLIMATE_PRESET_NONE;
}
should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
should_publish = should_publish || (!old_preset.has_value()) ||
(old_preset.value_or(CLIMATE_PRESET_NONE) != this->preset.value_or(CLIMATE_PRESET_NONE));
}
{
// Target temperature
@@ -936,7 +937,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
this->fan_mode = CLIMATE_FAN_HIGH;
break;
}
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
should_publish = should_publish || (!old_fan_mode.has_value()) ||
(old_fan_mode.value_or(CLIMATE_FAN_AUTO) != fan_mode.value_or(CLIMATE_FAN_AUTO));
}
// Display status
// should be before "Climate mode" because it is changing this->mode
@@ -402,7 +402,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
} else {
this->preset = CLIMATE_PRESET_NONE;
}
should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
should_publish = should_publish || (!old_preset.has_value()) ||
(old_preset.value_or(CLIMATE_PRESET_NONE) != this->preset.value_or(CLIMATE_PRESET_NONE));
}
{
// Target temperature
@@ -446,7 +447,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
this->fan_mode = CLIMATE_FAN_HIGH;
break;
}
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
should_publish = should_publish || (!old_fan_mode.has_value()) ||
(old_fan_mode.value_or(CLIMATE_FAN_AUTO) != fan_mode.value_or(CLIMATE_FAN_AUTO));
}
// Display status
// should be before "Climate mode" because it is changing this->mode
@@ -175,7 +175,7 @@ void HitachiClimate::transmit_state() {
set_temp_(static_cast<uint8_t>(this->target_temperature));
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
set_fan_(HITACHI_AC344_FAN_LOW);
break;
@@ -176,7 +176,7 @@ void HitachiClimate::transmit_state() {
set_temp_(static_cast<uint8_t>(this->target_temperature));
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
set_fan_(HITACHI_AC424_FAN_LOW);
break;
+4 -3
View File
@@ -180,7 +180,7 @@ void MitsubishiClimate::transmit_state() {
// For 5Level: Low = 1, Middle = 2, Medium = 3, High = 4
// For 4Level + Quiet: Low = 1, Middle = 2, Medium = 3, High = 4, Quiet = 5
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
remote_state[9] = 1;
break;
@@ -209,7 +209,8 @@ void MitsubishiClimate::transmit_state() {
break;
}
ESP_LOGD(TAG, "fan: %02x state: %02x", this->fan_mode.value(), remote_state[9]);
ESP_LOGD(TAG, "fan: %02x state: %02x", static_cast<uint8_t>(this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)),
remote_state[9]);
// Vertical Vane
switch (this->swing_mode) {
@@ -227,7 +228,7 @@ void MitsubishiClimate::transmit_state() {
ESP_LOGD(TAG, "default_vertical_direction_: %02X", this->default_vertical_direction_);
// Special modes
switch (this->preset.value()) {
switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) {
case climate::CLIMATE_PRESET_ECO:
remote_state[6] = MITSUBISHI_MODE_COOL | MITSUBISHI_OTHERWISE;
remote_state[8] = (remote_state[8] & ~7) | MITSUBISHI_MODE_A_COOL;
+1 -1
View File
@@ -71,7 +71,7 @@ void NoblexClimate::transmit_state() {
break;
}
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_LOW << 2);
break;
+1 -1
View File
@@ -89,7 +89,7 @@ void Tcl112Climate::transmit_state() {
// Set fan
uint8_t selected_fan;
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_HIGH:
selected_fan = TCL112_FAN_HIGH;
break;
@@ -84,7 +84,7 @@ void ThermostatClimate::refresh() {
this->switch_to_mode_(this->mode, false);
this->switch_to_action_(this->compute_action_(), false);
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
this->switch_to_fan_mode_(this->fan_mode.value(), false);
this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), false);
this->switch_to_swing_mode_(this->swing_mode, false);
this->switch_to_humidity_control_action_(this->compute_humidity_control_action_());
this->check_humidity_change_trigger_();
+4 -4
View File
@@ -502,7 +502,7 @@ void ToshibaClimate::transmit_generic_() {
}
uint8_t fan;
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_QUIET:
fan = TOSHIBA_FAN_SPEED_QUIET;
break;
@@ -567,7 +567,7 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
message[2] = RAC_PT1411HWRU_NO_FAN.code1;
message[7] = RAC_PT1411HWRU_NO_FAN.code2;
} else {
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
message[2] = RAC_PT1411HWRU_FAN_LOW.code1;
message[7] = RAC_PT1411HWRU_FAN_LOW.code2;
@@ -811,12 +811,12 @@ void ToshibaClimate::transmit_ras_2819t_() {
uint8_t temp_code = get_ras_2819t_temp_code(temperature);
// Get fan speed encoding for rc_code_1
climate::ClimateFanMode effective_fan_mode = this->fan_mode.value();
climate::ClimateFanMode effective_fan_mode = this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO);
// Dry mode only supports AUTO fan speed
if (this->mode == climate::CLIMATE_MODE_DRY) {
effective_fan_mode = climate::CLIMATE_FAN_AUTO;
if (this->fan_mode.value() != climate::CLIMATE_FAN_AUTO) {
if (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO) != climate::CLIMATE_FAN_AUTO) {
ESP_LOGW(TAG, "Dry mode only supports AUTO fan speed, forcing AUTO");
}
}
+1 -1
View File
@@ -82,7 +82,7 @@ void WhirlpoolClimate::transmit_state() {
remote_state[3] |= (uint8_t) (temp - this->temperature_min_()) << 4;
// Fan speed
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_HIGH:
remote_state[2] |= WHIRLPOOL_FAN_HIGH;
break;
+1 -1
View File
@@ -69,7 +69,7 @@ void Whynter::transmit_state() {
}
mode_before_ = this->mode;
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
remote_state |= FAN_LOW;
break;
+3 -3
View File
@@ -13,7 +13,7 @@ void ZHLT01Climate::transmit_state() {
ir_message[1] = 0x00; // Timer off
// Byte 3 : Turbo mode
if (this->preset.value() == climate::CLIMATE_PRESET_BOOST) {
if (this->preset.value_or(climate::CLIMATE_PRESET_NONE) == climate::CLIMATE_PRESET_BOOST) {
ir_message[3] = AC1_FAN_TURBO;
}
@@ -47,7 +47,7 @@ void ZHLT01Climate::transmit_state() {
}
// -- Fan
switch (this->preset.value()) {
switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) {
case climate::CLIMATE_PRESET_BOOST:
ir_message[7] |= AC1_FAN3;
break;
@@ -55,7 +55,7 @@ void ZHLT01Climate::transmit_state() {
ir_message[7] |= AC1_FAN_SILENT;
break;
default:
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
ir_message[7] |= AC1_FAN1;
break;
+5 -213
View File
@@ -1,220 +1,12 @@
#pragma once
//
// Copyright (c) 2017 Martin Moene
//
// https://github.com/martinmoene/optional-bare
//
// This code is licensed under the MIT License (MIT).
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// Modified by Otto Winter on 18.05.18
#include <algorithm>
#include <optional>
namespace esphome {
// type for nullopt
struct nullopt_t { // NOLINT
struct init {}; // NOLINT
nullopt_t(init /*unused*/) {}
};
// extra parenthesis to prevent the most vexing parse:
const nullopt_t nullopt((nullopt_t::init())); // NOLINT
// Simplistic optional: requires T to be default constructible, copyable.
template<typename T> class optional { // NOLINT
private:
using safe_bool = void (optional::*)() const;
public:
using value_type = T;
optional() {}
optional(nullopt_t /*unused*/) {}
optional(T const &arg) : has_value_(true), value_(arg) {} // NOLINT
template<class U> optional(optional<U> const &other) : has_value_(other.has_value()), value_(other.value()) {}
optional &operator=(nullopt_t /*unused*/) {
reset();
return *this;
}
bool operator==(optional<T> const &rhs) const {
if (has_value() && rhs.has_value())
return value() == rhs.value();
return !has_value() && !rhs.has_value();
}
template<class U> optional &operator=(optional<U> const &other) {
has_value_ = other.has_value();
value_ = other.value();
return *this;
}
void swap(optional &rhs) noexcept {
using std::swap;
if (has_value() && rhs.has_value()) {
swap(**this, *rhs);
} else if (!has_value() && rhs.has_value()) {
initialize(*rhs);
rhs.reset();
} else if (has_value() && !rhs.has_value()) {
rhs.initialize(**this);
reset();
}
}
// observers
value_type const *operator->() const { return &value_; }
value_type *operator->() { return &value_; }
value_type const &operator*() const { return value_; }
value_type &operator*() { return value_; }
operator safe_bool() const { return has_value() ? &optional::this_type_does_not_support_comparisons : nullptr; }
bool has_value() const { return has_value_; }
value_type const &value() const { return value_; }
value_type &value() { return value_; }
template<class U> value_type value_or(U const &v) const { return has_value() ? value() : static_cast<value_type>(v); }
// modifiers
void reset() { has_value_ = false; }
private:
void this_type_does_not_support_comparisons() const {} // NOLINT
template<typename V> void initialize(V const &value) { // NOLINT
value_ = value;
has_value_ = true;
}
bool has_value_{false}; // NOLINT
value_type value_; // NOLINT
};
// Relational operators
template<typename T, typename U> inline bool operator==(optional<T> const &x, optional<U> const &y) {
return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y;
}
template<typename T, typename U> inline bool operator!=(optional<T> const &x, optional<U> const &y) {
return !(x == y);
}
template<typename T, typename U> inline bool operator<(optional<T> const &x, optional<U> const &y) {
return (!y) ? false : (!x) ? true : *x < *y;
}
template<typename T, typename U> inline bool operator>(optional<T> const &x, optional<U> const &y) { return (y < x); }
template<typename T, typename U> inline bool operator<=(optional<T> const &x, optional<U> const &y) { return !(y < x); }
template<typename T, typename U> inline bool operator>=(optional<T> const &x, optional<U> const &y) { return !(x < y); }
// Comparison with nullopt
template<typename T> inline bool operator==(optional<T> const &x, nullopt_t /*unused*/) { return (!x); }
template<typename T> inline bool operator==(nullopt_t /*unused*/, optional<T> const &x) { return (!x); }
template<typename T> inline bool operator!=(optional<T> const &x, nullopt_t /*unused*/) { return bool(x); }
template<typename T> inline bool operator!=(nullopt_t /*unused*/, optional<T> const &x) { return bool(x); }
template<typename T> inline bool operator<(optional<T> const & /*unused*/, nullopt_t /*unused*/) { return false; }
template<typename T> inline bool operator<(nullopt_t /*unused*/, optional<T> const &x) { return bool(x); }
template<typename T> inline bool operator<=(optional<T> const &x, nullopt_t /*unused*/) { return (!x); }
template<typename T> inline bool operator<=(nullopt_t /*unused*/, optional<T> const & /*unused*/) { return true; }
template<typename T> inline bool operator>(optional<T> const &x, nullopt_t /*unused*/) { return bool(x); }
template<typename T> inline bool operator>(nullopt_t /*unused*/, optional<T> const & /*unused*/) { return false; }
template<typename T> inline bool operator>=(optional<T> const & /*unused*/, nullopt_t /*unused*/) { return true; }
template<typename T> inline bool operator>=(nullopt_t /*unused*/, optional<T> const &x) { return (!x); }
// Comparison with T
template<typename T, typename U> inline bool operator==(optional<T> const &x, U const &v) {
return bool(x) ? *x == v : false;
}
template<typename T, typename U> inline bool operator==(U const &v, optional<T> const &x) {
return bool(x) ? v == *x : false;
}
template<typename T, typename U> inline bool operator!=(optional<T> const &x, U const &v) {
return bool(x) ? *x != v : true;
}
template<typename T, typename U> inline bool operator!=(U const &v, optional<T> const &x) {
return bool(x) ? v != *x : true;
}
template<typename T, typename U> inline bool operator<(optional<T> const &x, U const &v) {
return bool(x) ? *x < v : true;
}
template<typename T, typename U> inline bool operator<(U const &v, optional<T> const &x) {
return bool(x) ? v < *x : false;
}
template<typename T, typename U> inline bool operator<=(optional<T> const &x, U const &v) {
return bool(x) ? *x <= v : true;
}
template<typename T, typename U> inline bool operator<=(U const &v, optional<T> const &x) {
return bool(x) ? v <= *x : false;
}
template<typename T, typename U> inline bool operator>(optional<T> const &x, U const &v) {
return bool(x) ? *x > v : false;
}
template<typename T, typename U> inline bool operator>(U const &v, optional<T> const &x) {
return bool(x) ? v > *x : true;
}
template<typename T, typename U> inline bool operator>=(optional<T> const &x, U const &v) {
return bool(x) ? *x >= v : false;
}
template<typename T, typename U> inline bool operator>=(U const &v, optional<T> const &x) {
return bool(x) ? v >= *x : true;
}
// Specialized algorithms
template<typename T> void swap(optional<T> &x, optional<T> &y) noexcept { x.swap(y); }
// Convenience function to create an optional.
template<typename T> inline optional<T> make_optional(T const &v) { return optional<T>(v); }
using std::make_optional;
using std::nullopt;
using std::nullopt_t;
using std::optional;
} // namespace esphome
+1 -3
View File
@@ -31,9 +31,7 @@ Component = esphome_ns.class_("Component")
ComponentPtr = Component.operator("ptr")
PollingComponent = esphome_ns.class_("PollingComponent", Component)
Application = esphome_ns.class_("Application")
# Create optional with explicit namespace to avoid ambiguity with std::optional
# The generated code will use esphome::optional instead of just optional
optional = global_ns.namespace("esphome").class_("optional")
optional = global_ns.namespace("std").class_("optional")
arduino_json_ns = global_ns.namespace("ArduinoJson")
JsonObject = arduino_json_ns.class_("JsonObject")
JsonObjectConst = arduino_json_ns.class_("JsonObjectConst")