ESPHome  2022.12.8
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 
12 float bedjet_temp_to_c(const uint8_t temp) {
13  // BedJet temp is "C*2"; to get C, divide by 2.
14  return temp / 2.0f;
15 }
16 
17 static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
18  if (fan_step < BEDJET_FAN_SPEED_COUNT)
19  return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
20  return nullptr;
21 }
22 
23 static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
24  for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
25  if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
26  return i;
27  }
28  }
29  return -1;
30 }
31 
32 static inline BedjetButton heat_button(BedjetHeatMode mode) {
33  return mode == HEAT_MODE_EXTENDED ? BTN_EXTHT : BTN_HEAT;
34 }
35 
36 std::string BedJetClimate::describe() { return "BedJet Climate"; }
37 
39  LOG_CLIMATE("", "BedJet Climate", this);
40  auto traits = this->get_traits();
41 
42  ESP_LOGCONFIG(TAG, " Supported modes:");
43  for (auto mode : traits.get_supported_modes()) {
44  ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode)));
45  }
46  if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
47  ESP_LOGCONFIG(TAG, " - BedJet heating mode: EXT HT");
48  } else {
49  ESP_LOGCONFIG(TAG, " - BedJet heating mode: HEAT");
50  }
51 
52  ESP_LOGCONFIG(TAG, " Supported fan modes:");
53  for (const auto &mode : traits.get_supported_fan_modes()) {
54  ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
55  }
56  for (const auto &mode : traits.get_supported_custom_fan_modes()) {
57  ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
58  }
59 
60  ESP_LOGCONFIG(TAG, " Supported presets:");
61  for (auto preset : traits.get_supported_presets()) {
62  ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
63  }
64  for (const auto &preset : traits.get_supported_custom_presets()) {
65  ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
66  }
67 }
68 
70  // restore set points
71  auto restore = this->restore_state_();
72  if (restore.has_value()) {
73  ESP_LOGI(TAG, "Restored previous saved state.");
74  restore->apply(this);
75  } else {
76  // Initial status is unknown until we connect
77  this->reset_state_();
78  }
79 }
80 
83  this->mode = CLIMATE_MODE_OFF;
84  this->action = CLIMATE_ACTION_IDLE;
85  this->target_temperature = NAN;
86  this->current_temperature = NAN;
87  this->preset.reset();
88  this->custom_preset.reset();
89  this->publish_state();
90 }
91 
93 
95  ESP_LOGD(TAG, "Received BedJetClimate::control");
96  if (!this->parent_->is_connected()) {
97  ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
98  return;
99  }
100 
101  if (call.get_mode().has_value()) {
102  ClimateMode mode = *call.get_mode();
103  bool button_result;
104  switch (mode) {
105  case CLIMATE_MODE_OFF:
106  button_result = this->parent_->button_off();
107  break;
108  case CLIMATE_MODE_HEAT:
109  button_result = this->parent_->send_button(heat_button(this->heating_mode_));
110  break;
112  button_result = this->parent_->button_cool();
113  break;
114  case CLIMATE_MODE_DRY:
115  button_result = this->parent_->button_dry();
116  break;
117  default:
118  ESP_LOGW(TAG, "Unsupported mode: %d", mode);
119  return;
120  }
121 
122  if (button_result) {
123  this->mode = mode;
124  // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
125  this->custom_preset.reset();
126  this->preset.reset();
127  }
128  }
129 
130  if (call.get_target_temperature().has_value()) {
131  auto target_temp = *call.get_target_temperature();
132  auto result = this->parent_->set_target_temp(target_temp);
133 
134  if (result) {
135  this->target_temperature = target_temp;
136  }
137  }
138 
139  if (call.get_preset().has_value()) {
140  ClimatePreset preset = *call.get_preset();
141  bool result;
142 
143  if (preset == CLIMATE_PRESET_BOOST) {
144  // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
145  result = this->parent_->button_turbo();
146 
147  if (result) {
148  this->mode = CLIMATE_MODE_HEAT;
149  this->preset = CLIMATE_PRESET_BOOST;
150  this->custom_preset.reset();
151  }
152  } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
153  if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
154  // We were in heat mode with Boost preset, and now preset is set to None, so revert to normal heat.
155  result = this->parent_->send_button(heat_button(this->heating_mode_));
156  if (result) {
157  this->preset.reset();
158  this->custom_preset.reset();
159  }
160  } else {
161  ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
162  LOG_STR_ARG(climate_preset_to_string(preset)), LOG_STR_ARG(climate_mode_to_string(this->mode)),
163  LOG_STR_ARG(climate_preset_to_string(this->preset.value_or(CLIMATE_PRESET_NONE))));
164  }
165  } else {
166  ESP_LOGW(TAG, "Unsupported preset: %d", preset);
167  return;
168  }
169  } else if (call.get_custom_preset().has_value()) {
170  std::string preset = *call.get_custom_preset();
171  bool result;
172 
173  if (preset == "M1") {
174  result = this->parent_->button_memory1();
175  } else if (preset == "M2") {
176  result = this->parent_->button_memory2();
177  } else if (preset == "M3") {
178  result = this->parent_->button_memory3();
179  } else if (preset == "LTD HT") {
180  result = this->parent_->button_heat();
181  } else if (preset == "EXT HT") {
182  result = this->parent_->button_ext_heat();
183  } else {
184  ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
185  return;
186  }
187 
188  if (result) {
189  this->custom_preset = preset;
190  this->preset.reset();
191  }
192  }
193 
194  if (call.get_fan_mode().has_value()) {
195  // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
196  // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
197  auto fan_mode = *call.get_fan_mode();
198  bool result;
199  if (fan_mode == CLIMATE_FAN_LOW) {
200  result = this->parent_->set_fan_speed(20);
201  } else if (fan_mode == CLIMATE_FAN_MEDIUM) {
202  result = this->parent_->set_fan_speed(50);
203  } else if (fan_mode == CLIMATE_FAN_HIGH) {
204  result = this->parent_->set_fan_speed(75);
205  } else {
206  ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
207  LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
208  return;
209  }
210 
211  if (result) {
212  this->fan_mode = fan_mode;
213  this->custom_fan_mode.reset();
214  }
215  } else if (call.get_custom_fan_mode().has_value()) {
216  auto fan_mode = *call.get_custom_fan_mode();
217  auto fan_index = bedjet_fan_speed_to_step(fan_mode);
218  if (fan_index <= 19) {
219  ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
220  fan_index);
221  bool result = this->parent_->set_fan_index(fan_index);
222  if (result) {
223  this->custom_fan_mode = fan_mode;
224  this->fan_mode.reset();
225  }
226  }
227  }
228 }
229 
230 void BedJetClimate::on_bedjet_state(bool is_ready) {}
231 
233  ESP_LOGV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data);
234 
235  auto converted_temp = bedjet_temp_to_c(data->target_temp_step);
236  if (converted_temp > 0)
237  this->target_temperature = converted_temp;
238 
239  converted_temp = bedjet_temp_to_c(data->ambient_temp_step);
240  if (converted_temp > 0)
241  this->current_temperature = converted_temp;
242 
243  const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
244  if (fan_mode_name != nullptr) {
245  this->custom_fan_mode = *fan_mode_name;
246  }
247 
248  // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
249  switch (data->mode) {
250  case MODE_WAIT: // Biorhythm "wait" step: device is idle
251  case MODE_STANDBY:
252  this->mode = CLIMATE_MODE_OFF;
253  this->action = CLIMATE_ACTION_IDLE;
254  this->fan_mode = CLIMATE_FAN_OFF;
255  this->custom_preset.reset();
256  this->preset.reset();
257  break;
258 
259  case MODE_HEAT:
260  this->mode = CLIMATE_MODE_HEAT;
261  this->action = CLIMATE_ACTION_HEATING;
262  this->preset.reset();
263  if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
264  this->set_custom_preset_("LTD HT");
265  } else {
266  this->custom_preset.reset();
267  }
268  break;
269 
270  case MODE_EXTHT:
271  this->mode = CLIMATE_MODE_HEAT;
272  this->action = CLIMATE_ACTION_HEATING;
273  this->preset.reset();
274  if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
275  this->custom_preset.reset();
276  } else {
277  this->set_custom_preset_("EXT HT");
278  }
279  break;
280 
281  case MODE_COOL:
282  this->mode = CLIMATE_MODE_FAN_ONLY;
283  this->action = CLIMATE_ACTION_COOLING;
284  this->custom_preset.reset();
285  this->preset.reset();
286  break;
287 
288  case MODE_DRY:
289  this->mode = CLIMATE_MODE_DRY;
290  this->action = CLIMATE_ACTION_DRYING;
291  this->custom_preset.reset();
292  this->preset.reset();
293  break;
294 
295  case MODE_TURBO:
297  this->custom_preset.reset();
298  this->mode = CLIMATE_MODE_HEAT;
299  this->action = CLIMATE_ACTION_HEATING;
300  break;
301 
302  default:
303  ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), data->mode);
304  break;
305  }
306 
307  ESP_LOGV(TAG, "[%s] After on_status, new mode=%s", this->get_name().c_str(),
308  LOG_STR_ARG(climate_mode_to_string(this->mode)));
309  // FIXME: compare new state to previous state.
310  this->publish_state();
311 }
312 
321  if (!this->parent_->is_connected())
322  return false;
323  if (!this->parent_->has_status())
324  return false;
325 
326  auto *status = this->parent_->get_status_packet();
327 
328  if (status == nullptr)
329  return false;
330 
331  this->on_status(status);
332 
333  if (this->is_valid_()) {
334  // TODO: only if state changed?
335  this->publish_state();
336  this->status_clear_warning();
337  return true;
338  }
339 
340  return false;
341 }
342 
344  ESP_LOGD(TAG, "[%s] update()", this->get_name().c_str());
345  // TODO: if the hub component is already polling, do we also need to include polling?
346  // We're already going to get on_status() at the hub's polling interval.
347  auto result = this->update_status_();
348  ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false");
349 }
350 
351 } // namespace bedjet
352 } // namespace esphome
353 
354 #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:80
void reset_state_()
Resets states to defaults.
const optional< ClimateMode > & get_mode() const
Definition: climate.cpp:260
Enter Extended Heat mode (limited to 10 hours)
Definition: bedjet_const.h:55
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:49
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
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.
float bedjet_temp_to_c(const uint8_t temp)
Converts a BedJet temp step into degrees Celsius.
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:272
const optional< ClimatePreset > & get_preset() const
Definition: climate.cpp:271
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:546
BedjetMode mode
BedJet operating mode.
Definition: bedjet_codec.h:151
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:270
const optional< float > & get_target_temperature() const
Definition: climate.cpp:261
ClimateMode
Enum for all modes a climate device can be in.
Definition: climate_mode.h:10
ClimateFanMode fan_mode
Definition: climate.h:540
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:23
const optional< ClimateFanMode > & get_fan_mode() const
Definition: climate.cpp:269
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
Definition: a4988.cpp:4
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:154
uint8_t custom_fan_mode
Definition: climate.h:541
float target_temperature
Definition: climate.h:550
void on_status(const BedjetStatusPacket *data) override
ClimatePreset preset
Definition: climate.h:545