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];
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]) {
48 auto *pkt = this->codec_->get_button_request(
MAGIC_UPDATE);
49 auto status = this->write_bedjet_packet_(pkt);
52 ESP_LOGW(TAG,
"[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
57 LOG_CLIMATE(
"",
"BedJet Climate",
this);
58 auto traits = this->get_traits();
60 ESP_LOGCONFIG(TAG,
" Supported modes:");
61 for (
auto mode : traits.get_supported_modes()) {
65 ESP_LOGCONFIG(TAG,
" Supported fan modes:");
66 for (
const auto &mode : traits.get_supported_fan_modes()) {
69 for (
const auto &mode : traits.get_supported_custom_fan_modes()) {
70 ESP_LOGCONFIG(TAG,
" - %s (c)", mode.c_str());
73 ESP_LOGCONFIG(TAG,
" Supported presets:");
74 for (
auto preset : traits.get_supported_presets()) {
77 for (
const auto &
preset : traits.get_supported_custom_presets()) {
78 ESP_LOGCONFIG(TAG,
" - %s (c)",
preset.c_str());
83 this->codec_ = make_unique<BedjetCodec>();
86 auto restore = this->restore_state_();
87 if (restore.has_value()) {
88 ESP_LOGI(TAG,
"Restored previous saved state.");
105 this->current_temperature = NAN;
108 this->publish_state();
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.");
125 pkt = this->codec_->get_button_request(
BTN_OFF);
128 pkt = this->codec_->get_button_request(heat_button(this->heating_mode_));
131 pkt = this->codec_->get_button_request(
BTN_COOL);
134 pkt = this->codec_->get_button_request(
BTN_DRY);
137 ESP_LOGW(TAG,
"Unsupported mode: %d", mode);
141 auto status = this->write_bedjet_packet_(pkt);
144 ESP_LOGW(TAG,
"[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
146 this->force_refresh_ =
true;
156 auto *pkt = this->codec_->get_set_target_temp_request(target_temp);
157 auto status = this->write_bedjet_packet_(pkt);
160 ESP_LOGW(TAG,
"[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
171 pkt = this->codec_->get_button_request(
BTN_TURBO);
173 ESP_LOGW(TAG,
"Unsupported preset: %d", preset);
177 auto status = this->write_bedjet_packet_(pkt);
179 ESP_LOGW(TAG,
"[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
185 this->force_refresh_ =
true;
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);
202 ESP_LOGW(TAG,
"Unsupported preset: %s", preset.c_str());
206 auto status = this->write_bedjet_packet_(pkt);
208 ESP_LOGW(TAG,
"[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
210 this->force_refresh_ =
true;
212 this->preset.reset();
222 pkt = this->codec_->get_set_fan_speed_request(3 );
224 pkt = this->codec_->get_set_fan_speed_request(9 );
226 pkt = this->codec_->get_set_fan_speed_request(14 );
228 ESP_LOGW(TAG,
"[%s] Unsupported fan mode: %s", this->get_name().c_str(),
233 auto status = this->write_bedjet_packet_(pkt);
235 ESP_LOGW(TAG,
"[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
237 this->force_refresh_ =
true;
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(),
246 BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step);
247 auto status = this->write_bedjet_packet_(pkt);
249 ESP_LOGW(TAG,
"[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
251 this->force_refresh_ =
true;
259 case ESP_GATTC_DISCONNECT_EVT: {
260 ESP_LOGV(TAG,
"Disconnected: reason=%d", param->disconnect.reason);
261 this->status_set_warning();
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());
270 this->char_handle_cmd_ = chr->handle;
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());
278 this->char_handle_status_ = chr->handle;
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());
291 this->config_descr_status_ = descr->handle;
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);
300 ESP_LOGI(TAG,
"[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status);
304 ESP_LOGD(TAG,
"Services complete: obtained char handles.");
305 this->node_state = espbt::ClientState::ESTABLISHED;
307 this->set_notify_(
true);
310 if (this->time_id_.has_value()) {
311 this->send_local_time();
316 case ESP_GATTC_WRITE_DESCR_EVT: {
317 if (param->write.status != ESP_GATT_OK) {
319 ESP_LOGW(TAG,
"Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status);
324 ESP_LOGV(TAG,
"[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
325 param->write.status);
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);
333 if (param->write.handle == this->char_handle_cmd_) {
334 if (this->force_refresh_) {
336 this->publish_state();
341 case ESP_GATTC_READ_CHAR_EVT: {
342 if (param->read.conn_id != this->parent_->conn_id)
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);
348 if (param->read.handle == this->char_handle_status_) {
350 this->codec_->decode_extra(param->read.value, param->read.value_len);
351 }
else if (param->read.handle == this->char_handle_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);
356 ESP_LOGV(TAG,
"[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str());
361 case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
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_);
375 this->write_notify_config_descriptor_(
true);
376 this->last_notify_ = 0;
377 this->force_refresh_ =
true;
380 case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
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_);
388 this->write_notify_config_descriptor_(
false);
389 this->last_notify_ = 0;
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);
406 auto delta = now - this->last_notify_;
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;
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);
417 ESP_LOGI(TAG,
"[%s] Unable to read extended status packet", this->get_name().c_str());
421 if (this->force_refresh_) {
424 this->force_refresh_ =
false;
430 ESP_LOGVV(TAG,
"[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event);
443 auto handle = this->config_descr_status_;
445 ESP_LOGW(TAG,
"No descriptor found for notify of handle 0x%x", this->char_handle_status_);
450 uint8_t notify_en[] = {0, 0};
451 notify_en[0] = enable;
453 esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle,
sizeof(notify_en),
454 ¬ify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
456 ESP_LOGW(TAG,
"esp_ble_gattc_write_char_descr error, status=%d", status);
459 ESP_LOGD(TAG,
"[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ?
"true" :
"false",
467 if (this->time_id_.has_value()) {
468 auto *time_id = *this->time_id_;
472 ESP_LOGD(TAG,
"Using time component to set BedJet clock: %d:%02d", now.
hour, now.
minute);
475 ESP_LOGI(TAG,
"`time_id` is not configured: will not sync BedJet clock.");
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(); });
486 ESP_LOGI(TAG,
"`time_id` is not configured: will not sync BedJet clock.");
493 if (this->node_state != espbt::ClientState::ESTABLISHED) {
494 ESP_LOGV(TAG,
"[%s] Not connected, cannot send time.", this->get_name().c_str());
498 BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
499 auto status = this->write_bedjet_packet_(pkt);
501 ESP_LOGW(TAG,
"Failed setting BedJet clock: %d", status);
503 ESP_LOGD(TAG,
"[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
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());
513 ESP_LOGW(TAG,
"[%s] Cannot write packet: Not connected", this->get_name().c_str());
517 auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_,
519 ESP_GATT_AUTH_REQ_NONE);
527 status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
528 this->char_handle_status_);
530 ESP_LOGW(TAG,
"[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
533 status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
534 this->char_handle_status_);
536 ESP_LOGW(TAG,
"[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status);
539 ESP_LOGV(TAG,
"[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
548 if (!this->codec_->has_status())
554 if (converted_temp > 0)
557 if (converted_temp > 0)
558 this->current_temperature = converted_temp;
560 const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.
fan_step);
561 if (fan_mode_name !=
nullptr) {
566 switch (status.
mode) {
581 this->set_custom_preset_(
"LTD HT");
594 this->set_custom_preset_(
"EXT HT");
620 ESP_LOGW(TAG,
"[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.
mode);
624 if (this->is_valid_()) {
625 this->publish_state();
626 this->codec_->clear_status();
627 this->status_clear_warning();
634 ESP_LOGV(TAG,
"[%s] update()", this->get_name().c_str());
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());
641 ESP_LOGD(TAG,
"[%s] Not connected, enabled=true", this->get_name().c_str());
647 auto result = this->update_status_();
650 uint32_t diff = now - this->last_notify_;
652 if (this->last_notify_ == 0) {
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);
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);
Enter Cool mode (fan only)
This class is used to encode all control actions on a climate device.
The fan mode is set to Low.
Start the M2 biorhythm/preset program.
uint8_t fan_step
BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5...
BedjetMode mode
BedJet operating mode.
The climate device is drying.
ClimatePreset
Enum for all preset modes.
bool is_valid() const
Check if this ESPTime is valid (all fields in range and year is greater than 2018) ...
const optional< ClimateMode > & get_mode() const
Enter Extended Heat mode (limited to 10 hours)
const LogString * climate_mode_to_string(ClimateMode mode)
Convert the given ClimateMode to a human-readable string.
void upgrade_firmware()
Attempts to check for and apply firmware updates.
The climate device is set to heat to reach the target temperature.
BedJet is in Extended Heat mode (limited to 10 hours)
Enter Heat mode (limited to 4 hours)
BedJet is in "wait" mode, a step during a biorhythm program.
void send_local_time()
Attempts to sync the local time (via time_id) to the BedJet device.
uint8_t ambient_temp_step
Current ambient air temp.
The climate device is set to dry/humidity mode.
The format of a BedJet V3 status packet.
BedjetMode mode
BedJet operating mode.
uint8_t set_notify_(bool enable)
Configures the local ESP BLE client to register (true) or unregister (false) for status notifications...
uint32_t IRAM_ATTR HOT millis()
BedJet is in Dry mode (high speed, no heat)
BedJet is in Cool mode (actually "Fan only" mode)
float bedjet_temp_to_c(const uint8_t temp)
Converts a BedJet temp step into degrees Celsius.
uint8_t minute
minutes after the hour [0-59]
Enter Dry mode (high speed, no heat)
Start the M1 biorhythm/preset program.
HVACMode.HEAT is handled using BTN_EXTHT.
void setup_time_()
Initializes time sync callbacks to support syncing current time to the BedJet.
const optional< std::string > & get_custom_preset() const
const optional< ClimatePreset > & get_preset() const
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.
BedJet is in Heat mode (limited to 4 hours)
void dump_config() override
const LogString * climate_preset_to_string(ClimatePreset preset)
Convert the given PresetMode to a human-readable string.
Start the M3 biorhythm/preset program.
The climate device is actively heating.
uint8_t write_notify_config_descriptor_(bool enable)
Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT.
const optional< std::string > & get_custom_fan_mode() const
The fan mode is set to Off.
const optional< float > & get_target_temperature() const
The fan mode is set to High.
ClimateMode
Enum for all modes a climate device can be in.
The climate device is off.
void reset_state_()
Resets states to defaults.
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.
const optional< ClimateFanMode > & get_fan_mode() const
Device is in boost preset.
BedJet is in Turbo mode (high heat, limited time)
BedjetHeatMode
Optional heating strategies to use for climate::CLIMATE_MODE_HEAT.
void set_clock(uint8_t hour, uint8_t minute)
Attempt to set the BedJet device's clock to the specified time.
uint8_t target_temp_step
Target temp that the BedJet will try to heat to. See actual_temp_step.
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override
The climate device is idle (monitoring climate but no action needed)
void control(const climate::ClimateCall &call) override
The fan mode is set to Medium.
Enter Turbo mode (high heat, limited to 10 minutes)
uint8_t fan_step
BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5...
The climate device only has the fan enabled, no heating or cooling is taking place.
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%.
The climate device is actively cooling.
uint8_t hour
hours since midnight [0-23]
Request a firmware update. This will also restart the Bedjet.