ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
bedjet_climate.cpp
Go to the documentation of this file.
1#include "bedjet_climate.h"
2#include "esphome/core/log.h"
3
4#ifdef USE_ESP32
5
6namespace esphome::bedjet {
7
8using namespace esphome::climate;
9
10static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
11 if (fan_step < BEDJET_FAN_SPEED_COUNT)
12 return BEDJET_FAN_STEP_NAMES[fan_step];
13 return nullptr;
14}
15
16static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) {
17 for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
18 if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) {
19 return i;
20 }
21 }
22 return -1;
23}
24
25static inline BedjetButton heat_button(BedjetHeatMode mode) {
27}
28
29std::string BedJetClimate::describe() { return "BedJet Climate"; }
30
32 LOG_CLIMATE("", "BedJet Climate", this);
33 auto traits = this->get_traits();
34
35 ESP_LOGCONFIG(TAG, " Supported modes:");
36 for (auto mode : traits.get_supported_modes()) {
37 ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode)));
38 }
39 if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
40 ESP_LOGCONFIG(TAG, " - BedJet heating mode: EXT HT");
41 } else {
42 ESP_LOGCONFIG(TAG, " - BedJet heating mode: HEAT");
43 }
44
45 ESP_LOGCONFIG(TAG, " Supported fan modes:");
46 for (const auto &mode : traits.get_supported_fan_modes()) {
47 ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
48 }
49 for (const auto &mode : traits.get_supported_custom_fan_modes()) {
50 ESP_LOGCONFIG(TAG, " - %s (c)", mode);
51 }
52
53 ESP_LOGCONFIG(TAG, " Supported presets:");
54 for (auto preset : traits.get_supported_presets()) {
55 ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
56 }
57 for (const auto &preset : traits.get_supported_custom_presets()) {
58 ESP_LOGCONFIG(TAG, " - %s (c)", preset);
59 }
60}
61
63 // Set custom modes once during setup — stored on Climate base class, wired via get_traits()
64 this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
66 this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
67 "M1",
68 "M2",
69 "M3",
70 });
71
72 // restore set points
73 auto restore = this->restore_state_();
74 if (restore.has_value()) {
75 ESP_LOGI(TAG, "Restored previous saved state.");
76 restore->apply(this);
77 } else {
78 // Initial status is unknown until we connect
79 this->reset_state_();
80 }
81}
82
85 this->mode = CLIMATE_MODE_OFF;
87 this->target_temperature = NAN;
88 this->current_temperature = NAN;
89 this->preset.reset();
91 this->publish_state();
92}
93
95 // This component is controlled via the parent BedJetHub
96 // Empty loop not needed, disable to save CPU cycles
97 this->disable_loop();
98}
99
101 ESP_LOGD(TAG, "Received BedJetClimate::control");
102 if (!this->parent_->is_connected()) {
103 ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
104 return;
105 }
106
107 auto mode_opt = call.get_mode();
108 if (mode_opt.has_value()) {
109 ClimateMode mode = *mode_opt;
110 bool button_result;
111 switch (mode) {
112 case CLIMATE_MODE_OFF:
113 button_result = this->parent_->button_off();
114 break;
116 button_result = this->parent_->send_button(heat_button(this->heating_mode_));
117 break;
119 button_result = this->parent_->button_cool();
120 break;
121 case CLIMATE_MODE_DRY:
122 button_result = this->parent_->button_dry();
123 break;
124 default:
125 ESP_LOGW(TAG, "Unsupported mode: %d", mode);
126 return;
127 }
128
129 if (button_result) {
130 this->mode = mode;
131 // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
132 this->clear_custom_preset_();
133 this->preset.reset();
134 }
135 }
136
137 auto target_temp_opt = call.get_target_temperature();
138 if (target_temp_opt.has_value()) {
139 auto target_temp = *target_temp_opt;
140 auto result = this->parent_->set_target_temp(target_temp);
141
142 if (result) {
143 this->target_temperature = target_temp;
144 }
145 }
146
147 auto preset_opt = call.get_preset();
148 if (preset_opt.has_value()) {
149 ClimatePreset preset = *preset_opt;
150 bool result;
151
153 // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
154 result = this->parent_->button_turbo();
155
156 if (result) {
157 this->mode = CLIMATE_MODE_HEAT;
159 }
160 } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
161 if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
162 // We were in heat mode with Boost preset, and now preset is set to None, so revert to normal heat.
163 result = this->parent_->send_button(heat_button(this->heating_mode_));
164 if (result) {
165 this->preset.reset();
166 this->clear_custom_preset_();
167 }
168 } else {
169 ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
170 LOG_STR_ARG(climate_preset_to_string(preset)), LOG_STR_ARG(climate_mode_to_string(this->mode)),
171 LOG_STR_ARG(climate_preset_to_string(this->preset.value_or(CLIMATE_PRESET_NONE))));
172 }
173 } else {
174 ESP_LOGW(TAG, "Unsupported preset: %d", preset);
175 return;
176 }
177 } else if (call.has_custom_preset()) {
178 auto preset = call.get_custom_preset();
179 bool result;
180
181 if (preset == "M1") {
182 result = this->parent_->button_memory1();
183 } else if (preset == "M2") {
184 result = this->parent_->button_memory2();
185 } else if (preset == "M3") {
186 result = this->parent_->button_memory3();
187 } else if (preset == "LTD HT") {
188 result = this->parent_->button_heat();
189 } else if (preset == "EXT HT") {
190 result = this->parent_->button_ext_heat();
191 } else {
192 ESP_LOGW(TAG, "Unsupported preset: %.*s", (int) preset.size(), preset.c_str());
193 return;
194 }
195
196 if (result) {
198 }
199 }
200
201 auto fan_mode_opt = call.get_fan_mode();
202 if (fan_mode_opt.has_value()) {
203 // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
204 // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
205 auto fan_mode = *fan_mode_opt;
206 bool result;
207 if (fan_mode == CLIMATE_FAN_LOW) {
208 result = this->parent_->set_fan_speed(20);
209 } else if (fan_mode == CLIMATE_FAN_MEDIUM) {
210 result = this->parent_->set_fan_speed(50);
211 } else if (fan_mode == CLIMATE_FAN_HIGH) {
212 result = this->parent_->set_fan_speed(75);
213 } else {
214 ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
216 return;
217 }
218
219 if (result) {
220 this->set_fan_mode_(fan_mode);
221 }
222 } else if (call.has_custom_fan_mode()) {
223 auto fan_mode = call.get_custom_fan_mode();
224 auto fan_index = bedjet_fan_speed_to_step(fan_mode.c_str());
225 if (fan_index <= 19) {
226 ESP_LOGV(TAG, "[%s] Converted fan mode %.*s to bedjet fan step %d", this->get_name().c_str(),
227 (int) fan_mode.size(), fan_mode.c_str(), fan_index);
228 bool result = this->parent_->set_fan_index(fan_index);
229 if (result) {
231 }
232 }
233 }
234}
235
236void BedJetClimate::on_bedjet_state(bool is_ready) {}
237
239 ESP_LOGV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data);
240
241 auto converted_temp = bedjet_temp_to_c(data->target_temp_step);
242 if (converted_temp > 0)
243 this->target_temperature = converted_temp;
244
246 converted_temp = bedjet_temp_to_c(data->actual_temp_step);
247 } else {
248 converted_temp = bedjet_temp_to_c(data->ambient_temp_step);
249 }
250 if (converted_temp > 0) {
251 this->current_temperature = converted_temp;
252 }
253
254 const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
255 if (fan_mode_name != nullptr) {
256 this->set_custom_fan_mode_(fan_mode_name);
257 }
258
259 // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
260 switch (data->mode) {
261 case MODE_WAIT: // Biorhythm "wait" step: device is idle
262 case MODE_STANDBY:
263 this->mode = CLIMATE_MODE_OFF;
266 this->clear_custom_preset_();
267 this->preset.reset();
268 break;
269
270 case MODE_HEAT:
271 this->mode = CLIMATE_MODE_HEAT;
273 this->preset.reset();
274 if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
275 this->set_custom_preset_("LTD HT");
276 } else {
277 this->clear_custom_preset_();
278 }
279 break;
280
281 case MODE_EXTHT:
282 this->mode = CLIMATE_MODE_HEAT;
284 this->preset.reset();
285 if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
286 this->clear_custom_preset_();
287 } else {
288 this->set_custom_preset_("EXT HT");
289 }
290 break;
291
292 case MODE_COOL:
295 this->clear_custom_preset_();
296 this->preset.reset();
297 break;
298
299 case MODE_DRY:
300 this->mode = CLIMATE_MODE_DRY;
302 this->clear_custom_preset_();
303 this->preset.reset();
304 break;
305
306 case MODE_TURBO:
308 this->mode = CLIMATE_MODE_HEAT;
310 break;
311
312 default:
313 ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), data->mode);
314 break;
315 }
316
317 ESP_LOGV(TAG, "[%s] After on_status, new mode=%s", this->get_name().c_str(),
318 LOG_STR_ARG(climate_mode_to_string(this->mode)));
319 // FIXME: compare new state to previous state.
320 this->publish_state();
321}
322
331 if (!this->parent_->is_connected())
332 return false;
333 if (!this->parent_->has_status())
334 return false;
335
336 auto *status = this->parent_->get_status_packet();
337
338 if (status == nullptr)
339 return false;
340
341 this->on_status(status);
342
343 if (this->is_valid_()) {
344 // TODO: only if state changed?
345 this->publish_state();
346 this->status_clear_warning();
347 return true;
348 }
349
350 return false;
351}
352
354 ESP_LOGD(TAG, "[%s] update()", this->get_name().c_str());
355 // TODO: if the hub component is already polling, do we also need to include polling?
356 // We're already going to get on_status() at the hub's polling interval.
357 auto result = this->update_status_();
358 ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false");
359}
360
361} // namespace esphome::bedjet
362
363#endif
uint8_t fan_step
BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): 5 + 5 /< * fan_st...
BedjetMode mode
BedJet operating mode.
uint8_t status
Definition bl0942.h:8
void disable_loop()
Disable this component's loop.
void status_clear_warning()
Definition component.h:289
const StringRef & get_name() const
Definition entity_base.h:71
bool update_status_()
Attempts to update the climate device from the last received BedjetStatusPacket.
BedjetTemperatureSource temperature_source_
climate::ClimateTraits traits() override
void on_bedjet_state(bool is_ready) override
void reset_state_()
Resets states to defaults.
void control(const climate::ClimateCall &call) override
std::string describe() override
void on_status(const BedjetStatusPacket *data) override
This class is used to encode all control actions on a climate device.
Definition climate.h:34
bool has_custom_fan_mode() const
Definition climate.h:121
ClimateMode mode
The active mode of the climate device.
Definition climate.h:293
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:287
void set_supported_custom_fan_modes(std::initializer_list< const char * > modes)
Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits).
Definition climate.h:239
ClimateTraits get_traits()
Get the traits of this climate device with all overrides applied.
Definition climate.cpp:486
float target_temperature
The target temperature of the climate device.
Definition climate.h:274
void set_supported_custom_presets(std::initializer_list< const char * > presets)
Set the supported custom presets (stored on Climate, referenced by ClimateTraits).
Definition climate.h:250
bool set_preset_(ClimatePreset preset)
Set preset. Reset custom preset. Return true if preset has been changed.
Definition climate.cpp:696
bool set_custom_preset_(const char *preset)
Set custom preset. Reset primary preset. Return true if preset has been changed.
Definition climate.h:325
void clear_custom_preset_()
Clear custom preset.
Definition climate.cpp:703
bool set_fan_mode_(ClimateFanMode mode)
Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
Definition climate.cpp:685
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:267
ClimateAction action
The active state of the climate device.
Definition climate.h:296
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:437
optional< ClimatePreset > preset
The active preset of the climate device.
Definition climate.h:290
optional< ClimateDeviceRestoreState > restore_state_()
Restore the state of the climate device, call this from your setup() method.
Definition climate.cpp:362
bool set_custom_fan_mode_(const char *mode)
Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
Definition climate.h:315
const ClimatePresetMask & get_supported_presets() const
const std::vector< const char * > & get_supported_custom_fan_modes() const
const ClimateFanModeMask & get_supported_fan_modes() const
const std::vector< const char * > & get_supported_custom_presets() const
const ClimateModeMask & get_supported_modes() const
BedjetHeatMode
Optional heating strategies to use for climate::CLIMATE_MODE_HEAT.
@ HEAT_MODE_EXTENDED
HVACMode.HEAT is handled using BTN_EXTHT.
@ MODE_DRY
BedJet is in Dry mode (high speed, no heat)
@ MODE_EXTHT
BedJet is in Extended Heat mode (limited to 10 hours)
@ MODE_COOL
BedJet is in Cool mode (actually "Fan only" mode)
@ MODE_TURBO
BedJet is in Turbo mode (high heat, limited time)
@ MODE_HEAT
BedJet is in Heat mode (limited to 4 hours)
@ MODE_WAIT
BedJet is in "wait" mode, a step during a biorhythm program.
@ MODE_STANDBY
BedJet is Off.
float bedjet_temp_to_c(uint8_t temp)
Converts a BedJet temp step into degrees Celsius.
@ BTN_EXTHT
Enter Extended Heat mode (limited to 10 hours)
@ BTN_HEAT
Enter Heat mode (limited to 4 hours)
const LogString * climate_preset_to_string(ClimatePreset preset)
Convert the given PresetMode to a human-readable string.
ClimatePreset
Enum for all preset modes NOTE: If adding values, update ClimatePresetMask in climate_traits....
@ CLIMATE_PRESET_NONE
No preset is active.
@ CLIMATE_PRESET_BOOST
Device is in boost preset.
const LogString * climate_fan_mode_to_string(ClimateFanMode fan_mode)
Convert the given ClimateFanMode to a human-readable string.
ClimateMode
Enum for all modes a climate device can be in.
@ CLIMATE_MODE_DRY
The climate device is set to dry/humidity mode.
@ CLIMATE_MODE_FAN_ONLY
The climate device only has the fan enabled, no heating or cooling is taking place.
@ CLIMATE_MODE_HEAT
The climate device is set to heat to reach the target temperature.
@ CLIMATE_MODE_OFF
The climate device is off.
const LogString * climate_mode_to_string(ClimateMode mode)
Convert the given ClimateMode to a human-readable string.
@ CLIMATE_ACTION_IDLE
The climate device is idle (monitoring climate but no action needed)
@ CLIMATE_ACTION_DRYING
The climate device is drying.
@ CLIMATE_ACTION_HEATING
The climate device is actively heating.
@ CLIMATE_ACTION_COOLING
The climate device is actively cooling.
@ CLIMATE_FAN_MEDIUM
The fan mode is set to Medium.
@ CLIMATE_FAN_LOW
The fan mode is set to Low.
@ CLIMATE_FAN_OFF
The fan mode is set to Off.
@ CLIMATE_FAN_HIGH
The fan mode is set to High.
The format of a BedJet V3 status packet.