mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Add BedJet BLE climate component (#2452)
This commit is contained in:
		| @@ -28,6 +28,7 @@ esphome/components/atc_mithermometer/* @ahpohl | |||||||
| esphome/components/b_parasite/* @rbaron | esphome/components/b_parasite/* @rbaron | ||||||
| esphome/components/ballu/* @bazuchan | esphome/components/ballu/* @bazuchan | ||||||
| esphome/components/bang_bang/* @OttoWinter | esphome/components/bang_bang/* @OttoWinter | ||||||
|  | esphome/components/bedjet/* @jhansche | ||||||
| esphome/components/bh1750/* @OttoWinter | esphome/components/bh1750/* @OttoWinter | ||||||
| esphome/components/binary_sensor/* @esphome/core | esphome/components/binary_sensor/* @esphome/core | ||||||
| esphome/components/bl0940/* @tobias- | esphome/components/bl0940/* @tobias- | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@jhansche"] | ||||||
							
								
								
									
										642
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										642
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,642 @@ | |||||||
|  | #include "bedjet.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bedjet { | ||||||
|  |  | ||||||
|  | using namespace esphome::climate; | ||||||
|  |  | ||||||
|  | /// Converts a BedJet temp step into degrees Celsius. | ||||||
|  | float bedjet_temp_to_c(const uint8_t temp) { | ||||||
|  |   // BedJet temp is "C*2"; to get C, divide by 2. | ||||||
|  |   return temp / 2.0f; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%. | ||||||
|  | uint8_t bedjet_fan_step_to_speed(const uint8_t fan) { | ||||||
|  |   //  0 =  5% | ||||||
|  |   // 19 = 100% | ||||||
|  |   return 5 * fan + 5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { | ||||||
|  |   if (fan_step >= 0 && fan_step <= 19) | ||||||
|  |     return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; | ||||||
|  |   return nullptr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { | ||||||
|  |   for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) { | ||||||
|  |     if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { | ||||||
|  |       return i; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return -1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Bedjet::upgrade_firmware() { | ||||||
|  |   auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); | ||||||
|  |   auto status = this->write_bedjet_packet_(pkt); | ||||||
|  |  | ||||||
|  |   if (status) { | ||||||
|  |     ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Bedjet::dump_config() { | ||||||
|  |   LOG_CLIMATE("", "BedJet Climate", this); | ||||||
|  |   auto traits = this->get_traits(); | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Supported modes:"); | ||||||
|  |   for (auto mode : traits.get_supported_modes()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_mode_to_string(mode))); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Supported fan modes:"); | ||||||
|  |   for (const auto &mode : traits.get_supported_fan_modes()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); | ||||||
|  |   } | ||||||
|  |   for (const auto &mode : traits.get_supported_custom_fan_modes()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "   - %s (c)", mode.c_str()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Supported presets:"); | ||||||
|  |   for (auto preset : traits.get_supported_presets()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_preset_to_string(preset))); | ||||||
|  |   } | ||||||
|  |   for (const auto &preset : traits.get_supported_custom_presets()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "   - %s (c)", preset.c_str()); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Bedjet::setup() { | ||||||
|  |   this->codec_ = make_unique<BedjetCodec>(); | ||||||
|  |  | ||||||
|  |   // restore set points | ||||||
|  |   auto restore = this->restore_state_(); | ||||||
|  |   if (restore.has_value()) { | ||||||
|  |     ESP_LOGI(TAG, "Restored previous saved state."); | ||||||
|  |     restore->apply(this); | ||||||
|  |   } else { | ||||||
|  |     // Initial status is unknown until we connect | ||||||
|  |     this->reset_state_(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | #ifdef USE_TIME | ||||||
|  |   this->setup_time_(); | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Resets states to defaults. */ | ||||||
|  | void Bedjet::reset_state_() { | ||||||
|  |   this->mode = climate::CLIMATE_MODE_OFF; | ||||||
|  |   this->action = climate::CLIMATE_ACTION_IDLE; | ||||||
|  |   this->target_temperature = NAN; | ||||||
|  |   this->current_temperature = NAN; | ||||||
|  |   this->preset.reset(); | ||||||
|  |   this->custom_preset.reset(); | ||||||
|  |   this->publish_state(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Bedjet::loop() {} | ||||||
|  |  | ||||||
|  | void Bedjet::control(const ClimateCall &call) { | ||||||
|  |   ESP_LOGD(TAG, "Received Bedjet::control"); | ||||||
|  |   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||||
|  |     ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (call.get_mode().has_value()) { | ||||||
|  |     ClimateMode mode = *call.get_mode(); | ||||||
|  |     BedjetPacket *pkt; | ||||||
|  |     switch (mode) { | ||||||
|  |       case climate::CLIMATE_MODE_OFF: | ||||||
|  |         pkt = this->codec_->get_button_request(BTN_OFF); | ||||||
|  |         break; | ||||||
|  |       case climate::CLIMATE_MODE_HEAT: | ||||||
|  |         pkt = this->codec_->get_button_request(BTN_EXTHT); | ||||||
|  |         break; | ||||||
|  |       case climate::CLIMATE_MODE_FAN_ONLY: | ||||||
|  |         pkt = this->codec_->get_button_request(BTN_COOL); | ||||||
|  |         break; | ||||||
|  |       case climate::CLIMATE_MODE_DRY: | ||||||
|  |         pkt = this->codec_->get_button_request(BTN_DRY); | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         ESP_LOGW(TAG, "Unsupported mode: %d", mode); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     auto status = this->write_bedjet_packet_(pkt); | ||||||
|  |  | ||||||
|  |     if (status) { | ||||||
|  |       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||||
|  |     } else { | ||||||
|  |       this->force_refresh_ = true; | ||||||
|  |       this->mode = mode; | ||||||
|  |       // We're using (custom) preset for Turbo & M1-3 presets, so changing climate mode will clear those | ||||||
|  |       this->custom_preset.reset(); | ||||||
|  |       this->preset.reset(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (call.get_target_temperature().has_value()) { | ||||||
|  |     auto target_temp = *call.get_target_temperature(); | ||||||
|  |     auto *pkt = this->codec_->get_set_target_temp_request(target_temp); | ||||||
|  |     auto status = this->write_bedjet_packet_(pkt); | ||||||
|  |  | ||||||
|  |     if (status) { | ||||||
|  |       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||||
|  |     } else { | ||||||
|  |       this->target_temperature = target_temp; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (call.get_preset().has_value()) { | ||||||
|  |     ClimatePreset preset = *call.get_preset(); | ||||||
|  |     BedjetPacket *pkt; | ||||||
|  |  | ||||||
|  |     if (preset == climate::CLIMATE_PRESET_BOOST) { | ||||||
|  |       pkt = this->codec_->get_button_request(BTN_TURBO); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Unsupported preset: %d", preset); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     auto status = this->write_bedjet_packet_(pkt); | ||||||
|  |     if (status) { | ||||||
|  |       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||||
|  |     } else { | ||||||
|  |       // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode. | ||||||
|  |       this->mode = climate::CLIMATE_MODE_HEAT; | ||||||
|  |       this->preset = preset; | ||||||
|  |       this->custom_preset.reset(); | ||||||
|  |       this->force_refresh_ = true; | ||||||
|  |     } | ||||||
|  |   } else if (call.get_custom_preset().has_value()) { | ||||||
|  |     std::string preset = *call.get_custom_preset(); | ||||||
|  |     BedjetPacket *pkt; | ||||||
|  |  | ||||||
|  |     if (preset == "M1") { | ||||||
|  |       pkt = this->codec_->get_button_request(BTN_M1); | ||||||
|  |     } else if (preset == "M2") { | ||||||
|  |       pkt = this->codec_->get_button_request(BTN_M2); | ||||||
|  |     } else if (preset == "M3") { | ||||||
|  |       pkt = this->codec_->get_button_request(BTN_M3); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     auto status = this->write_bedjet_packet_(pkt); | ||||||
|  |     if (status) { | ||||||
|  |       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||||
|  |     } else { | ||||||
|  |       this->force_refresh_ = true; | ||||||
|  |       this->custom_preset = preset; | ||||||
|  |       this->preset.reset(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (call.get_fan_mode().has_value()) { | ||||||
|  |     // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. | ||||||
|  |     // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. | ||||||
|  |     auto fan_mode = *call.get_fan_mode(); | ||||||
|  |     BedjetPacket *pkt; | ||||||
|  |     if (fan_mode == climate::CLIMATE_FAN_LOW) { | ||||||
|  |       pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */); | ||||||
|  |     } else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) { | ||||||
|  |       pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */); | ||||||
|  |     } else if (fan_mode == climate::CLIMATE_FAN_HIGH) { | ||||||
|  |       pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(), | ||||||
|  |                LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     auto status = this->write_bedjet_packet_(pkt); | ||||||
|  |     if (status) { | ||||||
|  |       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||||
|  |     } else { | ||||||
|  |       this->force_refresh_ = true; | ||||||
|  |     } | ||||||
|  |   } else if (call.get_custom_fan_mode().has_value()) { | ||||||
|  |     auto fan_mode = *call.get_custom_fan_mode(); | ||||||
|  |     auto fan_step = bedjet_fan_speed_to_step(fan_mode); | ||||||
|  |     if (fan_step >= 0 && fan_step <= 19) { | ||||||
|  |       ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), | ||||||
|  |                fan_step); | ||||||
|  |       // The index should represent the fan_step index. | ||||||
|  |       BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step); | ||||||
|  |       auto status = this->write_bedjet_packet_(pkt); | ||||||
|  |       if (status) { | ||||||
|  |         ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||||
|  |       } else { | ||||||
|  |         this->force_refresh_ = true; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { | ||||||
|  |   switch (event) { | ||||||
|  |     case ESP_GATTC_DISCONNECT_EVT: { | ||||||
|  |       ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason); | ||||||
|  |       this->status_set_warning(); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||||
|  |       auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID); | ||||||
|  |       if (chr == nullptr) { | ||||||
|  |         ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str()); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       this->char_handle_cmd_ = chr->handle; | ||||||
|  |  | ||||||
|  |       chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID); | ||||||
|  |       if (chr == nullptr) { | ||||||
|  |         ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str()); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this->char_handle_status_ = chr->handle; | ||||||
|  |       // We also need to obtain the config descriptor for this handle. | ||||||
|  |       // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be | ||||||
|  |       // able to look it up. | ||||||
|  |       auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_); | ||||||
|  |       if (descr == nullptr) { | ||||||
|  |         ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications", | ||||||
|  |                  this->char_handle_status_); | ||||||
|  |       } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || | ||||||
|  |                  descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { | ||||||
|  |         ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_, | ||||||
|  |                  descr->uuid.to_string().c_str()); | ||||||
|  |       } else { | ||||||
|  |         this->config_descr_status_ = descr->handle; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID); | ||||||
|  |       if (chr != nullptr) { | ||||||
|  |         this->char_handle_name_ = chr->handle; | ||||||
|  |         auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_, | ||||||
|  |                                               ESP_GATT_AUTH_REQ_NONE); | ||||||
|  |         if (status) { | ||||||
|  |           ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       ESP_LOGD(TAG, "Services complete: obtained char handles."); | ||||||
|  |       this->node_state = espbt::ClientState::ESTABLISHED; | ||||||
|  |  | ||||||
|  |       this->set_notify_(true); | ||||||
|  |  | ||||||
|  | #ifdef USE_TIME | ||||||
|  |       if (this->time_id_.has_value()) { | ||||||
|  |         this->send_local_time_(); | ||||||
|  |       } | ||||||
|  | #endif | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case ESP_GATTC_WRITE_DESCR_EVT: { | ||||||
|  |       if (param->write.status != ESP_GATT_OK) { | ||||||
|  |         // ESP_GATT_INVALID_ATTR_LEN | ||||||
|  |         ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       // [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0 | ||||||
|  |       // This might be the enable-notify descriptor? (or disable-notify) | ||||||
|  |       ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle, | ||||||
|  |                param->write.status); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case ESP_GATTC_WRITE_CHAR_EVT: { | ||||||
|  |       if (param->write.status != ESP_GATT_OK) { | ||||||
|  |         ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       if (param->write.handle == this->char_handle_cmd_) { | ||||||
|  |         if (this->force_refresh_) { | ||||||
|  |           // Command write was successful. Publish the pending state, hoping that notify will kick in. | ||||||
|  |           this->publish_state(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case ESP_GATTC_READ_CHAR_EVT: { | ||||||
|  |       if (param->read.conn_id != this->parent_->conn_id) | ||||||
|  |         break; | ||||||
|  |       if (param->read.status != ESP_GATT_OK) { | ||||||
|  |         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       if (param->read.handle == this->char_handle_status_) { | ||||||
|  |         // This is the additional packet that doesn't fit in the notify packet. | ||||||
|  |         this->codec_->decode_extra(param->read.value, param->read.value_len); | ||||||
|  |       } else if (param->read.handle == this->char_handle_name_) { | ||||||
|  |         // The data should represent the name. | ||||||
|  |         if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) { | ||||||
|  |           std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len); | ||||||
|  |           // this->set_name(bedjet_name); | ||||||
|  |           ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str()); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||||
|  |       // This event means that ESP received the request to enable notifications on the client side. But we also have to | ||||||
|  |       // tell the server that we want it to send notifications. Normally BLEClient parent would handle this | ||||||
|  |       // automatically, but as soon as we set our status to Established, the parent is going to purge all the | ||||||
|  |       // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable | ||||||
|  |       // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write | ||||||
|  |       // doesn't break anything. | ||||||
|  |  | ||||||
|  |       if (param->reg_for_notify.handle != this->char_handle_status_) { | ||||||
|  |         ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", | ||||||
|  |                  this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this->write_notify_config_descriptor_(true); | ||||||
|  |       this->last_notify_ = 0; | ||||||
|  |       this->force_refresh_ = true; | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { | ||||||
|  |       // This event is not handled by the parent BLEClient, so we need to do this either way. | ||||||
|  |       if (param->unreg_for_notify.handle != this->char_handle_status_) { | ||||||
|  |         ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", | ||||||
|  |                  this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this->write_notify_config_descriptor_(false); | ||||||
|  |       this->last_notify_ = 0; | ||||||
|  |       // Now we wait until the next update() poll to re-register notify... | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case ESP_GATTC_NOTIFY_EVT: { | ||||||
|  |       if (param->notify.handle != this->char_handle_status_) { | ||||||
|  |         ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(), | ||||||
|  |                  this->char_handle_status_, param->notify.handle); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we | ||||||
|  |       //  throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds). | ||||||
|  |       //  Another idea would be to keep notify off by default, and use update() as an opportunity to turn on | ||||||
|  |       //  notify to get enough data to update status, then turn off notify again. | ||||||
|  |  | ||||||
|  |       uint32_t now = millis(); | ||||||
|  |       auto delta = now - this->last_notify_; | ||||||
|  |  | ||||||
|  |       if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { | ||||||
|  |         bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); | ||||||
|  |         this->last_notify_ = now; | ||||||
|  |  | ||||||
|  |         if (needs_extra) { | ||||||
|  |           // this means the packet was partial, so read the status characteristic to get the second part. | ||||||
|  |           auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, | ||||||
|  |                                                 this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE); | ||||||
|  |           if (status) { | ||||||
|  |             ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str()); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this->force_refresh_) { | ||||||
|  |           // If we requested an immediate update, do that now. | ||||||
|  |           this->update(); | ||||||
|  |           this->force_refresh_ = false; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event); | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT. | ||||||
|  |  * | ||||||
|  |  * This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order | ||||||
|  |  * to undo the same on unregister. It also allows us to maintain the config descriptor separately, | ||||||
|  |  * since the parent BLEClient is going to purge all descriptors once we set our connection status | ||||||
|  |  * to `Established`. | ||||||
|  |  */ | ||||||
|  | uint8_t Bedjet::write_notify_config_descriptor_(bool enable) { | ||||||
|  |   auto handle = this->config_descr_status_; | ||||||
|  |   if (handle == 0) { | ||||||
|  |     ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_); | ||||||
|  |     return -1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits. | ||||||
|  |   uint8_t notify_en[] = {0, 0}; | ||||||
|  |   notify_en[0] = enable; | ||||||
|  |   auto status = | ||||||
|  |       esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en), | ||||||
|  |                                      ¬ify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||||
|  |   if (status) { | ||||||
|  |     ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); | ||||||
|  |     return status; | ||||||
|  |   } | ||||||
|  |   ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false", | ||||||
|  |            handle); | ||||||
|  |   return ESP_GATT_OK; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #ifdef USE_TIME | ||||||
|  | /** Attempts to sync the local time (via `time_id`) to the BedJet device. */ | ||||||
|  | void Bedjet::send_local_time_() { | ||||||
|  |   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||||
|  |     ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str()); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   auto *time_id = *this->time_id_; | ||||||
|  |   time::ESPTime now = time_id->now(); | ||||||
|  |   if (now.is_valid()) { | ||||||
|  |     uint8_t hour = now.hour; | ||||||
|  |     uint8_t minute = now.minute; | ||||||
|  |     BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute); | ||||||
|  |     auto status = this->write_bedjet_packet_(pkt); | ||||||
|  |     if (status) { | ||||||
|  |       ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Initializes time sync callbacks to support syncing current time to the BedJet. */ | ||||||
|  | void Bedjet::setup_time_() { | ||||||
|  |   if (this->time_id_.has_value()) { | ||||||
|  |     this->send_local_time_(); | ||||||
|  |     auto *time_id = *this->time_id_; | ||||||
|  |     time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); | ||||||
|  |     time::ESPTime now = time_id->now(); | ||||||
|  |     ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | /** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */ | ||||||
|  | uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) { | ||||||
|  |   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||||
|  |     if (!this->parent_->enabled) { | ||||||
|  |       ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str()); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str()); | ||||||
|  |     } | ||||||
|  |     return -1; | ||||||
|  |   } | ||||||
|  |   auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_, | ||||||
|  |                                          pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP, | ||||||
|  |                                          ESP_GATT_AUTH_REQ_NONE); | ||||||
|  |   return status; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */ | ||||||
|  | uint8_t Bedjet::set_notify_(const bool enable) { | ||||||
|  |   uint8_t status; | ||||||
|  |   if (enable) { | ||||||
|  |     status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, | ||||||
|  |                                                this->char_handle_status_); | ||||||
|  |     if (status) { | ||||||
|  |       ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, | ||||||
|  |                                                  this->char_handle_status_); | ||||||
|  |     if (status) { | ||||||
|  |       ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status); | ||||||
|  |   return status; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Attempts to update the climate device from the last received BedjetStatusPacket. | ||||||
|  |  * | ||||||
|  |  * @return `true` if the status has been applied; `false` if there is nothing to apply. | ||||||
|  |  */ | ||||||
|  | bool Bedjet::update_status_() { | ||||||
|  |   if (!this->codec_->has_status()) | ||||||
|  |     return false; | ||||||
|  |  | ||||||
|  |   BedjetStatusPacket status = *this->codec_->get_status_packet(); | ||||||
|  |  | ||||||
|  |   auto converted_temp = bedjet_temp_to_c(status.target_temp_step); | ||||||
|  |   if (converted_temp > 0) | ||||||
|  |     this->target_temperature = converted_temp; | ||||||
|  |   converted_temp = bedjet_temp_to_c(status.ambient_temp_step); | ||||||
|  |   if (converted_temp > 0) | ||||||
|  |     this->current_temperature = converted_temp; | ||||||
|  |  | ||||||
|  |   const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step); | ||||||
|  |   if (fan_mode_name != nullptr) { | ||||||
|  |     this->custom_fan_mode = *fan_mode_name; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. | ||||||
|  |   switch (status.mode) { | ||||||
|  |     case MODE_WAIT:  // Biorhythm "wait" step: device is idle | ||||||
|  |     case MODE_STANDBY: | ||||||
|  |       this->mode = climate::CLIMATE_MODE_OFF; | ||||||
|  |       this->action = climate::CLIMATE_ACTION_IDLE; | ||||||
|  |       this->fan_mode = climate::CLIMATE_FAN_OFF; | ||||||
|  |       this->custom_preset.reset(); | ||||||
|  |       this->preset.reset(); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     case MODE_HEAT: | ||||||
|  |     case MODE_EXTHT: | ||||||
|  |       this->mode = climate::CLIMATE_MODE_HEAT; | ||||||
|  |       this->action = climate::CLIMATE_ACTION_HEATING; | ||||||
|  |       this->custom_preset.reset(); | ||||||
|  |       this->preset.reset(); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     case MODE_COOL: | ||||||
|  |       this->mode = climate::CLIMATE_MODE_FAN_ONLY; | ||||||
|  |       this->action = climate::CLIMATE_ACTION_COOLING; | ||||||
|  |       this->custom_preset.reset(); | ||||||
|  |       this->preset.reset(); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     case MODE_DRY: | ||||||
|  |       this->mode = climate::CLIMATE_MODE_DRY; | ||||||
|  |       this->action = climate::CLIMATE_ACTION_DRYING; | ||||||
|  |       this->custom_preset.reset(); | ||||||
|  |       this->preset.reset(); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     case MODE_TURBO: | ||||||
|  |       this->preset = climate::CLIMATE_PRESET_BOOST; | ||||||
|  |       this->custom_preset.reset(); | ||||||
|  |       this->mode = climate::CLIMATE_MODE_HEAT; | ||||||
|  |       this->action = climate::CLIMATE_ACTION_HEATING; | ||||||
|  |       break; | ||||||
|  |  | ||||||
|  |     default: | ||||||
|  |       ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode); | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (this->is_valid_()) { | ||||||
|  |     this->publish_state(); | ||||||
|  |     this->codec_->clear_status(); | ||||||
|  |     this->status_clear_warning(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Bedjet::update() { | ||||||
|  |   ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str()); | ||||||
|  |  | ||||||
|  |   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||||
|  |     if (!this->parent()->enabled) { | ||||||
|  |       ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str()); | ||||||
|  |     } else { | ||||||
|  |       // Possibly still trying to connect. | ||||||
|  |       ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   auto result = this->update_status_(); | ||||||
|  |   if (!result) { | ||||||
|  |     uint32_t now = millis(); | ||||||
|  |     uint32_t diff = now - this->last_notify_; | ||||||
|  |  | ||||||
|  |     if (this->last_notify_ == 0) { | ||||||
|  |       // This means we're connected and haven't received a notification, so it likely means that the BedJet is off. | ||||||
|  |       // However, it could also mean that it's running, but failing to send notifications. | ||||||
|  |       // We can try to unregister for notifications now, and then re-register, hoping to clear it up... | ||||||
|  |       // But how do we know for sure which state we're in, and how do we actually clear out the buggy state? | ||||||
|  |  | ||||||
|  |       ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); | ||||||
|  |       this->set_notify_(false); | ||||||
|  |     } else if (diff > NOTIFY_WARN_THRESHOLD) { | ||||||
|  |       ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { | ||||||
|  |       ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); | ||||||
|  |       this->parent()->set_enabled(false); | ||||||
|  |       this->parent()->set_enabled(true); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace bedjet | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
							
								
								
									
										121
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/ble_client/ble_client.h" | ||||||
|  | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||||
|  | #include "esphome/components/climate/climate.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "bedjet_base.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_TIME | ||||||
|  | #include "esphome/components/time/real_time_clock.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
|  | #include <esp_gattc_api.h> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bedjet { | ||||||
|  |  | ||||||
|  | namespace espbt = esphome::esp32_ble_tracker; | ||||||
|  |  | ||||||
|  | static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574"); | ||||||
|  | static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574"); | ||||||
|  | static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574"); | ||||||
|  | static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574"); | ||||||
|  |  | ||||||
|  | class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void loop() override; | ||||||
|  |   void update() override; | ||||||
|  |   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||||
|  |                            esp_ble_gattc_cb_param_t *param) override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } | ||||||
|  |  | ||||||
|  | #ifdef USE_TIME | ||||||
|  |   void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } | ||||||
|  | #endif | ||||||
|  |   void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } | ||||||
|  |  | ||||||
|  |   /** Attempts to check for and apply firmware updates. */ | ||||||
|  |   void upgrade_firmware(); | ||||||
|  |  | ||||||
|  |   climate::ClimateTraits traits() override { | ||||||
|  |     auto traits = climate::ClimateTraits(); | ||||||
|  |     traits.set_supports_action(true); | ||||||
|  |     traits.set_supports_current_temperature(true); | ||||||
|  |     traits.set_supported_modes({ | ||||||
|  |         climate::CLIMATE_MODE_OFF, | ||||||
|  |         climate::CLIMATE_MODE_HEAT, | ||||||
|  |         // climate::CLIMATE_MODE_TURBO // Not supported by Climate: see presets instead | ||||||
|  |         climate::CLIMATE_MODE_FAN_ONLY, | ||||||
|  |         climate::CLIMATE_MODE_DRY, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // It would be better if we had a slider for the fan modes. | ||||||
|  |     traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); | ||||||
|  |     traits.set_supported_presets({ | ||||||
|  |         // If we support NONE, then have to decide what happens if the user switches to it (turn off?) | ||||||
|  |         // climate::CLIMATE_PRESET_NONE, | ||||||
|  |         // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. | ||||||
|  |         climate::CLIMATE_PRESET_BOOST, | ||||||
|  |     }); | ||||||
|  |     traits.set_supported_custom_presets({ | ||||||
|  |         // We could fetch biodata from bedjet and set these names that way. | ||||||
|  |         // But then we have to invert the lookup in order to send the right preset. | ||||||
|  |         // For now, we can leave them as M1-3 to match the remote buttons. | ||||||
|  |         "M1", | ||||||
|  |         "M2", | ||||||
|  |         "M3", | ||||||
|  |     }); | ||||||
|  |     traits.set_visual_min_temperature(19.0); | ||||||
|  |     traits.set_visual_max_temperature(43.0); | ||||||
|  |     traits.set_visual_temperature_step(1.0); | ||||||
|  |     return traits; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void control(const climate::ClimateCall &call) override; | ||||||
|  |  | ||||||
|  | #ifdef USE_TIME | ||||||
|  |   void setup_time_(); | ||||||
|  |   void send_local_time_(); | ||||||
|  |   optional<time::RealTimeClock *> time_id_{}; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; | ||||||
|  |  | ||||||
|  |   static const uint32_t MIN_NOTIFY_THROTTLE = 5000; | ||||||
|  |   static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; | ||||||
|  |   static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000; | ||||||
|  |  | ||||||
|  |   uint8_t set_notify_(bool enable); | ||||||
|  |   uint8_t write_bedjet_packet_(BedjetPacket *pkt); | ||||||
|  |   void reset_state_(); | ||||||
|  |   bool update_status_(); | ||||||
|  |  | ||||||
|  |   bool is_valid_() { | ||||||
|  |     // FIXME: find a better way to check this? | ||||||
|  |     return !std::isnan(this->current_temperature) && !std::isnan(this->target_temperature) && | ||||||
|  |            this->current_temperature > 1 && this->target_temperature > 1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint32_t last_notify_ = 0; | ||||||
|  |   bool force_refresh_ = false; | ||||||
|  |  | ||||||
|  |   std::unique_ptr<BedjetCodec> codec_; | ||||||
|  |   uint16_t char_handle_cmd_; | ||||||
|  |   uint16_t char_handle_name_; | ||||||
|  |   uint16_t char_handle_status_; | ||||||
|  |   uint16_t config_descr_status_; | ||||||
|  |  | ||||||
|  |   uint8_t write_notify_config_descriptor_(bool enable); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace bedjet | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
							
								
								
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | |||||||
|  | #include "bedjet_base.h" | ||||||
|  | #include <cstdio> | ||||||
|  | #include <cstring> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bedjet { | ||||||
|  |  | ||||||
|  | /// Converts a BedJet temp step into degrees Fahrenheit. | ||||||
|  | float bedjet_temp_to_f(const uint8_t temp) { | ||||||
|  |   // BedJet temp is "C*2"; to get F, multiply by 0.9 (half 1.8) and add 32. | ||||||
|  |   return 0.9f * temp + 32.0f; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Cleans up the packet before sending. */ | ||||||
|  | BedjetPacket *BedjetCodec::clean_packet_() { | ||||||
|  |   // So far no commands require more than 2 bytes of data. | ||||||
|  |   assert(this->packet_.data_length <= 2); | ||||||
|  |   for (int i = this->packet_.data_length; i < 2; i++) { | ||||||
|  |     this->packet_.data[i] = '\0'; | ||||||
|  |   } | ||||||
|  |   ESP_LOGV(TAG, "Created packet: %02X, %02X %02X", this->packet_.command, this->packet_.data[0], this->packet_.data[1]); | ||||||
|  |   return &this->packet_; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Returns a BedjetPacket that will initiate a BedjetButton press. */ | ||||||
|  | BedjetPacket *BedjetCodec::get_button_request(BedjetButton button) { | ||||||
|  |   this->packet_.command = CMD_BUTTON; | ||||||
|  |   this->packet_.data_length = 1; | ||||||
|  |   this->packet_.data[0] = button; | ||||||
|  |   return this->clean_packet_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Returns a BedjetPacket that will set the device's target `temperature`. */ | ||||||
|  | BedjetPacket *BedjetCodec::get_set_target_temp_request(float temperature) { | ||||||
|  |   this->packet_.command = CMD_SET_TEMP; | ||||||
|  |   this->packet_.data_length = 1; | ||||||
|  |   this->packet_.data[0] = temperature * 2; | ||||||
|  |   return this->clean_packet_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Returns a BedjetPacket that will set the device's target fan speed. */ | ||||||
|  | BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) { | ||||||
|  |   this->packet_.command = CMD_SET_FAN; | ||||||
|  |   this->packet_.data_length = 1; | ||||||
|  |   this->packet_.data[0] = fan_step; | ||||||
|  |   return this->clean_packet_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Returns a BedjetPacket that will set the device's current time. */ | ||||||
|  | BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_t minute) { | ||||||
|  |   this->packet_.command = CMD_SET_TIME; | ||||||
|  |   this->packet_.data_length = 2; | ||||||
|  |   this->packet_.data[0] = hour; | ||||||
|  |   this->packet_.data[1] = minute; | ||||||
|  |   return this->clean_packet_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Decodes the extra bytes that were received after being notified with a partial packet. */ | ||||||
|  | void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { | ||||||
|  |   ESP_LOGV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); | ||||||
|  |   uint8_t offset = this->last_buffer_size_; | ||||||
|  |   if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { | ||||||
|  |     memcpy(((uint8_t *) (&this->buf_)) + offset, data, length); | ||||||
|  |     ESP_LOGV(TAG, | ||||||
|  |              "Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, " | ||||||
|  |              "flags=BedjetFlags <conn=%c, leds=%c, units=%c, mute=%c, others=%02x>", | ||||||
|  |              this->buf_._skip_1_, this->buf_._skip_2_, this->buf_._skip_3_, this->buf_.update_phase, | ||||||
|  |              this->buf_.flags & 0x20 ? '1' : '0', this->buf_.flags & 0x10 ? '1' : '0', | ||||||
|  |              this->buf_.flags & 0x04 ? '1' : '0', this->buf_.flags & 0x01 ? '1' : '0', | ||||||
|  |              this->buf_.flags & ~(0x20 | 0x10 | 0x04 | 0x01)); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGI(TAG, "Could not determine where to append to, last offset=%d, max size=%u, new size would be %d", offset, | ||||||
|  |              sizeof(BedjetStatusPacket), length + offset); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Decodes the incoming status packet received on the BEDJET_STATUS_UUID. | ||||||
|  |  * | ||||||
|  |  * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise. | ||||||
|  |  */ | ||||||
|  | bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { | ||||||
|  |   ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); | ||||||
|  |  | ||||||
|  |   if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { | ||||||
|  |     this->status_packet_.reset(); | ||||||
|  |  | ||||||
|  |     // Clear old buffer | ||||||
|  |     memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); | ||||||
|  |     // Copy new data into buffer | ||||||
|  |     memcpy(&this->buf_, data, length); | ||||||
|  |     this->last_buffer_size_ = length; | ||||||
|  |  | ||||||
|  |     // TODO: validate the packet checksum? | ||||||
|  |     if (this->buf_.mode >= 0 && this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && | ||||||
|  |         this->buf_.target_temp_step <= 86 && this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 && | ||||||
|  |         this->buf_.ambient_temp_step > 1 && this->buf_.ambient_temp_step <= 100) { | ||||||
|  |       // and save it for the update() loop | ||||||
|  |       this->status_packet_ = this->buf_; | ||||||
|  |       return this->buf_.is_partial == 1; | ||||||
|  |     } else { | ||||||
|  |       // TODO: log a warning if we detect that we connected to a non-V3 device. | ||||||
|  |       ESP_LOGW(TAG, "Received potentially invalid packet (len %d):", length); | ||||||
|  |     } | ||||||
|  |   } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { | ||||||
|  |     // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself. | ||||||
|  |     ESP_LOGV(TAG, | ||||||
|  |              "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF;  [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " | ||||||
|  |              "[12]=%d, [-1]=%d", | ||||||
|  |              bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], data[9], | ||||||
|  |              data[10], data[11], data[12], data[length - 1]); | ||||||
|  |  | ||||||
|  |     if (this->has_status()) { | ||||||
|  |       this->status_packet_->ambient_temp_step = data[6]; | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     // TODO: log a warning if we detect that we connected to a non-V3 device. | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace bedjet | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | #include "bedjet_const.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bedjet { | ||||||
|  |  | ||||||
|  | struct BedjetPacket { | ||||||
|  |   uint8_t data_length; | ||||||
|  |   BedjetCommand command; | ||||||
|  |   uint8_t data[2]; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | struct BedjetFlags { | ||||||
|  |   /* uint8_t */ | ||||||
|  |   int a_ : 1;                // 0x80 | ||||||
|  |   int b_ : 1;                // 0x40 | ||||||
|  |   int conn_test_passed : 1;  ///< (0x20) Bit is set `1` if the last connection test passed. | ||||||
|  |   int leds_enabled : 1;      ///< (0x10) Bit is set `1` if the LEDs on the device are enabled. | ||||||
|  |   int c_ : 1;                // 0x08 | ||||||
|  |   int units_setup : 1;       ///< (0x04) Bit is set `1` if the device's units have been configured. | ||||||
|  |   int d_ : 1;                // 0x02 | ||||||
|  |   int beeps_muted : 1;       ///< (0x01) Bit is set `1` if the device's sound output is muted. | ||||||
|  | } __attribute__((packed)); | ||||||
|  |  | ||||||
|  | enum BedjetPacketFormat : uint8_t { | ||||||
|  |   PACKET_FORMAT_DEBUG = 0x05,    //  5 | ||||||
|  |   PACKET_FORMAT_V3_HOME = 0x56,  // 86 | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum BedjetPacketType : uint8_t { | ||||||
|  |   PACKET_TYPE_STATUS = 0x1, | ||||||
|  |   PACKET_TYPE_DEBUG = 0x2, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** The format of a BedJet V3 status packet. */ | ||||||
|  | struct BedjetStatusPacket { | ||||||
|  |   // [0] | ||||||
|  |   uint8_t is_partial : 8;  ///< `1` indicates that this is a partial packet, and more data can be read directly from the | ||||||
|  |                            ///< characteristic. | ||||||
|  |   BedjetPacketFormat packet_format : 8;  ///< BedjetPacketFormat::PACKET_FORMAT_V3_HOME for BedJet V3 status packet | ||||||
|  |                                          ///< format. BedjetPacketFormat::PACKET_FORMAT_DEBUG for debugging packets. | ||||||
|  |   uint8_t | ||||||
|  |       expecting_length : 8;  ///< The expected total length of the status packet after merging the additional packet. | ||||||
|  |   BedjetPacketType packet_type : 8;  ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet. | ||||||
|  |  | ||||||
|  |   // [4] | ||||||
|  |   uint8_t time_remaining_hrs : 8;   ///< Hours remaining in program runtime | ||||||
|  |   uint8_t time_remaining_mins : 8;  ///< Minutes remaining in program runtime | ||||||
|  |   uint8_t time_remaining_secs : 8;  ///< Seconds remaining in program runtime | ||||||
|  |  | ||||||
|  |   // [7] | ||||||
|  |   uint8_t actual_temp_step : 8;  ///< Actual temp of the air blown by the BedJet fan; value represents `2 * | ||||||
|  |                                  ///< degrees_celsius`. See #bedjet_temp_to_c and #bedjet_temp_to_f | ||||||
|  |   uint8_t target_temp_step : 8;  ///< Target temp that the BedJet will try to heat to. See #actual_temp_step. | ||||||
|  |  | ||||||
|  |   // [9] | ||||||
|  |   BedjetMode mode : 8;  ///< BedJet operating mode. | ||||||
|  |  | ||||||
|  |   // [10] | ||||||
|  |   uint8_t fan_step : 8;  ///< BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5 | ||||||
|  |                          ///< * fan_step` | ||||||
|  |   uint8_t max_hrs : 8;   ///< Max hours of mode runtime | ||||||
|  |   uint8_t max_mins : 8;  ///< Max minutes of mode runtime | ||||||
|  |   uint8_t min_temp_step : 8;  ///< Min temp allowed in mode. See #actual_temp_step. | ||||||
|  |   uint8_t max_temp_step : 8;  ///< Max temp allowed in mode. See #actual_temp_step. | ||||||
|  |  | ||||||
|  |   // [15-16] | ||||||
|  |   uint16_t turbo_time : 16;  ///< Time remaining in BedjetMode::MODE_TURBO. | ||||||
|  |  | ||||||
|  |   // [17] | ||||||
|  |   uint8_t ambient_temp_step : 8;  ///< Current ambient air temp. This is the coldest air the BedJet can blow. See | ||||||
|  |                                   ///< #actual_temp_step. | ||||||
|  |   uint8_t shutdown_reason : 8;    ///< The reason for the last device shutdown. | ||||||
|  |  | ||||||
|  |   // [19-25]; the initial partial packet cuts off here after [19] | ||||||
|  |   // Skip 7 bytes? | ||||||
|  |   uint32_t _skip_1_ : 32;  // Unknown 19-22 = 0x01810112 | ||||||
|  |  | ||||||
|  |   uint16_t _skip_2_ : 16;  // Unknown 23-24 = 0x1310 | ||||||
|  |   uint8_t _skip_3_ : 8;    // Unknown 25 = 0x00 | ||||||
|  |  | ||||||
|  |   // [26] | ||||||
|  |   //   0x18(24) = "Connection test has completed OK" | ||||||
|  |   //   0x1a(26) = "Firmware update is not needed" | ||||||
|  |   uint8_t update_phase : 8;  ///< The current status/phase of a firmware update. | ||||||
|  |  | ||||||
|  |   // [27] | ||||||
|  |   // FIXME: cannot nest packed struct of matching length here? | ||||||
|  |   /* BedjetFlags */ uint8_t flags : 8;  /// See BedjetFlags for the packed byte flags. | ||||||
|  |   // [28-31]; 20+11 bytes | ||||||
|  |   uint32_t _skip_4_ : 32;  // Unknown | ||||||
|  |  | ||||||
|  | } __attribute__((packed)); | ||||||
|  |  | ||||||
|  | /** This class is responsible for encoding command packets and decoding status packets. | ||||||
|  |  * | ||||||
|  |  * Status Packets | ||||||
|  |  * ============== | ||||||
|  |  * The BedJet protocol depends on registering for notifications on the esphome::BedJet::BEDJET_SERVICE_UUID | ||||||
|  |  * characteristic. If the BedJet is on, it will send rapid updates as notifications. If it is off, | ||||||
|  |  * it generally will not notify of any status. | ||||||
|  |  * | ||||||
|  |  * As the BedJet V3's BedjetStatusPacket exceeds the buffer size allowed for BLE notification packets, | ||||||
|  |  * the notification packet will contain `BedjetStatusPacket::is_partial == 1`. When that happens, an additional | ||||||
|  |  * read of the esphome::BedJet::BEDJET_SERVICE_UUID characteristic will contain the second portion of the | ||||||
|  |  * full status packet. | ||||||
|  |  * | ||||||
|  |  * Command Packets | ||||||
|  |  * =============== | ||||||
|  |  * This class supports encoding a number of BedjetPacket commands: | ||||||
|  |  * - Button press | ||||||
|  |  *   This simulates a press of one of the BedjetButton values. | ||||||
|  |  *   - BedjetPacket#command = BedjetCommand::CMD_BUTTON | ||||||
|  |  *   - BedjetPacket#data [0] contains the BedjetButton value | ||||||
|  |  * - Set target temp | ||||||
|  |  *   This sets the BedJet's target temp to a concrete temperature value. | ||||||
|  |  *   - BedjetPacket#command = BedjetCommand::CMD_SET_TEMP | ||||||
|  |  *   - BedjetPacket#data [0] contains the BedJet temp value; see BedjetStatusPacket#actual_temp_step | ||||||
|  |  * - Set fan speed | ||||||
|  |  *   This sets the BedJet fan speed. | ||||||
|  |  *   - BedjetPacket#command = BedjetCommand::CMD_SET_FAN | ||||||
|  |  *   - BedjetPacket#data [0] contains the BedJet fan step in the range 0-19. | ||||||
|  |  * - Set current time | ||||||
|  |  *   The BedJet needs to have its clock set properly in order to run the biorhythm programs, which might | ||||||
|  |  *   contain time-of-day based step rules. | ||||||
|  |  *   - BedjetPacket#command = BedjetCommand::CMD_SET_TIME | ||||||
|  |  *   - BedjetPacket#data [0] is hours, [1] is minutes | ||||||
|  |  */ | ||||||
|  | class BedjetCodec { | ||||||
|  |  public: | ||||||
|  |   BedjetPacket *get_button_request(BedjetButton button); | ||||||
|  |   BedjetPacket *get_set_target_temp_request(float temperature); | ||||||
|  |   BedjetPacket *get_set_fan_speed_request(uint8_t fan_step); | ||||||
|  |   BedjetPacket *get_set_time_request(uint8_t hour, uint8_t minute); | ||||||
|  |  | ||||||
|  |   bool decode_notify(const uint8_t *data, uint16_t length); | ||||||
|  |   void decode_extra(const uint8_t *data, uint16_t length); | ||||||
|  |  | ||||||
|  |   inline bool has_status() { return this->status_packet_.has_value(); } | ||||||
|  |   const optional<BedjetStatusPacket> &get_status_packet() const { return this->status_packet_; } | ||||||
|  |   void clear_status() { this->status_packet_.reset(); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   BedjetPacket *clean_packet_(); | ||||||
|  |  | ||||||
|  |   uint8_t last_buffer_size_ = 0; | ||||||
|  |  | ||||||
|  |   BedjetPacket packet_; | ||||||
|  |  | ||||||
|  |   optional<BedjetStatusPacket> status_packet_; | ||||||
|  |   BedjetStatusPacket buf_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace bedjet | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										78
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <set> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bedjet { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "bedjet"; | ||||||
|  |  | ||||||
|  | enum BedjetMode : uint8_t { | ||||||
|  |   /// BedJet is Off | ||||||
|  |   MODE_STANDBY = 0, | ||||||
|  |   /// BedJet is in Heat mode (limited to 4 hours) | ||||||
|  |   MODE_HEAT = 1, | ||||||
|  |   /// BedJet is in Turbo mode (high heat, limited time) | ||||||
|  |   MODE_TURBO = 2, | ||||||
|  |   /// BedJet is in Extended Heat mode (limited to 10 hours) | ||||||
|  |   MODE_EXTHT = 3, | ||||||
|  |   /// BedJet is in Cool mode (actually "Fan only" mode) | ||||||
|  |   MODE_COOL = 4, | ||||||
|  |   /// BedJet is in Dry mode (high speed, no heat) | ||||||
|  |   MODE_DRY = 5, | ||||||
|  |   /// BedJet is in "wait" mode, a step during a biorhythm program | ||||||
|  |   MODE_WAIT = 6, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum BedjetButton : uint8_t { | ||||||
|  |   /// Turn BedJet off | ||||||
|  |   BTN_OFF = 0x1, | ||||||
|  |   /// Enter Cool mode (fan only) | ||||||
|  |   BTN_COOL = 0x2, | ||||||
|  |   /// Enter Heat mode (limited to 4 hours) | ||||||
|  |   BTN_HEAT = 0x3, | ||||||
|  |   /// Enter Turbo mode (high heat, limited to 10 minutes) | ||||||
|  |   BTN_TURBO = 0x4, | ||||||
|  |   /// Enter Dry mode (high speed, no heat) | ||||||
|  |   BTN_DRY = 0x5, | ||||||
|  |   /// Enter Extended Heat mode (limited to 10 hours) | ||||||
|  |   BTN_EXTHT = 0x6, | ||||||
|  |  | ||||||
|  |   /// Start the M1 biorhythm/preset program | ||||||
|  |   BTN_M1 = 0x20, | ||||||
|  |   /// Start the M2 biorhythm/preset program | ||||||
|  |   BTN_M2 = 0x21, | ||||||
|  |   /// Start the M3 biorhythm/preset program | ||||||
|  |   BTN_M3 = 0x22, | ||||||
|  |  | ||||||
|  |   /* These are "MAGIC" buttons */ | ||||||
|  |  | ||||||
|  |   /// Turn debug mode on/off | ||||||
|  |   MAGIC_DEBUG_ON = 0x40, | ||||||
|  |   MAGIC_DEBUG_OFF = 0x41, | ||||||
|  |   /// Perform a connection test. | ||||||
|  |   MAGIC_CONNTEST = 0x42, | ||||||
|  |   /// Request a firmware update. This will also restart the Bedjet. | ||||||
|  |   MAGIC_UPDATE = 0x43, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum BedjetCommand : uint8_t { | ||||||
|  |   CMD_BUTTON = 0x1, | ||||||
|  |   CMD_SET_TEMP = 0x3, | ||||||
|  |   CMD_STATUS = 0x6, | ||||||
|  |   CMD_SET_FAN = 0x7, | ||||||
|  |   CMD_SET_TIME = 0x8, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #define BEDJET_FAN_STEP_NAMES_ \ | ||||||
|  |   { \ | ||||||
|  |     "  5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \ | ||||||
|  |         " 75%", " 80%", " 85%", " 90%", " 95%", "100%" \ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; | ||||||
|  | static const std::string BEDJET_FAN_STEP_NAME_STRINGS[20] = BEDJET_FAN_STEP_NAMES_; | ||||||
|  | static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; | ||||||
|  |  | ||||||
|  | }  // namespace bedjet | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										42
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import climate, ble_client, time | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_RECEIVE_TIMEOUT, | ||||||
|  |     CONF_TIME_ID, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@jhansche"] | ||||||
|  | DEPENDENCIES = ["ble_client"] | ||||||
|  |  | ||||||
|  | bedjet_ns = cg.esphome_ns.namespace("bedjet") | ||||||
|  | Bedjet = bedjet_ns.class_( | ||||||
|  |     "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     climate.CLIMATE_SCHEMA.extend( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(Bedjet), | ||||||
|  |             cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), | ||||||
|  |             cv.Optional( | ||||||
|  |                 CONF_RECEIVE_TIMEOUT, default="0s" | ||||||
|  |             ): cv.positive_time_period_milliseconds, | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||||
|  |     .extend(cv.polling_component_schema("30s")) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await climate.register_climate(var, config) | ||||||
|  |     await ble_client.register_ble_node(var, config) | ||||||
|  |     if CONF_TIME_ID in config: | ||||||
|  |         time_ = await cg.get_variable(config[CONF_TIME_ID]) | ||||||
|  |         cg.add(var.set_time_id(time_)) | ||||||
|  |     if CONF_RECEIVE_TIMEOUT in config: | ||||||
|  |         cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) | ||||||
| @@ -291,6 +291,8 @@ ble_client: | |||||||
|     on_disconnect: |     on_disconnect: | ||||||
|       then: |       then: | ||||||
|         - switch.turn_on: ble1_status |         - switch.turn_on: ble1_status | ||||||
|  |   - mac_address: C4:4F:33:11:22:33 | ||||||
|  |     id: my_bedjet_ble_client | ||||||
| mcp23s08: | mcp23s08: | ||||||
|   - id: "mcp23s08_hub" |   - id: "mcp23s08_hub" | ||||||
|     cs_pin: GPIO12 |     cs_pin: GPIO12 | ||||||
| @@ -1870,6 +1872,9 @@ climate: | |||||||
|     ble_client_id: ble_blah |     ble_client_id: ble_blah | ||||||
|     unit_of_measurement: c |     unit_of_measurement: c | ||||||
|     icon: mdi:stove |     icon: mdi:stove | ||||||
|  |   - platform: bedjet | ||||||
|  |     name: My Bedjet | ||||||
|  |     ble_client_id: my_bedjet_ble_client | ||||||
|  |  | ||||||
| script: | script: | ||||||
|   - id: climate_custom |   - id: climate_custom | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user