ESPHome  2022.6.3
bedjet.cpp
Go to the documentation of this file.
1 #include "bedjet.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 
18 uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
19  // 0 = 5%
20  // 19 = 100%
21  return 5 * fan + 5;
22 }
23 
24 static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
25  if (fan_step >= 0 && fan_step <= 19)
26  return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
27  return nullptr;
28 }
29 
30 static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
31  for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) {
32  if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
33  return i;
34  }
35  }
36  return -1;
37 }
38 
39 static BedjetButton heat_button(BedjetHeatMode mode) {
40  BedjetButton btn = BTN_HEAT;
41  if (mode == HEAT_MODE_EXTENDED) {
42  btn = BTN_EXTHT;
43  }
44  return btn;
45 }
46 
48  auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE);
49  auto status = this->write_bedjet_packet_(pkt);
50 
51  if (status) {
52  ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
53  }
54 }
55 
57  LOG_CLIMATE("", "BedJet Climate", this);
58  auto traits = this->get_traits();
59 
60  ESP_LOGCONFIG(TAG, " Supported modes:");
61  for (auto mode : traits.get_supported_modes()) {
62  ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode)));
63  }
64 
65  ESP_LOGCONFIG(TAG, " Supported fan modes:");
66  for (const auto &mode : traits.get_supported_fan_modes()) {
67  ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
68  }
69  for (const auto &mode : traits.get_supported_custom_fan_modes()) {
70  ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
71  }
72 
73  ESP_LOGCONFIG(TAG, " Supported presets:");
74  for (auto preset : traits.get_supported_presets()) {
75  ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
76  }
77  for (const auto &preset : traits.get_supported_custom_presets()) {
78  ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
79  }
80 }
81 
82 void Bedjet::setup() {
83  this->codec_ = make_unique<BedjetCodec>();
84 
85  // restore set points
86  auto restore = this->restore_state_();
87  if (restore.has_value()) {
88  ESP_LOGI(TAG, "Restored previous saved state.");
89  restore->apply(this);
90  } else {
91  // Initial status is unknown until we connect
92  this->reset_state_();
93  }
94 
95 #ifdef USE_TIME
96  this->setup_time_();
97 #endif
98 }
99 
102  this->mode = climate::CLIMATE_MODE_OFF;
103  this->action = climate::CLIMATE_ACTION_IDLE;
104  this->target_temperature = NAN;
105  this->current_temperature = NAN;
106  this->preset.reset();
107  this->custom_preset.reset();
108  this->publish_state();
109 }
110 
111 void Bedjet::loop() {}
112 
113 void Bedjet::control(const ClimateCall &call) {
114  ESP_LOGD(TAG, "Received Bedjet::control");
115  if (this->node_state != espbt::ClientState::ESTABLISHED) {
116  ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
117  return;
118  }
119 
120  if (call.get_mode().has_value()) {
121  ClimateMode mode = *call.get_mode();
122  BedjetPacket *pkt;
123  switch (mode) {
125  pkt = this->codec_->get_button_request(BTN_OFF);
126  break;
128  pkt = this->codec_->get_button_request(heat_button(this->heating_mode_));
129  break;
131  pkt = this->codec_->get_button_request(BTN_COOL);
132  break;
134  pkt = this->codec_->get_button_request(BTN_DRY);
135  break;
136  default:
137  ESP_LOGW(TAG, "Unsupported mode: %d", mode);
138  return;
139  }
140 
141  auto status = this->write_bedjet_packet_(pkt);
142 
143  if (status) {
144  ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
145  } else {
146  this->force_refresh_ = true;
147  this->mode = mode;
148  // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
149  this->custom_preset.reset();
150  this->preset.reset();
151  }
152  }
153 
154  if (call.get_target_temperature().has_value()) {
155  auto target_temp = *call.get_target_temperature();
156  auto *pkt = this->codec_->get_set_target_temp_request(target_temp);
157  auto status = this->write_bedjet_packet_(pkt);
158 
159  if (status) {
160  ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
161  } else {
162  this->target_temperature = target_temp;
163  }
164  }
165 
166  if (call.get_preset().has_value()) {
167  ClimatePreset preset = *call.get_preset();
168  BedjetPacket *pkt;
169 
170  if (preset == climate::CLIMATE_PRESET_BOOST) {
171  pkt = this->codec_->get_button_request(BTN_TURBO);
172  } else {
173  ESP_LOGW(TAG, "Unsupported preset: %d", preset);
174  return;
175  }
176 
177  auto status = this->write_bedjet_packet_(pkt);
178  if (status) {
179  ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
180  } else {
181  // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
182  this->mode = climate::CLIMATE_MODE_HEAT;
183  this->preset = preset;
184  this->custom_preset.reset();
185  this->force_refresh_ = true;
186  }
187  } else if (call.get_custom_preset().has_value()) {
188  std::string preset = *call.get_custom_preset();
189  BedjetPacket *pkt;
190 
191  if (preset == "M1") {
192  pkt = this->codec_->get_button_request(BTN_M1);
193  } else if (preset == "M2") {
194  pkt = this->codec_->get_button_request(BTN_M2);
195  } else if (preset == "M3") {
196  pkt = this->codec_->get_button_request(BTN_M3);
197  } else if (preset == "LTD HT") {
198  pkt = this->codec_->get_button_request(BTN_HEAT);
199  } else if (preset == "EXT HT") {
200  pkt = this->codec_->get_button_request(BTN_EXTHT);
201  } else {
202  ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
203  return;
204  }
205 
206  auto status = this->write_bedjet_packet_(pkt);
207  if (status) {
208  ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
209  } else {
210  this->force_refresh_ = true;
211  this->custom_preset = preset;
212  this->preset.reset();
213  }
214  }
215 
216  if (call.get_fan_mode().has_value()) {
217  // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
218  // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
219  auto fan_mode = *call.get_fan_mode();
220  BedjetPacket *pkt;
222  pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */);
223  } else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) {
224  pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */);
225  } else if (fan_mode == climate::CLIMATE_FAN_HIGH) {
226  pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */);
227  } else {
228  ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
229  LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
230  return;
231  }
232 
233  auto status = this->write_bedjet_packet_(pkt);
234  if (status) {
235  ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
236  } else {
237  this->force_refresh_ = true;
238  }
239  } else if (call.get_custom_fan_mode().has_value()) {
240  auto fan_mode = *call.get_custom_fan_mode();
241  auto fan_step = bedjet_fan_speed_to_step(fan_mode);
242  if (fan_step >= 0 && fan_step <= 19) {
243  ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
244  fan_step);
245  // The index should represent the fan_step index.
246  BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step);
247  auto status = this->write_bedjet_packet_(pkt);
248  if (status) {
249  ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
250  } else {
251  this->force_refresh_ = true;
252  }
253  }
254  }
255 }
256 
257 void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
258  switch (event) {
259  case ESP_GATTC_DISCONNECT_EVT: {
260  ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason);
261  this->status_set_warning();
262  break;
263  }
264  case ESP_GATTC_SEARCH_CMPL_EVT: {
265  auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID);
266  if (chr == nullptr) {
267  ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str());
268  break;
269  }
270  this->char_handle_cmd_ = chr->handle;
271 
272  chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID);
273  if (chr == nullptr) {
274  ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str());
275  break;
276  }
277 
278  this->char_handle_status_ = chr->handle;
279  // We also need to obtain the config descriptor for this handle.
280  // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be
281  // able to look it up.
282  auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_);
283  if (descr == nullptr) {
284  ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications",
285  this->char_handle_status_);
286  } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
287  descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
288  ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_,
289  descr->uuid.to_string().c_str());
290  } else {
291  this->config_descr_status_ = descr->handle;
292  }
293 
294  chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID);
295  if (chr != nullptr) {
296  this->char_handle_name_ = chr->handle;
297  auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_,
298  ESP_GATT_AUTH_REQ_NONE);
299  if (status) {
300  ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status);
301  }
302  }
303 
304  ESP_LOGD(TAG, "Services complete: obtained char handles.");
305  this->node_state = espbt::ClientState::ESTABLISHED;
306 
307  this->set_notify_(true);
308 
309 #ifdef USE_TIME
310  if (this->time_id_.has_value()) {
311  this->send_local_time();
312  }
313 #endif
314  break;
315  }
316  case ESP_GATTC_WRITE_DESCR_EVT: {
317  if (param->write.status != ESP_GATT_OK) {
318  // ESP_GATT_INVALID_ATTR_LEN
319  ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status);
320  break;
321  }
322  // [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0
323  // This might be the enable-notify descriptor? (or disable-notify)
324  ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
325  param->write.status);
326  break;
327  }
328  case ESP_GATTC_WRITE_CHAR_EVT: {
329  if (param->write.status != ESP_GATT_OK) {
330  ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status);
331  break;
332  }
333  if (param->write.handle == this->char_handle_cmd_) {
334  if (this->force_refresh_) {
335  // Command write was successful. Publish the pending state, hoping that notify will kick in.
336  this->publish_state();
337  }
338  }
339  break;
340  }
341  case ESP_GATTC_READ_CHAR_EVT: {
342  if (param->read.conn_id != this->parent_->conn_id)
343  break;
344  if (param->read.status != ESP_GATT_OK) {
345  ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
346  break;
347  }
348  if (param->read.handle == this->char_handle_status_) {
349  // This is the additional packet that doesn't fit in the notify packet.
350  this->codec_->decode_extra(param->read.value, param->read.value_len);
351  } else if (param->read.handle == this->char_handle_name_) {
352  // The data should represent the name.
353  if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) {
354  std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len);
355  // this->set_name(bedjet_name);
356  ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str());
357  }
358  }
359  break;
360  }
361  case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
362  // This event means that ESP received the request to enable notifications on the client side. But we also have to
363  // tell the server that we want it to send notifications. Normally BLEClient parent would handle this
364  // automatically, but as soon as we set our status to Established, the parent is going to purge all the
365  // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable
366  // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write
367  // doesn't break anything.
368 
369  if (param->reg_for_notify.handle != this->char_handle_status_) {
370  ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x",
371  this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_);
372  break;
373  }
374 
375  this->write_notify_config_descriptor_(true);
376  this->last_notify_ = 0;
377  this->force_refresh_ = true;
378  break;
379  }
380  case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
381  // This event is not handled by the parent BLEClient, so we need to do this either way.
382  if (param->unreg_for_notify.handle != this->char_handle_status_) {
383  ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x",
384  this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_);
385  break;
386  }
387 
388  this->write_notify_config_descriptor_(false);
389  this->last_notify_ = 0;
390  // Now we wait until the next update() poll to re-register notify...
391  break;
392  }
393  case ESP_GATTC_NOTIFY_EVT: {
394  if (param->notify.handle != this->char_handle_status_) {
395  ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(),
396  this->char_handle_status_, param->notify.handle);
397  break;
398  }
399 
400  // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we
401  // throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds).
402  // Another idea would be to keep notify off by default, and use update() as an opportunity to turn on
403  // notify to get enough data to update status, then turn off notify again.
404 
405  uint32_t now = millis();
406  auto delta = now - this->last_notify_;
407 
408  if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) {
409  bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
410  this->last_notify_ = now;
411 
412  if (needs_extra) {
413  // this means the packet was partial, so read the status characteristic to get the second part.
414  auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id,
415  this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE);
416  if (status) {
417  ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str());
418  }
419  }
420 
421  if (this->force_refresh_) {
422  // If we requested an immediate update, do that now.
423  this->update();
424  this->force_refresh_ = false;
425  }
426  }
427  break;
428  }
429  default:
430  ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event);
431  break;
432  }
433 }
434 
443  auto handle = this->config_descr_status_;
444  if (handle == 0) {
445  ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_);
446  return -1;
447  }
448 
449  // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits.
450  uint8_t notify_en[] = {0, 0};
451  notify_en[0] = enable;
452  auto status =
453  esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en),
454  &notify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
455  if (status) {
456  ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
457  return status;
458  }
459  ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false",
460  handle);
461  return ESP_GATT_OK;
462 }
463 
464 #ifdef USE_TIME
465 
467  if (this->time_id_.has_value()) {
468  auto *time_id = *this->time_id_;
469  time::ESPTime now = time_id->now();
470  if (now.is_valid()) {
471  this->set_clock(now.hour, now.minute);
472  ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
473  }
474  } else {
475  ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
476  }
477 }
478 
481  if (this->time_id_.has_value()) {
482  this->send_local_time();
483  auto *time_id = *this->time_id_;
484  time_id->add_on_time_sync_callback([this] { this->send_local_time(); });
485  } else {
486  ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
487  }
488 }
489 #endif
490 
492 void Bedjet::set_clock(uint8_t hour, uint8_t minute) {
493  if (this->node_state != espbt::ClientState::ESTABLISHED) {
494  ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
495  return;
496  }
497 
498  BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
499  auto status = this->write_bedjet_packet_(pkt);
500  if (status) {
501  ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
502  } else {
503  ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
504  }
505 }
506 
509  if (this->node_state != espbt::ClientState::ESTABLISHED) {
510  if (!this->parent_->enabled) {
511  ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str());
512  } else {
513  ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str());
514  }
515  return -1;
516  }
517  auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_,
518  pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP,
519  ESP_GATT_AUTH_REQ_NONE);
520  return status;
521 }
522 
524 uint8_t Bedjet::set_notify_(const bool enable) {
525  uint8_t status;
526  if (enable) {
527  status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
528  this->char_handle_status_);
529  if (status) {
530  ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
531  }
532  } else {
533  status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
534  this->char_handle_status_);
535  if (status) {
536  ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status);
537  }
538  }
539  ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
540  return status;
541 }
542 
548  if (!this->codec_->has_status())
549  return false;
550 
551  BedjetStatusPacket status = *this->codec_->get_status_packet();
552 
553  auto converted_temp = bedjet_temp_to_c(status.target_temp_step);
554  if (converted_temp > 0)
555  this->target_temperature = converted_temp;
556  converted_temp = bedjet_temp_to_c(status.ambient_temp_step);
557  if (converted_temp > 0)
558  this->current_temperature = converted_temp;
559 
560  const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step);
561  if (fan_mode_name != nullptr) {
562  this->custom_fan_mode = *fan_mode_name;
563  }
564 
565  // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
566  switch (status.mode) {
567  case MODE_WAIT: // Biorhythm "wait" step: device is idle
568  case MODE_STANDBY:
569  this->mode = climate::CLIMATE_MODE_OFF;
570  this->action = climate::CLIMATE_ACTION_IDLE;
572  this->custom_preset.reset();
573  this->preset.reset();
574  break;
575 
576  case MODE_HEAT:
577  this->mode = climate::CLIMATE_MODE_HEAT;
578  this->action = climate::CLIMATE_ACTION_HEATING;
579  this->preset.reset();
580  if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
581  this->set_custom_preset_("LTD HT");
582  } else {
583  this->custom_preset.reset();
584  }
585  break;
586 
587  case MODE_EXTHT:
588  this->mode = climate::CLIMATE_MODE_HEAT;
589  this->action = climate::CLIMATE_ACTION_HEATING;
590  this->preset.reset();
591  if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
592  this->custom_preset.reset();
593  } else {
594  this->set_custom_preset_("EXT HT");
595  }
596  break;
597 
598  case MODE_COOL:
599  this->mode = climate::CLIMATE_MODE_FAN_ONLY;
600  this->action = climate::CLIMATE_ACTION_COOLING;
601  this->custom_preset.reset();
602  this->preset.reset();
603  break;
604 
605  case MODE_DRY:
606  this->mode = climate::CLIMATE_MODE_DRY;
607  this->action = climate::CLIMATE_ACTION_DRYING;
608  this->custom_preset.reset();
609  this->preset.reset();
610  break;
611 
612  case MODE_TURBO:
614  this->custom_preset.reset();
615  this->mode = climate::CLIMATE_MODE_HEAT;
616  this->action = climate::CLIMATE_ACTION_HEATING;
617  break;
618 
619  default:
620  ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode);
621  break;
622  }
623 
624  if (this->is_valid_()) {
625  this->publish_state();
626  this->codec_->clear_status();
627  this->status_clear_warning();
628  }
629 
630  return true;
631 }
632 
634  ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str());
635 
636  if (this->node_state != espbt::ClientState::ESTABLISHED) {
637  if (!this->parent()->enabled) {
638  ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str());
639  } else {
640  // Possibly still trying to connect.
641  ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str());
642  }
643 
644  return;
645  }
646 
647  auto result = this->update_status_();
648  if (!result) {
649  uint32_t now = millis();
650  uint32_t diff = now - this->last_notify_;
651 
652  if (this->last_notify_ == 0) {
653  // This means we're connected and haven't received a notification, so it likely means that the BedJet is off.
654  // However, it could also mean that it's running, but failing to send notifications.
655  // We can try to unregister for notifications now, and then re-register, hoping to clear it up...
656  // But how do we know for sure which state we're in, and how do we actually clear out the buggy state?
657 
658  ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str());
659  this->set_notify_(false);
660  } else if (diff > NOTIFY_WARN_THRESHOLD) {
661  ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000);
662  }
663 
664  if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) {
665  ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_);
666  this->parent()->set_enabled(false);
667  this->parent()->set_enabled(true);
668  }
669  }
670 }
671 
672 } // namespace bedjet
673 } // namespace esphome
674 
675 #endif
Enter Cool mode (fan only)
Definition: bedjet_const.h:39
This class is used to encode all control actions on a climate device.
Definition: climate.h:33
The fan mode is set to Low.
Definition: climate_mode.h:54
Start the M2 biorhythm/preset program.
Definition: bedjet_const.h:52
uint8_t fan_step
BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5...
Definition: bedjet_base.h:64
void update() override
Definition: bedjet.cpp:633
BedjetMode mode
BedJet operating mode.
Definition: bedjet_base.h:102
The climate device is drying.
Definition: climate_mode.h:41
ClimatePreset
Enum for all preset modes.
Definition: climate_mode.h:80
bool is_valid() const
Check if this ESPTime is valid (all fields in range and year is greater than 2018) ...
void loop() override
Definition: bedjet.cpp:111
const optional< ClimateMode > & get_mode() const
Definition: climate.cpp:260
Enter Extended Heat mode (limited to 10 hours)
Definition: bedjet_const.h:47
const LogString * climate_mode_to_string(ClimateMode mode)
Convert the given ClimateMode to a human-readable string.
Definition: climate_mode.cpp:6
void upgrade_firmware()
Attempts to check for and apply firmware updates.
Definition: bedjet.cpp:47
The climate device is set to heat to reach the target temperature.
Definition: climate_mode.h:18
BedJet is in Extended Heat mode (limited to 10 hours)
Definition: bedjet_const.h:18
Enter Heat mode (limited to 4 hours)
Definition: bedjet_const.h:41
BedJet is in "wait" mode, a step during a biorhythm program.
Definition: bedjet_const.h:24
void send_local_time()
Attempts to sync the local time (via time_id) to the BedJet device.
Definition: bedjet.cpp:466
Turn BedJet off.
Definition: bedjet_const.h:37
uint8_t ambient_temp_step
Current ambient air temp.
Definition: bedjet_base.h:75
bool has_value() const
Definition: optional.h:87
The climate device is set to dry/humidity mode.
Definition: climate_mode.h:22
The format of a BedJet V3 status packet.
Definition: bedjet_base.h:40
BedjetMode mode
BedJet operating mode.
Definition: bedjet_base.h:61
uint8_t set_notify_(bool enable)
Configures the local ESP BLE client to register (true) or unregister (false) for status notifications...
Definition: bedjet.cpp:524
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:26
BedJet is in Dry mode (high speed, no heat)
Definition: bedjet_const.h:22
BedJet is in Cool mode (actually "Fan only" mode)
Definition: bedjet_const.h:20
float bedjet_temp_to_c(const uint8_t temp)
Converts a BedJet temp step into degrees Celsius.
Definition: bedjet.cpp:12
uint8_t minute
minutes after the hour [0-59]
Enter Dry mode (high speed, no heat)
Definition: bedjet_const.h:45
Start the M1 biorhythm/preset program.
Definition: bedjet_const.h:50
HVACMode.HEAT is handled using BTN_EXTHT.
Definition: bedjet_const.h:32
void setup_time_()
Initializes time sync callbacks to support syncing current time to the BedJet.
Definition: bedjet.cpp:480
const optional< std::string > & get_custom_preset() const
Definition: climate.cpp:272
void setup() override
Definition: bedjet.cpp:82
const optional< ClimatePreset > & get_preset() const
Definition: climate.cpp:271
A more user-friendly version of struct tm from time.h.
bool update_status_()
Attempts to update the climate device from the last received BedjetStatusPacket.
Definition: bedjet.cpp:547
BedJet is in Heat mode (limited to 4 hours)
Definition: bedjet_const.h:14
void dump_config() override
Definition: bedjet.cpp:56
uint8_t custom_preset
Definition: climate.h:546
const LogString * climate_preset_to_string(ClimatePreset preset)
Convert the given PresetMode to a human-readable string.
Start the M3 biorhythm/preset program.
Definition: bedjet_const.h:54
The climate device is actively heating.
Definition: climate_mode.h:37
uint8_t write_notify_config_descriptor_(bool enable)
Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT.
Definition: bedjet.cpp:442
const optional< std::string > & get_custom_fan_mode() const
Definition: climate.cpp:270
The fan mode is set to Off.
Definition: climate_mode.h:50
const optional< float > & get_target_temperature() const
Definition: climate.cpp:261
The fan mode is set to High.
Definition: climate_mode.h:58
ClimateMode
Enum for all modes a climate device can be in.
Definition: climate_mode.h:10
The climate device is off.
Definition: climate_mode.h:12
ClimateFanMode fan_mode
Definition: climate.h:540
void reset_state_()
Resets states to defaults.
Definition: bedjet.cpp:101
const LogString * climate_fan_mode_to_string(ClimateFanMode fan_mode)
Convert the given ClimateFanMode to a human-readable string.
uint8_t write_bedjet_packet_(BedjetPacket *pkt)
Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID.
Definition: bedjet.cpp:508
const optional< ClimateFanMode > & get_fan_mode() const
Definition: climate.cpp:269
Device is in boost preset.
Definition: climate_mode.h:88
BedJet is in Turbo mode (high heat, limited time)
Definition: bedjet_const.h:16
BedjetHeatMode
Optional heating strategies to use for climate::CLIMATE_MODE_HEAT.
Definition: bedjet_const.h:28
void set_clock(uint8_t hour, uint8_t minute)
Attempt to set the BedJet device&#39;s clock to the specified time.
Definition: bedjet.cpp:492
uint8_t target_temp_step
Target temp that the BedJet will try to heat to. See actual_temp_step.
Definition: bedjet_base.h:58
Definition: a4988.cpp:4
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override
Definition: bedjet.cpp:257
The climate device is idle (monitoring climate but no action needed)
Definition: climate_mode.h:39
void control(const climate::ClimateCall &call) override
Definition: bedjet.cpp:113
The fan mode is set to Medium.
Definition: climate_mode.h:56
Enter Turbo mode (high heat, limited to 10 minutes)
Definition: bedjet_const.h:43
uint8_t fan_step
BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5...
Definition: bedjet_base.h:105
The climate device only has the fan enabled, no heating or cooling is taking place.
Definition: climate_mode.h:20
uint8_t bedjet_fan_step_to_speed(const uint8_t fan)
Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
Definition: bedjet.cpp:18
The climate device is actively cooling.
Definition: climate_mode.h:35
uint8_t custom_fan_mode
Definition: climate.h:541
float target_temperature
Definition: climate.h:550
uint8_t hour
hours since midnight [0-23]
ClimatePreset preset
Definition: climate.h:545
Request a firmware update. This will also restart the Bedjet.
Definition: bedjet_const.h:64