ESPHome 2025.9.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 std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
12 if (fan_step < BEDJET_FAN_SPEED_COUNT)
13 return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
14 return nullptr;
15}
16
17static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
18 for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
19 if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
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.c_str());
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.c_str());
60 }
61}
62
64 // restore set points
65 auto restore = this->restore_state_();
66 if (restore.has_value()) {
67 ESP_LOGI(TAG, "Restored previous saved state.");
68 restore->apply(this);
69 } else {
70 // Initial status is unknown until we connect
71 this->reset_state_();
72 }
73}
74
77 this->mode = CLIMATE_MODE_OFF;
79 this->target_temperature = NAN;
80 this->current_temperature = NAN;
81 this->preset.reset();
82 this->custom_preset.reset();
83 this->publish_state();
84}
85
87 // This component is controlled via the parent BedJetHub
88 // Empty loop not needed, disable to save CPU cycles
89 this->disable_loop();
90}
91
93 ESP_LOGD(TAG, "Received BedJetClimate::control");
94 if (!this->parent_->is_connected()) {
95 ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
96 return;
97 }
98
99 if (call.get_mode().has_value()) {
100 ClimateMode mode = *call.get_mode();
101 bool button_result;
102 switch (mode) {
103 case CLIMATE_MODE_OFF:
104 button_result = this->parent_->button_off();
105 break;
107 button_result = this->parent_->send_button(heat_button(this->heating_mode_));
108 break;
110 button_result = this->parent_->button_cool();
111 break;
112 case CLIMATE_MODE_DRY:
113 button_result = this->parent_->button_dry();
114 break;
115 default:
116 ESP_LOGW(TAG, "Unsupported mode: %d", mode);
117 return;
118 }
119
120 if (button_result) {
121 this->mode = mode;
122 // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
123 this->custom_preset.reset();
124 this->preset.reset();
125 }
126 }
127
128 if (call.get_target_temperature().has_value()) {
129 auto target_temp = *call.get_target_temperature();
130 auto result = this->parent_->set_target_temp(target_temp);
131
132 if (result) {
133 this->target_temperature = target_temp;
134 }
135 }
136
137 if (call.get_preset().has_value()) {
138 ClimatePreset preset = *call.get_preset();
139 bool result;
140
142 // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
143 result = this->parent_->button_turbo();
144
145 if (result) {
146 this->mode = CLIMATE_MODE_HEAT;
147 this->preset = CLIMATE_PRESET_BOOST;
148 this->custom_preset.reset();
149 }
150 } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
151 if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
152 // We were in heat mode with Boost preset, and now preset is set to None, so revert to normal heat.
153 result = this->parent_->send_button(heat_button(this->heating_mode_));
154 if (result) {
155 this->preset.reset();
156 this->custom_preset.reset();
157 }
158 } else {
159 ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
160 LOG_STR_ARG(climate_preset_to_string(preset)), LOG_STR_ARG(climate_mode_to_string(this->mode)),
161 LOG_STR_ARG(climate_preset_to_string(this->preset.value_or(CLIMATE_PRESET_NONE))));
162 }
163 } else {
164 ESP_LOGW(TAG, "Unsupported preset: %d", preset);
165 return;
166 }
167 } else if (call.get_custom_preset().has_value()) {
168 std::string preset = *call.get_custom_preset();
169 bool result;
170
171 if (preset == "M1") {
172 result = this->parent_->button_memory1();
173 } else if (preset == "M2") {
174 result = this->parent_->button_memory2();
175 } else if (preset == "M3") {
176 result = this->parent_->button_memory3();
177 } else if (preset == "LTD HT") {
178 result = this->parent_->button_heat();
179 } else if (preset == "EXT HT") {
180 result = this->parent_->button_ext_heat();
181 } else {
182 ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
183 return;
184 }
185
186 if (result) {
187 this->custom_preset = preset;
188 this->preset.reset();
189 }
190 }
191
192 if (call.get_fan_mode().has_value()) {
193 // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
194 // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
195 auto fan_mode = *call.get_fan_mode();
196 bool result;
197 if (fan_mode == CLIMATE_FAN_LOW) {
198 result = this->parent_->set_fan_speed(20);
199 } else if (fan_mode == CLIMATE_FAN_MEDIUM) {
200 result = this->parent_->set_fan_speed(50);
201 } else if (fan_mode == CLIMATE_FAN_HIGH) {
202 result = this->parent_->set_fan_speed(75);
203 } else {
204 ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
206 return;
207 }
208
209 if (result) {
210 this->fan_mode = fan_mode;
211 this->custom_fan_mode.reset();
212 }
213 } else if (call.get_custom_fan_mode().has_value()) {
214 auto fan_mode = *call.get_custom_fan_mode();
215 auto fan_index = bedjet_fan_speed_to_step(fan_mode);
216 if (fan_index <= 19) {
217 ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
218 fan_index);
219 bool result = this->parent_->set_fan_index(fan_index);
220 if (result) {
222 this->fan_mode.reset();
223 }
224 }
225 }
226}
227
228void BedJetClimate::on_bedjet_state(bool is_ready) {}
229
231 ESP_LOGV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data);
232
233 auto converted_temp = bedjet_temp_to_c(data->target_temp_step);
234 if (converted_temp > 0)
235 this->target_temperature = converted_temp;
236
238 converted_temp = bedjet_temp_to_c(data->actual_temp_step);
239 } else {
240 converted_temp = bedjet_temp_to_c(data->ambient_temp_step);
241 }
242 if (converted_temp > 0) {
243 this->current_temperature = converted_temp;
244 }
245
246 const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
247 if (fan_mode_name != nullptr) {
248 this->custom_fan_mode = *fan_mode_name;
249 }
250
251 // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
252 switch (data->mode) {
253 case MODE_WAIT: // Biorhythm "wait" step: device is idle
254 case MODE_STANDBY:
255 this->mode = CLIMATE_MODE_OFF;
258 this->custom_preset.reset();
259 this->preset.reset();
260 break;
261
262 case MODE_HEAT:
263 this->mode = CLIMATE_MODE_HEAT;
265 this->preset.reset();
266 if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
267 this->set_custom_preset_("LTD HT");
268 } else {
269 this->custom_preset.reset();
270 }
271 break;
272
273 case MODE_EXTHT:
274 this->mode = CLIMATE_MODE_HEAT;
276 this->preset.reset();
277 if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
278 this->custom_preset.reset();
279 } else {
280 this->set_custom_preset_("EXT HT");
281 }
282 break;
283
284 case MODE_COOL:
287 this->custom_preset.reset();
288 this->preset.reset();
289 break;
290
291 case MODE_DRY:
292 this->mode = CLIMATE_MODE_DRY;
294 this->custom_preset.reset();
295 this->preset.reset();
296 break;
297
298 case MODE_TURBO:
300 this->custom_preset.reset();
301 this->mode = CLIMATE_MODE_HEAT;
303 break;
304
305 default:
306 ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), data->mode);
307 break;
308 }
309
310 ESP_LOGV(TAG, "[%s] After on_status, new mode=%s", this->get_name().c_str(),
311 LOG_STR_ARG(climate_mode_to_string(this->mode)));
312 // FIXME: compare new state to previous state.
313 this->publish_state();
314}
315
324 if (!this->parent_->is_connected())
325 return false;
326 if (!this->parent_->has_status())
327 return false;
328
329 auto *status = this->parent_->get_status_packet();
330
331 if (status == nullptr)
332 return false;
333
334 this->on_status(status);
335
336 if (this->is_valid_()) {
337 // TODO: only if state changed?
338 this->publish_state();
339 this->status_clear_warning();
340 return true;
341 }
342
343 return false;
344}
345
347 ESP_LOGD(TAG, "[%s] update()", this->get_name().c_str());
348 // TODO: if the hub component is already polling, do we also need to include polling?
349 // We're already going to get on_status() at the hub's polling interval.
350 auto result = this->update_status_();
351 ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false");
352}
353
354} // namespace bedjet
355} // namespace esphome
356
357#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()
const StringRef & get_name() const
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:33
const optional< float > & get_target_temperature() const
Definition climate.cpp:274
const optional< std::string > & get_custom_fan_mode() const
Definition climate.cpp:279
ClimateMode mode
The active mode of the climate device.
Definition climate.h:173
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:199
bool set_custom_preset_(const std::string &preset)
Set custom preset. Reset primary preset. Return true if preset has been changed.
Definition climate.cpp:565
ClimateTraits get_traits()
Get the traits of this climate device with all overrides applied.
Definition climate.cpp:440
float target_temperature
The target temperature of the climate device.
Definition climate.h:186
optional< std::string > custom_fan_mode
The active custom fan mode of the climate device.
Definition climate.h:205
optional< std::string > custom_preset
The active custom preset mode of the climate device.
Definition climate.h:211
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:179
ClimateAction action
The active state of the climate device.
Definition climate.h:176
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:395
optional< ClimatePreset > preset
The active preset of the climate device.
Definition climate.h:208
optional< ClimateDeviceRestoreState > restore_state_()
Restore the state of the climate device, call this from your setup() method.
Definition climate.cpp:329
const std::set< climate::ClimatePreset > & get_supported_presets() const
const std::set< std::string > & get_supported_custom_fan_modes() const
const std::set< std::string > & get_supported_custom_presets() const
const std::set< ClimateFanMode > & get_supported_fan_modes() const
const std::set< ClimateMode > & get_supported_modes() const
bool has_value() const
Definition optional.h:92
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.
@ 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.
uint8_t target_temp_step
Target temp that the BedJet will try to heat to. See actual_temp_step.
uint8_t actual_temp_step
Actual temp of the air blown by the BedJet fan; value represents 2 * /< degrees_celsius.
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 ambient_temp_step
Current ambient air temp.