diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 46ff0c2d53..f8772948fc 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -12,6 +12,8 @@ from esphome.const import ( CONF_OSCILLATION_COMMAND_TOPIC, CONF_OSCILLATION_STATE_TOPIC, CONF_SPEED, + CONF_SPEED_LEVEL_COMMAND_TOPIC, + CONF_SPEED_LEVEL_STATE_TOPIC, CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_NAME, @@ -57,6 +59,12 @@ FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( cv.Optional(CONF_OSCILLATION_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), + cv.Optional(CONF_SPEED_LEVEL_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_SPEED_LEVEL_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.subscribe_topic + ), cv.Optional(CONF_SPEED_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), @@ -104,6 +112,18 @@ async def setup_fan_core_(var, config): config[CONF_OSCILLATION_COMMAND_TOPIC] ) ) + if CONF_SPEED_LEVEL_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_speed_level_state_topic( + config[CONF_SPEED_LEVEL_STATE_TOPIC] + ) + ) + if CONF_SPEED_LEVEL_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_speed_level_command_topic( + config[CONF_SPEED_LEVEL_COMMAND_TOPIC] + ) + ) if CONF_SPEED_STATE_TOPIC in config: cg.add(mqtt_.set_custom_speed_state_topic(config[CONF_SPEED_STATE_TOPIC])) if CONF_SPEED_COMMAND_TOPIC in config: diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index c8db5ecece..ed1ab605aa 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -16,6 +16,7 @@ MQTTFanComponent::MQTTFanComponent(FanState *state) : MQTTComponent(), state_(st FanState *MQTTFanComponent::get_state() const { return this->state_; } std::string MQTTFanComponent::component_type() const { return "fan"; } + void MQTTFanComponent::setup() { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { auto val = parse_on_off(payload.c_str()); @@ -64,6 +65,26 @@ void MQTTFanComponent::setup() { }); } + if (this->state_->get_traits().supports_speed()) { + this->subscribe(this->get_speed_level_command_topic(), + [this](const std::string &topic, const std::string &payload) { + optional speed_level_opt = parse_int(payload); + if (speed_level_opt.has_value()) { + const int speed_level = speed_level_opt.value(); + if (speed_level >= 0 && speed_level <= this->state_->get_traits().supported_speed_count()) { + ESP_LOGD(TAG, "New speed level %d", speed_level); + this->state_->make_call().set_speed(speed_level).perform(); + } else { + ESP_LOGW(TAG, "Invalid speed level %d", speed_level); + this->status_momentary_warning("speed", 5000); + } + } else { + ESP_LOGW(TAG, "Invalid speed level %s (int expected)", payload.c_str()); + this->status_momentary_warning("speed", 5000); + } + }); + } + if (this->state_->get_traits().supports_speed()) { this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) { this->state_->make_call() @@ -75,6 +96,22 @@ void MQTTFanComponent::setup() { auto f = std::bind(&MQTTFanComponent::publish_state, this); this->state_->add_on_state_callback([this, f]() { this->defer("send", f); }); } + +void MQTTFanComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Fan '%s': ", this->state_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true); + if (this->state_->get_traits().supports_oscillation()) { + ESP_LOGCONFIG(TAG, " Oscillation State Topic: '%s'", this->get_oscillation_state_topic().c_str()); + ESP_LOGCONFIG(TAG, " Oscillation Command Topic: '%s'", this->get_oscillation_command_topic().c_str()); + } + if (this->state_->get_traits().supports_speed()) { + ESP_LOGCONFIG(TAG, " Speed Level State Topic: '%s'", this->get_speed_level_state_topic().c_str()); + ESP_LOGCONFIG(TAG, " Speed Level Command Topic: '%s'", this->get_speed_level_command_topic().c_str()); + ESP_LOGCONFIG(TAG, " Speed State Topic: '%s'", this->get_speed_state_topic().c_str()); + ESP_LOGCONFIG(TAG, " Speed Command Topic: '%s'", this->get_speed_command_topic().c_str()); + } +} + bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } std::string MQTTFanComponent::friendly_name() const { return this->state_->get_name(); } void MQTTFanComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { @@ -83,6 +120,8 @@ void MQTTFanComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfi root["oscillation_state_topic"] = this->get_oscillation_state_topic(); } if (this->state_->get_traits().supports_speed()) { + root["speed_level_command_topic"] = this->get_speed_level_command_topic(); + root["speed_level_state_topic"] = this->get_speed_level_state_topic(); root["speed_command_topic"] = this->get_speed_command_topic(); root["speed_state_topic"] = this->get_speed_state_topic(); } @@ -99,6 +138,11 @@ bool MQTTFanComponent::publish_state() { failed = failed || !success; } auto traits = this->state_->get_traits(); + if (traits.supports_speed()) { + std::string payload = to_string(this->state_->speed); + bool success = this->publish(this->get_speed_level_state_topic(), payload); + failed = failed || !success; + } if (traits.supports_speed()) { const char *payload; // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index 99d9c055cf..00263e13eb 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -17,6 +17,8 @@ class MQTTFanComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, command) MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, state) + MQTT_COMPONENT_CUSTOM_TOPIC(speed_level, command) + MQTT_COMPONENT_CUSTOM_TOPIC(speed_level, state) MQTT_COMPONENT_CUSTOM_TOPIC(speed, command) MQTT_COMPONENT_CUSTOM_TOPIC(speed, state) @@ -26,6 +28,9 @@ class MQTTFanComponent : public mqtt::MQTTComponent { // (In most use cases you won't need these) /// Setup the fan subscriptions and discovery. void setup() override; + + void dump_config() override; + /// Send the full current state to MQTT. bool send_initial_state() override; bool publish_state(); diff --git a/esphome/const.py b/esphome/const.py index f032cf0fc3..2cf261b4b5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -597,6 +597,8 @@ CONF_SOURCE = "source" CONF_SPEED = "speed" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" CONF_SPEED_COUNT = "speed_count" +CONF_SPEED_LEVEL_COMMAND_TOPIC = "speed_level_command_topic" +CONF_SPEED_LEVEL_STATE_TOPIC = "speed_level_state_topic" CONF_SPEED_STATE_TOPIC = "speed_state_topic" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" diff --git a/tests/test1.yaml b/tests/test1.yaml index c1ddf26488..0fb14fd34b 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1978,6 +1978,8 @@ fan: direction_output: gpio_26 oscillation_state_topic: oscillation/state/topic oscillation_command_topic: oscillation/command/topic + speed_level_state_topic: speed_level/state/topic + speed_level_command_topic: speed_level/command/topic speed_state_topic: speed/state/topic speed_command_topic: speed/command/topic on_speed_set: