ESPHome  2024.12.2
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 
6 namespace esphome {
7 namespace bedjet {
8 
9 using namespace esphome::climate;
10 
11 static 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 
17 static 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 
26 static inline BedjetButton heat_button(BedjetHeatMode mode) {
27  return mode == HEAT_MODE_EXTENDED ? BTN_EXTHT : BTN_HEAT;
28 }
29 
30 std::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;
78  this->action = CLIMATE_ACTION_IDLE;
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 
89  ESP_LOGD(TAG, "Received BedJetClimate::control");
90  if (!this->parent_->is_connected()) {
91  ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
92  return;
93  }
94 
95  if (call.get_mode().has_value()) {
96  ClimateMode mode = *call.get_mode();
97  bool button_result;
98  switch (mode) {
99  case CLIMATE_MODE_OFF:
100  button_result = this->parent_->button_off();
101  break;
102  case CLIMATE_MODE_HEAT:
103  button_result = this->parent_->send_button(heat_button(this->heating_mode_));
104  break;
106  button_result = this->parent_->button_cool();
107  break;
108  case CLIMATE_MODE_DRY:
109  button_result = this->parent_->button_dry();
110  break;
111  default:
112  ESP_LOGW(TAG, "Unsupported mode: %d", mode);
113  return;
114  }
115 
116  if (button_result) {
117  this->mode = mode;
118  // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
119  this->custom_preset.reset();
120  this->preset.reset();
121  }
122  }
123 
124  if (call.get_target_temperature().has_value()) {
125  auto target_temp = *call.get_target_temperature();
126  auto result = this->parent_->set_target_temp(target_temp);
127 
128  if (result) {
129  this->target_temperature = target_temp;
130  }
131  }
132 
133  if (call.get_preset().has_value()) {
134  ClimatePreset preset = *call.get_preset();
135  bool result;
136 
137  if (preset == CLIMATE_PRESET_BOOST) {
138  // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
139  result = this->parent_->button_turbo();
140 
141  if (result) {
142  this->mode = CLIMATE_MODE_HEAT;
143  this->preset = CLIMATE_PRESET_BOOST;
144  this->custom_preset.reset();
145  }
146  } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
147  if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
148  // We were in heat mode with Boost preset, and now preset is set to None, so revert to normal heat.
149  result = this->parent_->send_button(heat_button(this->heating_mode_));
150  if (result) {
151  this->preset.reset();
152  this->custom_preset.reset();
153  }
154  } else {
155  ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
156  LOG_STR_ARG(climate_preset_to_string(preset)), LOG_STR_ARG(climate_mode_to_string(this->mode)),
157  LOG_STR_ARG(climate_preset_to_string(this->preset.value_or(CLIMATE_PRESET_NONE))));
158  }
159  } else {
160  ESP_LOGW(TAG, "Unsupported preset: %d", preset);
161  return;
162  }
163  } else if (call.get_custom_preset().has_value()) {
164  std::string preset = *call.get_custom_preset();
165  bool result;
166 
167  if (preset == "M1") {
168  result = this->parent_->button_memory1();
169  } else if (preset == "M2") {
170  result = this->parent_->button_memory2();
171  } else if (preset == "M3") {
172  result = this->parent_->button_memory3();
173  } else if (preset == "LTD HT") {
174  result = this->parent_->button_heat();
175  } else if (preset == "EXT HT") {
176  result = this->parent_->button_ext_heat();
177  } else {
178  ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
179  return;
180  }
181 
182  if (result) {
183  this->custom_preset = preset;
184  this->preset.reset();
185  }
186  }
187 
188  if (call.get_fan_mode().has_value()) {
189  // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
190  // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
191  auto fan_mode = *call.get_fan_mode();
192  bool result;
193  if (fan_mode == CLIMATE_FAN_LOW) {
194  result = this->parent_->set_fan_speed(20);
195  } else if (fan_mode == CLIMATE_FAN_MEDIUM) {
196  result = this->parent_->set_fan_speed(50);
197  } else if (fan_mode == CLIMATE_FAN_HIGH) {
198  result = this->parent_->set_fan_speed(75);
199  } else {
200  ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
201  LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
202  return;
203  }
204 
205  if (result) {
206  this->fan_mode = fan_mode;
207  this->custom_fan_mode.reset();
208  }
209  } else if (call.get_custom_fan_mode().has_value()) {
210  auto fan_mode = *call.get_custom_fan_mode();
211  auto fan_index = bedjet_fan_speed_to_step(fan_mode);
212  if (fan_index <= 19) {
213  ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
214  fan_index);
215  bool result = this->parent_->set_fan_index(fan_index);
216  if (result) {
217  this->custom_fan_mode = fan_mode;
218  this->fan_mode.reset();
219  }
220  }
221  }
222 }
223 
224 void BedJetClimate::on_bedjet_state(bool is_ready) {}
225 
227  ESP_LOGV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data);
228 
229  auto converted_temp = bedjet_temp_to_c(data->target_temp_step);
230  if (converted_temp > 0)
231  this->target_temperature = converted_temp;
232 
233  if (this->temperature_source_ == TEMPERATURE_SOURCE_OUTLET) {
234  converted_temp = bedjet_temp_to_c(data->actual_temp_step);
235  } else {
236  converted_temp = bedjet_temp_to_c(data->ambient_temp_step);
237  }
238  if (converted_temp > 0) {
239  this->current_temperature = converted_temp;
240  }
241 
242  const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
243  if (fan_mode_name != nullptr) {
244  this->custom_fan_mode = *fan_mode_name;
245  }
246 
247  // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
248  switch (data->mode) {
249  case MODE_WAIT: // Biorhythm "wait" step: device is idle
250  case MODE_STANDBY:
251  this->mode = CLIMATE_MODE_OFF;
252  this->action = CLIMATE_ACTION_IDLE;
253  this->fan_mode = CLIMATE_FAN_OFF;
254  this->custom_preset.reset();
255  this->preset.reset();
256  break;
257 
258  case MODE_HEAT:
259  this->mode = CLIMATE_MODE_HEAT;
260  this->action = CLIMATE_ACTION_HEATING;
261  this->preset.reset();
262  if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
263  this->set_custom_preset_("LTD HT");
264  } else {
265  this->custom_preset.reset();
266  }
267  break;
268 
269  case MODE_EXTHT:
270  this->mode = CLIMATE_MODE_HEAT;
271  this->action = CLIMATE_ACTION_HEATING;
272  this->preset.reset();
273  if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
274  this->custom_preset.reset();
275  } else {
276  this->set_custom_preset_("EXT HT");
277  }
278  break;
279 
280  case MODE_COOL:
281  this->mode = CLIMATE_MODE_FAN_ONLY;
282  this->action = CLIMATE_ACTION_COOLING;
283  this->custom_preset.reset();
284  this->preset.reset();
285  break;
286 
287  case MODE_DRY:
288  this->mode = CLIMATE_MODE_DRY;
289  this->action = CLIMATE_ACTION_DRYING;
290  this->custom_preset.reset();
291  this->preset.reset();
292  break;
293 
294  case MODE_TURBO:
296  this->custom_preset.reset();
297  this->mode = CLIMATE_MODE_HEAT;
298  this->action = CLIMATE_ACTION_HEATING;
299  break;
300 
301  default:
302  ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), data->mode);
303  break;
304  }
305 
306  ESP_LOGV(TAG, "[%s] After on_status, new mode=%s", this->get_name().c_str(),
307  LOG_STR_ARG(climate_mode_to_string(this->mode)));
308  // FIXME: compare new state to previous state.
309  this->publish_state();
310 }
311 
320  if (!this->parent_->is_connected())
321  return false;
322  if (!this->parent_->has_status())
323  return false;
324 
325  auto *status = this->parent_->get_status_packet();
326 
327  if (status == nullptr)
328  return false;
329 
330  this->on_status(status);
331 
332  if (this->is_valid_()) {
333  // TODO: only if state changed?
334  this->publish_state();
335  this->status_clear_warning();
336  return true;
337  }
338 
339  return false;
340 }
341 
343  ESP_LOGD(TAG, "[%s] update()", this->get_name().c_str());
344  // TODO: if the hub component is already polling, do we also need to include polling?
345  // We're already going to get on_status() at the hub's polling interval.
346  auto result = this->update_status_();
347  ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false");
348 }
349 
350 } // namespace bedjet
351 } // namespace esphome
352 
353 #endif
void on_bedjet_state(bool is_ready) override
This class is used to encode all control actions on a climate device.
Definition: climate.h:33
ClimatePreset
Enum for all preset modes.
Definition: climate_mode.h:82
void reset_state_()
Resets states to defaults.
const optional< ClimateMode > & get_mode() const
Definition: climate.cpp:273
Enter Extended Heat mode (limited to 10 hours)
Definition: bedjet_const.h:63
const LogString * climate_mode_to_string(ClimateMode mode)
Convert the given ClimateMode to a human-readable string.
Definition: climate_mode.cpp:6
BedJet is in Extended Heat mode (limited to 10 hours)
Definition: bedjet_const.h:26
Enter Heat mode (limited to 4 hours)
Definition: bedjet_const.h:57
BedJet is in "wait" mode, a step during a biorhythm program.
Definition: bedjet_const.h:32
bool has_value() const
Definition: optional.h:87
The format of a BedJet V3 status packet.
Definition: bedjet_codec.h:39
float bedjet_temp_to_c(uint8_t temp)
Converts a BedJet temp step into degrees Celsius.
BedJet is in Dry mode (high speed, no heat)
Definition: bedjet_const.h:30
BedJet is in Cool mode (actually "Fan only" mode)
Definition: bedjet_const.h:28
bool update_status_()
Attempts to update the climate device from the last received BedjetStatusPacket.
std::string describe() override
HVACMode.HEAT is handled using BTN_EXTHT.
Definition: bedjet_const.h:40
const optional< std::string > & get_custom_preset() const
Definition: climate.cpp:281
const optional< ClimatePreset > & get_preset() const
Definition: climate.cpp:280
void control(const climate::ClimateCall &call) override
BedJet is in Heat mode (limited to 4 hours)
Definition: bedjet_const.h:22
uint8_t custom_preset
Definition: climate.h:579
BedjetMode mode
BedJet operating mode.
Definition: bedjet_codec.h:183
const LogString * climate_preset_to_string(ClimatePreset preset)
Convert the given PresetMode to a human-readable string.
const optional< std::string > & get_custom_fan_mode() const
Definition: climate.cpp:279
const optional< float > & get_target_temperature() const
Definition: climate.cpp:274
ClimateMode
Enum for all modes a climate device can be in.
Definition: climate_mode.h:10
ClimateFanMode fan_mode
Definition: climate.h:573
const LogString * climate_fan_mode_to_string(ClimateFanMode fan_mode)
Convert the given ClimateFanMode to a human-readable string.
uint8_t status
Definition: bl0942.h:74
const optional< ClimateFanMode > & get_fan_mode() const
Definition: climate.cpp:278
BedJet is in Turbo mode (high heat, limited time)
Definition: bedjet_const.h:24
BedjetHeatMode
Optional heating strategies to use for climate::CLIMATE_MODE_HEAT.
Definition: bedjet_const.h:36
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
uint8_t fan_step
BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5...
Definition: bedjet_codec.h:186
uint8_t custom_fan_mode
Definition: climate.h:574
float target_temperature
Definition: climate.h:138
void on_status(const BedjetStatusPacket *data) override
ClimatePreset preset
Definition: climate.h:578