ESPHome 2026.5.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 {
7namespace bedjet {
8
9using namespace esphome::climate;
10
11static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
12 if (fan_step < BEDJET_FAN_SPEED_COUNT)
13 return BEDJET_FAN_STEP_NAMES[fan_step];
14 return nullptr;
15}
16
17static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) {
18 for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
19 if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) {
20 return i;
21 }
22 }
23 return -1;
24}
25
26static inline BedjetButton heat_button(BedjetHeatMode mode) {
28}
29
30std::string BedJetClimate::describe() { return "BedJet Climate"; }
31
33 LOG_CLIMATE("", "BedJet Climate", this);
34 auto traits = this->get_traits();
35
36 ESP_LOGCONFIG(TAG, " Supported modes:");
37 for (auto mode : traits.get_supported_modes()) {
38 ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode)));
39 }
40 if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
41 ESP_LOGCONFIG(TAG, " - BedJet heating mode: EXT HT");
42 } else {
43 ESP_LOGCONFIG(TAG, " - BedJet heating mode: HEAT");
44 }
45
46 ESP_LOGCONFIG(TAG, " Supported fan modes:");
47 for (const auto &mode : traits.get_supported_fan_modes()) {
48 ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
49 }
50 for (const auto &mode : traits.get_supported_custom_fan_modes()) {
51 ESP_LOGCONFIG(TAG, " - %s (c)", mode);
52 }
53
54 ESP_LOGCONFIG(TAG, " Supported presets:");
55 for (auto preset : traits.get_supported_presets()) {
56 ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
57 }
58 for (const auto &preset : traits.get_supported_custom_presets()) {
59 ESP_LOGCONFIG(TAG, " - %s (c)", preset);
60 }
61}
62
64 // Set custom modes once during setup — stored on Climate base class, wired via get_traits()
65 this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
67 this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
68 "M1",
69 "M2",
70 "M3",
71 });
72
73 // restore set points
74 auto restore = this->restore_state_();
75 if (restore.has_value()) {
76 ESP_LOGI(TAG, "Restored previous saved state.");
77 restore->apply(this);
78 } else {
79 // Initial status is unknown until we connect
80 this->reset_state_();
81 }
82}
83
86 this->mode = CLIMATE_MODE_OFF;
88 this->target_temperature = NAN;
89 this->current_temperature = NAN;
90 this->preset.reset();
92 this->publish_state();
93}
94
96 // This component is controlled via the parent BedJetHub
97 // Empty loop not needed, disable to save CPU cycles
98 this->disable_loop();
99}
100
102 ESP_LOGD(TAG, "Received BedJetClimate::control");
103 if (!this->parent_->is_connected()) {
104 ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
105 return;
106 }
107
108 auto mode_opt = call.get_mode();
109 if (mode_opt.has_value()) {
110 ClimateMode mode = *mode_opt;
111 bool button_result;
112 switch (mode) {
113 case CLIMATE_MODE_OFF:
114 button_result = this->parent_->button_off();
115 break;
117 button_result = this->parent_->send_button(heat_button(this->heating_mode_));
118 break;
120 button_result = this->parent_->button_cool();
121 break;
122 case CLIMATE_MODE_DRY:
123 button_result = this->parent_->button_dry();
124 break;
125 default:
126 ESP_LOGW(TAG, "Unsupported mode: %d", mode);
127 return;
128 }
129
130 if (button_result) {
131 this->mode = mode;
132 // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
133 this->clear_custom_preset_();
134 this->preset.reset();
135 }
136 }
137
138 auto target_temp_opt = call.get_target_temperature();
139 if (target_temp_opt.has_value()) {
140 auto target_temp = *target_temp_opt;
141 auto result = this->parent_->set_target_temp(target_temp);
142
143 if (result) {
144 this->target_temperature = target_temp;
145 }
146 }
147
148 auto preset_opt = call.get_preset();
149 if (preset_opt.has_value()) {
150 ClimatePreset preset = *preset_opt;
151 bool result;
152
154 // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
155 result = this->parent_->button_turbo();
156
157 if (result) {
158 this->mode = CLIMATE_MODE_HEAT;
160 }
161 } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
162 if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
163 // We were in heat mode with Boost preset, and now preset is set to None, so revert to normal heat.
164 result = this->parent_->send_button(heat_button(this->heating_mode_));
165 if (result) {
166 this->preset.reset();
167 this->clear_custom_preset_();
168 }
169 } else {
170 ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
171 LOG_STR_ARG(climate_preset_to_string(preset)), LOG_STR_ARG(climate_mode_to_string(this->mode)),
172 LOG_STR_ARG(climate_preset_to_string(this->preset.value_or(CLIMATE_PRESET_NONE))));
173 }
174 } else {
175 ESP_LOGW(TAG, "Unsupported preset: %d", preset);
176 return;
177 }
178 } else if (call.has_custom_preset()) {
179 auto preset = call.get_custom_preset();
180 bool result;
181
182 if (preset == "M1") {
183 result = this->parent_->button_memory1();
184 } else if (preset == "M2") {
185 result = this->parent_->button_memory2();
186 } else if (preset == "M3") {
187 result = this->parent_->button_memory3();
188 } else if (preset == "LTD HT") {
189 result = this->parent_->button_heat();
190 } else if (preset == "EXT HT") {
191 result = this->parent_->button_ext_heat();
192 } else {
193 ESP_LOGW(TAG, "Unsupported preset: %.*s", (int) preset.size(), preset.c_str());
194 return;
195 }
196
197 if (result) {
199 }
200 }
201
202 auto fan_mode_opt = call.get_fan_mode();
203 if (fan_mode_opt.has_value()) {
204 // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
205 // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
206 auto fan_mode = *fan_mode_opt;
207 bool result;
208 if (fan_mode == CLIMATE_FAN_LOW) {
209 result = this->parent_->set_fan_speed(20);
210 } else if (fan_mode == CLIMATE_FAN_MEDIUM) {
211 result = this->parent_->set_fan_speed(50);
212 } else if (fan_mode == CLIMATE_FAN_HIGH) {
213 result = this->parent_->set_fan_speed(75);
214 } else {
215 ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
217 return;
218 }
219
220 if (result) {
221 this->set_fan_mode_(fan_mode);
222 }
223 } else if (call.has_custom_fan_mode()) {
224 auto fan_mode = call.get_custom_fan_mode();
225 auto fan_index = bedjet_fan_speed_to_step(fan_mode.c_str());
226 if (fan_index <= 19) {
227 ESP_LOGV(TAG, "[%s] Converted fan mode %.*s to bedjet fan step %d", this->get_name().c_str(),
228 (int) fan_mode.size(), fan_mode.c_str(), fan_index);
229 bool result = this->parent_->set_fan_index(fan_index);
230 if (result) {
232 }
233 }
234 }
235}
236
237void BedJetClimate::on_bedjet_state(bool is_ready) {}
238
240 ESP_LOGV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data);
241
242 auto converted_temp = bedjet_temp_to_c(data->target_temp_step);
243 if (converted_temp > 0)
244 this->target_temperature = converted_temp;
245
247 converted_temp = bedjet_temp_to_c(data->actual_temp_step);
248 } else {
249 converted_temp = bedjet_temp_to_c(data->ambient_temp_step);
250 }
251 if (converted_temp > 0) {
252 this->current_temperature = converted_temp;
253 }
254
255 const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
256 if (fan_mode_name != nullptr) {
257 this->set_custom_fan_mode_(fan_mode_name);
258 }
259
260 // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
261 switch (data->mode) {
262 case MODE_WAIT: // Biorhythm "wait" step: device is idle
263 case MODE_STANDBY:
264 this->mode = CLIMATE_MODE_OFF;
267 this->clear_custom_preset_();
268 this->preset.reset();
269 break;
270
271 case MODE_HEAT:
272 this->mode = CLIMATE_MODE_HEAT;
274 this->preset.reset();
275 if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
276 this->set_custom_preset_("LTD HT");
277 } else {
278 this->clear_custom_preset_();
279 }
280 break;
281
282 case MODE_EXTHT:
283 this->mode = CLIMATE_MODE_HEAT;
285 this->preset.reset();
286 if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
287 this->clear_custom_preset_();
288 } else {
289 this->set_custom_preset_("EXT HT");
290 }
291 break;
292
293 case MODE_COOL:
296 this->clear_custom_preset_();
297 this->preset.reset();
298 break;
299
300 case MODE_DRY:
301 this->mode = CLIMATE_MODE_DRY;
303 this->clear_custom_preset_();
304 this->preset.reset();
305 break;
306
307 case MODE_TURBO:
309 this->mode = CLIMATE_MODE_HEAT;
311 break;
312
313 default:
314 ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), data->mode);
315 break;
316 }
317
318 ESP_LOGV(TAG, "[%s] After on_status, new mode=%s", this->get_name().c_str(),
319 LOG_STR_ARG(climate_mode_to_string(this->mode)));
320 // FIXME: compare new state to previous state.
321 this->publish_state();
322}
323
332 if (!this->parent_->is_connected())
333 return false;
334 if (!this->parent_->has_status())
335 return false;
336
337 auto *status = this->parent_->get_status_packet();
338
339 if (status == nullptr)
340 return false;
341
342 this->on_status(status);
343
344 if (this->is_valid_()) {
345 // TODO: only if state changed?
346 this->publish_state();
347 this->status_clear_warning();
348 return true;
349 }
350
351 return false;
352}
353
355 ESP_LOGD(TAG, "[%s] update()", this->get_name().c_str());
356 // TODO: if the hub component is already polling, do we also need to include polling?
357 // We're already going to get on_status() at the hub's polling interval.
358 auto result = this->update_status_();
359 ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false");
360}
361
362} // namespace bedjet
363} // namespace esphome
364
365#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:306
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:485
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:695
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:702
bool set_fan_mode_(ClimateFanMode mode)
Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
Definition climate.cpp:684
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:436
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.
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
The format of a BedJet V3 status packet.