diff --git a/CODEOWNERS b/CODEOWNERS index fe6e712d45..ae112b3330 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -29,6 +29,8 @@ esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter esphome/components/bedjet/* @jhansche +esphome/components/bedjet/climate/* @jhansche +esphome/components/bedjet/fan/* @jhansche esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/bl0939/* @ziceva diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index bd2fb2421d..27a75b2671 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -89,8 +89,10 @@ enum BedjetCommand : uint8_t { "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 uint8_t BEDJET_FAN_SPEED_COUNT = 20; + +static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; +static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static const std::set BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; } // namespace bedjet diff --git a/esphome/components/bedjet/climate.py b/esphome/components/bedjet/climate/__init__.py similarity index 89% rename from esphome/components/bedjet/climate.py rename to esphome/components/bedjet/climate/__init__.py index 9865cd716d..b12622868f 100644 --- a/esphome/components/bedjet/climate.py +++ b/esphome/components/bedjet/climate/__init__.py @@ -9,19 +9,17 @@ from esphome.const import ( CONF_RECEIVE_TIMEOUT, CONF_TIME_ID, ) -from . import ( +from .. import ( BEDJET_CLIENT_SCHEMA, + bedjet_ns, register_bedjet_child, ) _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jhansche"] -DEPENDENCIES = ["ble_client"] +DEPENDENCIES = ["bedjet"] -bedjet_ns = cg.esphome_ns.namespace("bedjet") -BedJetClimate = bedjet_ns.class_( - "BedJetClimate", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent -) +BedJetClimate = bedjet_ns.class_("BedJetClimate", climate.Climate, cg.PollingComponent) BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode") BEDJET_HEAT_MODES = { "heat": BedjetHeatMode.HEAT_MODE_HEAT, diff --git a/esphome/components/bedjet/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp similarity index 99% rename from esphome/components/bedjet/bedjet_climate.cpp rename to esphome/components/bedjet/climate/bedjet_climate.cpp index 8d9fdd7318..431cf614e9 100644 --- a/esphome/components/bedjet/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -15,13 +15,13 @@ float bedjet_temp_to_c(const uint8_t temp) { } static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { - if (fan_step <= 19) + if (fan_step < BEDJET_FAN_SPEED_COUNT) 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++) { + for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) { if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { return i; } diff --git a/esphome/components/bedjet/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h similarity index 95% rename from esphome/components/bedjet/bedjet_climate.h rename to esphome/components/bedjet/climate/bedjet_climate.h index 27ee5c7501..48c50d842f 100644 --- a/esphome/components/bedjet/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -1,12 +1,12 @@ #pragma once -#include "esphome/components/climate/climate.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "bedjet_child.h" -#include "bedjet_codec.h" -#include "bedjet_hub.h" +#include "esphome/components/bedjet/bedjet_child.h" +#include "esphome/components/bedjet/bedjet_codec.h" +#include "esphome/components/bedjet/bedjet_hub.h" +#include "esphome/components/climate/climate.h" #ifdef USE_ESP32 diff --git a/esphome/components/bedjet/fan/__init__.py b/esphome/components/bedjet/fan/__init__.py new file mode 100644 index 0000000000..06e81ea979 --- /dev/null +++ b/esphome/components/bedjet/fan/__init__.py @@ -0,0 +1,36 @@ +import logging + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import fan +from esphome.const import ( + CONF_ID, +) +from .. import ( + BEDJET_CLIENT_SCHEMA, + bedjet_ns, + register_bedjet_child, +) + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@jhansche"] +DEPENDENCIES = ["bedjet"] + +BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent) + +CONFIG_SCHEMA = ( + fan.FAN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BedJetFan), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(BEDJET_CLIENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await fan.register_fan(var, config) + await register_bedjet_child(var, config) diff --git a/esphome/components/bedjet/fan/bedjet_fan.cpp b/esphome/components/bedjet/fan/bedjet_fan.cpp new file mode 100644 index 0000000000..02ac289e0e --- /dev/null +++ b/esphome/components/bedjet/fan/bedjet_fan.cpp @@ -0,0 +1,108 @@ +#include "bedjet_fan.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace bedjet { + +using namespace esphome::fan; + +void BedJetFan::dump_config() { LOG_FAN("", "BedJet Fan", this); } +std::string BedJetFan::describe() { return "BedJet Fan"; } + +void BedJetFan::control(const fan::FanCall &call) { + ESP_LOGD(TAG, "Received BedJetFan::control"); + if (!this->parent_->is_connected()) { + ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); + return; + } + bool did_change = false; + + if (call.get_state().has_value() && this->state != *call.get_state()) { + // Turning off is easy: + if (this->state && this->parent_->button_off()) { + this->state = false; + this->publish_state(); + return; + } + + // Turning on, we have to choose a specific mode; for now, use "COOL" mode + // In the future we could configure the mode to use for fan.turn_on. + if (this->parent_->button_cool()) { + this->state = true; + did_change = true; + } + } + + // ignore speed changes if not on or turning on + if (this->state && call.get_speed().has_value()) { + this->speed = *call.get_speed(); + this->parent_->set_fan_index(this->speed); + did_change = true; + } + + if (did_change) { + this->publish_state(); + } +} + +void BedJetFan::on_status(const BedjetStatusPacket *data) { + ESP_LOGVV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data); + bool did_change = false; + bool new_state = data->mode != MODE_STANDBY && data->mode != MODE_WAIT; + + if (new_state != this->state) { + this->state = new_state; + did_change = true; + } + + if (data->fan_step != this->speed) { + this->speed = data->fan_step; + did_change = true; + } + + if (did_change) { + this->publish_state(); + } +} + +/** Attempts to update the fan device from the last received BedjetStatusPacket. + * + * This will be called from #on_status() when the parent dispatches new status packets, + * and from #update() when the polling interval is triggered. + * + * @return `true` if the status has been applied; `false` if there is nothing to apply. + */ +bool BedJetFan::update_status_() { + if (!this->parent_->is_connected()) + return false; + if (!this->parent_->has_status()) + return false; + + auto *status = this->parent_->get_status_packet(); + + if (status == nullptr) + return false; + + this->on_status(status); + return true; +} + +void BedJetFan::update() { + ESP_LOGD(TAG, "[%s] update()", this->get_name().c_str()); + // TODO: if the hub component is already polling, do we also need to include polling? + // We're already going to get on_status() at the hub's polling interval. + auto result = this->update_status_(); + ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false"); +} + +/** Resets states to defaults. */ +void BedJetFan::reset_state_() { + this->state = false; + this->publish_state(); +} +} // namespace bedjet +} // namespace esphome + +#endif diff --git a/esphome/components/bedjet/fan/bedjet_fan.h b/esphome/components/bedjet/fan/bedjet_fan.h new file mode 100644 index 0000000000..19db06e9d3 --- /dev/null +++ b/esphome/components/bedjet/fan/bedjet_fan.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "esphome/components/bedjet/bedjet_child.h" +#include "esphome/components/bedjet/bedjet_codec.h" +#include "esphome/components/bedjet/bedjet_hub.h" +#include "esphome/components/fan/fan.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace bedjet { + +class BedJetFan : public fan::Fan, public BedJetClient, public PollingComponent { + public: + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + /* BedJetClient status update */ + void on_status(const BedjetStatusPacket *data) override; + void on_bedjet_state(bool is_ready) override{}; + std::string describe() override; + + fan::FanTraits get_traits() override { return fan::FanTraits(false, true, false, BEDJET_FAN_SPEED_COUNT); } + + protected: + void control(const fan::FanCall &call) override; + + private: + void reset_state_(); + bool update_status_(); +}; + +} // namespace bedjet +} // namespace esphome + +#endif diff --git a/tests/test1.yaml b/tests/test1.yaml index fcc7107bd6..b741f3e255 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2219,6 +2219,9 @@ fan: on_speed_set: then: - logger.log: "Fan speed was changed!" + - platform: bedjet + name: My Bedjet fan + bedjet_id: my_bedjet_client - platform: copy source_id: fan_speed name: "Fan Speed Copy"