🏗 Merge C++ into python codebase (#504)

## Description:

Move esphome-core codebase into esphome (and a bunch of other refactors). See https://github.com/esphome/feature-requests/issues/97

Yes this is a shit ton of work and no there's no way to automate it :( But it will be worth it 👍

Progress:
- Core support (file copy etc): 80%
- Base Abstractions (light, switch): ~50%
- Integrations: ~10%
- Working? Yes, (but only with ported components).

Other refactors:
- Moves all codegen related stuff into a single class: `esphome.codegen` (imported as `cg`)
- Rework coroutine syntax
- Move from `component/platform.py` to `domain/component.py` structure as with HA
- Move all defaults out of C++ and into config validation.
- Remove `make_...` helpers from Application class. Reason: Merge conflicts with every single new integration.
- Pointer Variables are stored globally instead of locally in setup(). Reason: stack size limit.

Future work:
- Rework const.py - Move all `CONF_...` into a conf class (usage `conf.UPDATE_INTERVAL` vs `CONF_UPDATE_INTERVAL`). Reason: Less convoluted import block
- Enable loading from `custom_components` folder.

**Related issue (if applicable):** https://github.com/esphome/feature-requests/issues/97

**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here>

## Checklist:
  - [ ] The code change is tested and works locally.
  - [ ] Tests have been added to verify that the new code works (under `tests/` folder).

If user exposed functionality or configuration variables are added/changed:
  - [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs).
This commit is contained in:
Otto Winter
2019-04-17 12:06:00 +02:00
committed by GitHub
parent 049807e3ab
commit 6682c43dfa
817 changed files with 54156 additions and 10830 deletions
+314
View File
@@ -0,0 +1,314 @@
import re
from esphome import automation
from esphome.automation import ACTION_REGISTRY, CONDITION_REGISTRY, Condition
from esphome.components import logger
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CLIENT_ID, \
CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, \
CONF_ID, CONF_KEEPALIVE, CONF_LEVEL, CONF_LOG_TOPIC, CONF_ON_JSON_MESSAGE, CONF_ON_MESSAGE, \
CONF_PASSWORD, CONF_PAYLOAD, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PORT, \
CONF_QOS, CONF_REBOOT_TIMEOUT, CONF_RETAIN, CONF_SHUTDOWN_MESSAGE, CONF_SSL_FINGERPRINTS, \
CONF_STATE_TOPIC, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_TRIGGER_ID, CONF_USERNAME, \
CONF_WILL_MESSAGE
from esphome.core import coroutine_with_priority, coroutine, CORE
AUTO_LOAD = ['json']
def validate_message_just_topic(value):
value = cv.publish_topic(value)
return MQTT_MESSAGE_BASE({CONF_TOPIC: value})
MQTT_MESSAGE_BASE = cv.Schema({
cv.Required(CONF_TOPIC): cv.publish_topic,
cv.Optional(CONF_QOS, default=0): cv.mqtt_qos,
cv.Optional(CONF_RETAIN, default=True): cv.boolean,
})
MQTT_MESSAGE_TEMPLATE_SCHEMA = cv.Any(None, MQTT_MESSAGE_BASE, validate_message_just_topic)
MQTT_MESSAGE_SCHEMA = cv.Any(None, MQTT_MESSAGE_BASE.extend({
cv.Required(CONF_PAYLOAD): cv.mqtt_payload,
}))
mqtt_ns = cg.esphome_ns.namespace('mqtt')
MQTTMessage = mqtt_ns.struct('MQTTMessage')
MQTTClientComponent = mqtt_ns.class_('MQTTClientComponent', cg.Component)
MQTTPublishAction = mqtt_ns.class_('MQTTPublishAction', cg.Action)
MQTTPublishJsonAction = mqtt_ns.class_('MQTTPublishJsonAction', cg.Action)
MQTTMessageTrigger = mqtt_ns.class_('MQTTMessageTrigger', cg.Trigger.template(cg.std_string))
MQTTJsonMessageTrigger = mqtt_ns.class_('MQTTJsonMessageTrigger',
cg.Trigger.template(cg.JsonObjectConstRef))
MQTTComponent = mqtt_ns.class_('MQTTComponent', cg.Component)
MQTTConnectedCondition = mqtt_ns.class_('MQTTConnectedCondition', Condition)
MQTTBinarySensorComponent = mqtt_ns.class_('MQTTBinarySensorComponent', MQTTComponent)
MQTTClimateComponent = mqtt_ns.class_('MQTTClimateComponent', MQTTComponent)
MQTTCoverComponent = mqtt_ns.class_('MQTTCoverComponent', MQTTComponent)
MQTTFanComponent = mqtt_ns.class_('MQTTFanComponent', MQTTComponent)
MQTTJSONLightComponent = mqtt_ns.class_('MQTTJSONLightComponent', MQTTComponent)
MQTTSensorComponent = mqtt_ns.class_('MQTTSensorComponent', MQTTComponent)
MQTTSwitchComponent = mqtt_ns.class_('MQTTSwitchComponent', MQTTComponent)
MQTTTextSensor = mqtt_ns.class_('MQTTTextSensor', MQTTComponent)
def validate_config(value):
# Populate default fields
out = value.copy()
topic_prefix = value[CONF_TOPIC_PREFIX]
if CONF_BIRTH_MESSAGE not in value:
out[CONF_BIRTH_MESSAGE] = {
CONF_TOPIC: '{}/status'.format(topic_prefix),
CONF_PAYLOAD: 'online',
CONF_QOS: 0,
CONF_RETAIN: True,
}
if CONF_WILL_MESSAGE not in value:
out[CONF_WILL_MESSAGE] = {
CONF_TOPIC: '{}/status'.format(topic_prefix),
CONF_PAYLOAD: 'offline',
CONF_QOS: 0,
CONF_RETAIN: True,
}
if CONF_SHUTDOWN_MESSAGE not in value:
out[CONF_SHUTDOWN_MESSAGE] = {
CONF_TOPIC: '{}/status'.format(topic_prefix),
CONF_PAYLOAD: 'offline',
CONF_QOS: 0,
CONF_RETAIN: True,
}
if CONF_LOG_TOPIC not in value:
out[CONF_LOG_TOPIC] = {
CONF_TOPIC: '{}/debug'.format(topic_prefix),
CONF_QOS: 0,
CONF_RETAIN: True,
}
return out
def validate_fingerprint(value):
value = cv.string(value)
if re.match(r'^[0-9a-f]{40}$', value) is None:
raise cv.Invalid(u"fingerprint must be valid SHA1 hash")
return value
CONFIG_SCHEMA = cv.All(cv.Schema({
cv.GenerateID(): cv.declare_variable_id(MQTTClientComponent),
cv.Required(CONF_BROKER): cv.string_strict,
cv.Optional(CONF_PORT, default=1883): cv.port,
cv.Optional(CONF_USERNAME, default=''): cv.string,
cv.Optional(CONF_PASSWORD, default=''): cv.string,
cv.Optional(CONF_CLIENT_ID, default=lambda: CORE.name): cv.All(cv.string, cv.Length(max=23)),
cv.Optional(CONF_DISCOVERY, default=True): cv.Any(cv.boolean, cv.one_of("CLEAN", upper=True)),
cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean,
cv.Optional(CONF_DISCOVERY_PREFIX, default="homeassistant"): cv.publish_topic,
cv.Optional(CONF_BIRTH_MESSAGE): MQTT_MESSAGE_SCHEMA,
cv.Optional(CONF_WILL_MESSAGE): MQTT_MESSAGE_SCHEMA,
cv.Optional(CONF_SHUTDOWN_MESSAGE): MQTT_MESSAGE_SCHEMA,
cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.publish_topic,
cv.Optional(CONF_LOG_TOPIC): cv.Any(None, MQTT_MESSAGE_BASE.extend({
cv.Optional(CONF_LEVEL): logger.is_log_level,
}), validate_message_just_topic),
cv.Optional(CONF_SSL_FINGERPRINTS): cv.All(cv.only_on_esp8266,
cv.ensure_list(validate_fingerprint)),
cv.Optional(CONF_KEEPALIVE, default='15s'): cv.positive_time_period_seconds,
cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(MQTTMessageTrigger),
cv.Required(CONF_TOPIC): cv.subscribe_topic,
cv.Optional(CONF_QOS, default=0): cv.mqtt_qos,
cv.Optional(CONF_PAYLOAD): cv.string_strict,
}),
cv.Optional(CONF_ON_JSON_MESSAGE): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(MQTTJsonMessageTrigger),
cv.Required(CONF_TOPIC): cv.subscribe_topic,
cv.Optional(CONF_QOS, default=0): cv.mqtt_qos,
}),
}), validate_config)
def exp_mqtt_message(config):
if config is None:
return cg.optional(cg.TemplateArguments(MQTTMessage))
exp = cg.StructInitializer(
MQTTMessage,
('topic', config[CONF_TOPIC]),
('payload', config.get(CONF_PAYLOAD, "")),
('qos', config[CONF_QOS]),
('retain', config[CONF_RETAIN])
)
return exp
@coroutine_with_priority(40.0)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_broker_address(config[CONF_BROKER]))
cg.add(var.set_broker_port(config[CONF_PORT]))
cg.add(var.set_username(config[CONF_USERNAME]))
cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_client_id(config[CONF_CLIENT_ID]))
discovery = config[CONF_DISCOVERY]
discovery_retain = config[CONF_DISCOVERY_RETAIN]
discovery_prefix = config[CONF_DISCOVERY_PREFIX]
if not discovery:
cg.add(var.disable_discovery())
elif discovery == "CLEAN":
cg.add(var.set_discovery_info(discovery_prefix, discovery_retain, True))
elif CONF_DISCOVERY_RETAIN in config or CONF_DISCOVERY_PREFIX in config:
cg.add(var.set_discovery_info(discovery_prefix, discovery_retain))
cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX]))
birth_message = config[CONF_BIRTH_MESSAGE]
if not birth_message:
cg.add(var.disable_birth_message())
else:
cg.add(var.set_birth_message(exp_mqtt_message(birth_message)))
will_message = config[CONF_WILL_MESSAGE]
if not will_message:
cg.add(var.disable_last_will())
else:
cg.add(var.set_last_will(exp_mqtt_message(will_message)))
shutdown_message = config[CONF_SHUTDOWN_MESSAGE]
if not shutdown_message:
cg.add(var.disable_shutdown_message())
else:
cg.add(var.set_shutdown_message(exp_mqtt_message(shutdown_message)))
log_topic = config[CONF_LOG_TOPIC]
if not log_topic:
cg.add(var.disable_log_message())
else:
cg.add(var.set_log_message_template(exp_mqtt_message(log_topic)))
if CONF_LEVEL in log_topic:
cg.add(var.set_log_level(logger.LOG_LEVELS[log_topic[CONF_LEVEL]]))
if CONF_SSL_FINGERPRINTS in config:
for fingerprint in config[CONF_SSL_FINGERPRINTS]:
arr = [cg.RawExpression("0x{}".format(fingerprint[i:i + 2])) for i in range(0, 40, 2)]
cg.add(var.add_ssl_fingerprint(arr))
cg.add_build_flag('-DASYNC_TCP_SSL_ENABLED=1')
cg.add(var.set_keep_alive(config[CONF_KEEPALIVE]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
for conf in config.get(CONF_ON_MESSAGE, []):
trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC])
cg.add(trig.set_qos(conf[CONF_QOS]))
if CONF_PAYLOAD in conf:
cg.add(trig.set_payload(conf[CONF_PAYLOAD]))
yield automation.build_automation(trig, [(cg.std_string, 'x')], conf)
for conf in config.get(CONF_ON_JSON_MESSAGE, []):
trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS])
yield automation.build_automation(trig, [(cg.JsonObjectConstRef, 'x')], conf)
cg.add_library('AsyncMqttClient', '0.8.2')
cg.add_define('USE_MQTT')
cg.add_global(mqtt_ns.using)
MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema({
cv.GenerateID(): cv.use_variable_id(MQTTClientComponent),
cv.Required(CONF_TOPIC): cv.templatable(cv.publish_topic),
cv.Required(CONF_PAYLOAD): cv.templatable(cv.mqtt_payload),
cv.Optional(CONF_QOS, default=0): cv.templatable(cv.mqtt_qos),
cv.Optional(CONF_RETAIN, default=False): cv.templatable(cv.boolean),
})
@ACTION_REGISTRY.register('mqtt.publish', MQTT_PUBLISH_ACTION_SCHEMA)
def mqtt_publish_action_to_code(config, action_id, template_arg, args):
var = yield cg.get_variable(config[CONF_ID])
type = MQTTPublishAction.template(template_arg)
rhs = type.new(var)
action = cg.Pvariable(action_id, rhs, type=type)
template_ = yield cg.templatable(config[CONF_TOPIC], args, cg.std_string)
cg.add(action.set_topic(template_))
template_ = yield cg.templatable(config[CONF_PAYLOAD], args, cg.std_string)
cg.add(action.set_payload(template_))
template_ = yield cg.templatable(config[CONF_QOS], args, cg.uint8)
cg.add(action.set_qos(template_))
template_ = yield cg.templatable(config[CONF_RETAIN], args, bool)
cg.add(action.set_retain(template_))
yield action
MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema({
cv.GenerateID(): cv.use_variable_id(MQTTClientComponent),
cv.Required(CONF_TOPIC): cv.templatable(cv.publish_topic),
cv.Required(CONF_PAYLOAD): cv.lambda_,
cv.Optional(CONF_QOS, default=0): cv.templatable(cv.mqtt_qos),
cv.Optional(CONF_RETAIN, default=False): cv.templatable(cv.boolean),
})
@ACTION_REGISTRY.register('mqtt.publish_json', MQTT_PUBLISH_JSON_ACTION_SCHEMA)
def mqtt_publish_json_action_to_code(config, action_id, template_arg, args):
var = yield cg.get_variable(config[CONF_ID])
type = MQTTPublishJsonAction.template(template_arg)
rhs = type.new(var)
action = cg.Pvariable(action_id, rhs, type=type)
template_ = yield cg.templatable(config[CONF_TOPIC], args, cg.std_string)
cg.add(action.set_topic(template_))
args_ = args + [(cg.JsonObjectRef, 'root')]
lambda_ = yield cg.process_lambda(config[CONF_PAYLOAD], args_, return_type=cg.void)
cg.add(action.set_payload(lambda_))
template_ = yield cg.templatable(config[CONF_QOS], args, cg.uint8)
cg.add(action.set_qos(template_))
template_ = yield cg.templatable(config[CONF_RETAIN], args, bool)
cg.add(action.set_retain(template_))
yield action
def get_default_topic_for(data, component_type, name, suffix):
whitelist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
sanitized_name = ''.join(x for x in name.lower().replace(' ', '_') if x in whitelist)
return '{}/{}/{}/{}'.format(data.topic_prefix, component_type,
sanitized_name, suffix)
@coroutine
def register_mqtt_component(var, config):
yield cg.register_component(var, {})
if CONF_RETAIN in config:
cg.add(var.set_retain(config[CONF_RETAIN]))
if not config.get(CONF_DISCOVERY, True):
cg.add(var.disable_discovery())
if CONF_STATE_TOPIC in config:
cg.add(var.set_custom_state_topic(config[CONF_STATE_TOPIC]))
if CONF_COMMAND_TOPIC in config:
cg.add(var.set_custom_command_topic(config[CONF_COMMAND_TOPIC]))
if CONF_AVAILABILITY in config:
availability = config[CONF_AVAILABILITY]
if not availability:
cg.add(var.disable_availability())
else:
cg.add(var.set_availability(availability[CONF_TOPIC],
availability[CONF_PAYLOAD_AVAILABLE],
availability[CONF_PAYLOAD_NOT_AVAILABLE]))
@CONDITION_REGISTRY.register('mqtt.connected', cv.Schema({
cv.GenerateID(): cv.use_variable_id(MQTTClientComponent),
}))
def mqtt_connected_to_code(config, condition_id, template_arg, args):
var = yield cg.get_variable(config[CONF_ID])
type = MQTTConnectedCondition.template(template_arg)
rhs = type.new(var)
yield cg.Pvariable(condition_id, rhs, type=type)
@@ -0,0 +1,54 @@
#include "mqtt_binary_sensor.h"
#include "esphome/core/log.h"
#ifdef USE_BINARY_SENSOR
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt.binary_sensor";
std::string MQTTBinarySensorComponent::component_type() const { return "binary_sensor"; }
void MQTTBinarySensorComponent::setup() {
this->binary_sensor_->add_on_state_callback([this](bool state) { this->publish_state(state); });
}
void MQTTBinarySensorComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Binary Sensor '%s':", this->binary_sensor_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false)
}
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
: MQTTComponent(), binary_sensor_(binary_sensor) {}
std::string MQTTBinarySensorComponent::friendly_name() const { return this->binary_sensor_->get_name(); }
void MQTTBinarySensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) {
if (!this->binary_sensor_->get_device_class().empty())
root["device_class"] = this->binary_sensor_->get_device_class();
if (this->is_status_)
root["payload_on"] = mqtt::global_mqtt_client->get_availability().payload_available;
if (this->is_status_)
root["payload_off"] = mqtt::global_mqtt_client->get_availability().payload_not_available;
config.command_topic = false;
}
bool MQTTBinarySensorComponent::send_initial_state() {
if (this->binary_sensor_->has_state()) {
return this->publish_state(this->binary_sensor_->state);
} else {
return true;
}
}
bool MQTTBinarySensorComponent::is_internal() { return this->binary_sensor_->is_internal(); }
bool MQTTBinarySensorComponent::publish_state(bool state) {
if (this->is_status_)
return true;
const char *state_s = state ? "ON" : "OFF";
return this->publish(this->get_state_topic_(), state_s);
}
void MQTTBinarySensorComponent::set_is_status(bool status) { this->is_status_ = status; }
} // namespace mqtt
} // namespace esphome
#endif
@@ -0,0 +1,44 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "mqtt_component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace mqtt {
class MQTTBinarySensorComponent : public mqtt::MQTTComponent {
public:
/** Construct a MQTTBinarySensorComponent.
*
* @param binary_sensor The binary sensor.
*/
explicit MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor);
void setup() override;
void dump_config() override;
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override;
void set_is_status(bool status);
bool send_initial_state() override;
bool publish_state(bool state);
bool is_internal() override;
protected:
std::string friendly_name() const override;
std::string component_type() const override;
binary_sensor::BinarySensor *binary_sensor_;
bool is_status_{false};
};
} // namespace mqtt
} // namespace esphome
#endif
+554
View File
@@ -0,0 +1,554 @@
#include "mqtt_client.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/util.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
#include "lwip/err.h"
#include "lwip/dns.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt";
MQTTClientComponent::MQTTClientComponent() { global_mqtt_client = this; }
// Connection
void MQTTClientComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up MQTT...");
this->mqtt_client_.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties,
size_t len, size_t index, size_t total) {
std::string payload_s(payload, len);
std::string topic_s(topic);
this->on_message(topic_s, payload_s);
});
this->mqtt_client_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) {
this->state_ = MQTT_CLIENT_DISCONNECTED;
this->disconnect_reason_ = reason;
});
#ifdef USE_LOGGER
if (this->is_log_message_enabled() && logger::global_logger != nullptr) {
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
if (level <= this->log_level_ && this->is_connected()) {
this->publish(this->log_message_.topic, message, strlen(message), this->log_message_.qos,
this->log_message_.retain);
}
});
}
#endif
this->last_connected_ = millis();
this->start_dnslookup_();
}
void MQTTClientComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT:");
ESP_LOGCONFIG(TAG, " Server Address: %s:%u (%s)", this->credentials_.address.c_str(), this->credentials_.port,
this->ip_.toString().c_str());
ESP_LOGCONFIG(TAG, " Username: " LOG_SECRET("'%s'"), this->credentials_.username.c_str());
ESP_LOGCONFIG(TAG, " Client ID: " LOG_SECRET("'%s'"), this->credentials_.client_id.c_str());
if (!this->discovery_info_.prefix.empty()) {
ESP_LOGCONFIG(TAG, " Discovery prefix: '%s'", this->discovery_info_.prefix.c_str());
ESP_LOGCONFIG(TAG, " Discovery retain: %s", YESNO(this->discovery_info_.retain));
}
ESP_LOGCONFIG(TAG, " Topic Prefix: '%s'", this->topic_prefix_.c_str());
if (!this->log_message_.topic.empty()) {
ESP_LOGCONFIG(TAG, " Log Topic: '%s'", this->log_message_.topic.c_str());
}
if (!this->availability_.topic.empty()) {
ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str());
}
}
bool MQTTClientComponent::can_proceed() { return this->is_connected(); }
void MQTTClientComponent::start_dnslookup_() {
for (auto &subscription : this->subscriptions_) {
subscription.subscribed = false;
subscription.resubscribe_timeout = 0;
}
this->status_set_warning();
this->dns_resolve_error_ = false;
this->dns_resolved_ = false;
ip_addr_t addr;
#ifdef ARDUINO_ARCH_ESP32
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, this->dns_found_callback, this,
LWIP_DNS_ADDRTYPE_IPV4);
#endif
#ifdef ARDUINO_ARCH_ESP8266
err_t err = dns_gethostbyname(this->credentials_.address.c_str(), &addr,
esphome::mqtt::MQTTClientComponent::dns_found_callback, this);
#endif
switch (err) {
case ERR_OK: {
// Got IP immediately
this->dns_resolved_ = true;
#ifdef ARDUINO_ARCH_ESP32
this->ip_ = IPAddress(addr.u_addr.ip4.addr);
#endif
#ifdef ARDUINO_ARCH_ESP8266
this->ip_ = IPAddress(addr.addr);
#endif
this->start_connect_();
return;
}
case ERR_INPROGRESS: {
// wait for callback
ESP_LOGD(TAG, "Resolving MQTT broker IP address...");
break;
}
default:
case ERR_ARG: {
// error
#if defined(ARDUINO_ARCH_ESP8266)
ESP_LOGW(TAG, "Error resolving MQTT broker IP address: %ld", err);
#else
ESP_LOGW(TAG, "Error resolving MQTT broker IP address: %d", err);
#endif
break;
}
}
this->state_ = MQTT_CLIENT_RESOLVING_ADDRESS;
this->connect_begin_ = millis();
}
void MQTTClientComponent::check_dnslookup_() {
if (!this->dns_resolved_ && millis() - this->connect_begin_ > 20000) {
this->dns_resolve_error_ = true;
}
if (this->dns_resolve_error_) {
ESP_LOGW(TAG, "Couldn't resolve IP address for '%s'!", this->credentials_.address.c_str());
this->state_ = MQTT_CLIENT_DISCONNECTED;
return;
}
if (!this->dns_resolved_) {
return;
}
ESP_LOGD(TAG, "Resolved broker IP address to %s", this->ip_.toString().c_str());
this->start_connect_();
}
#if defined(ARDUINO_ARCH_ESP8266) && LWIP_VERSION_MAJOR == 1
void MQTTClientComponent::dns_found_callback(const char *name, ip_addr_t *ipaddr, void *callback_arg) {
#else
void MQTTClientComponent::dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg) {
#endif
auto *a_this = (MQTTClientComponent *) callback_arg;
if (ipaddr == nullptr) {
a_this->dns_resolve_error_ = true;
} else {
#ifdef ARDUINO_ARCH_ESP32
a_this->ip_ = IPAddress(ipaddr->u_addr.ip4.addr);
#endif
#ifdef ARDUINO_ARCH_ESP8266
a_this->ip_ = IPAddress(ipaddr->addr);
#endif
a_this->dns_resolved_ = true;
}
}
void MQTTClientComponent::start_connect_() {
if (!network_is_connected())
return;
ESP_LOGI(TAG, "Connecting to MQTT...");
// Force disconnect first
this->mqtt_client_.disconnect(true);
this->mqtt_client_.setClientId(this->credentials_.client_id.c_str());
const char *username = nullptr;
if (!this->credentials_.username.empty())
username = this->credentials_.username.c_str();
const char *password = nullptr;
if (!this->credentials_.password.empty())
password = this->credentials_.password.c_str();
this->mqtt_client_.setCredentials(username, password);
this->mqtt_client_.setServer(this->ip_, this->credentials_.port);
if (!this->last_will_.topic.empty()) {
this->mqtt_client_.setWill(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain,
this->last_will_.payload.c_str(), this->last_will_.payload.length());
}
this->mqtt_client_.connect();
this->state_ = MQTT_CLIENT_CONNECTING;
this->connect_begin_ = millis();
}
bool MQTTClientComponent::is_connected() {
return this->state_ == MQTT_CLIENT_CONNECTED && this->mqtt_client_.connected();
}
void MQTTClientComponent::check_connected() {
if (!this->mqtt_client_.connected()) {
if (millis() - this->connect_begin_ > 60000) {
this->state_ = MQTT_CLIENT_DISCONNECTED;
this->start_dnslookup_();
}
return;
}
this->state_ = MQTT_CLIENT_CONNECTED;
this->sent_birth_message_ = false;
this->status_clear_warning();
ESP_LOGI(TAG, "MQTT Connected!");
// MQTT Client needs some time to be fully set up.
delay(100);
this->resubscribe_subscriptions_();
for (MQTTComponent *component : this->children_)
component->schedule_resend_state();
}
void MQTTClientComponent::loop() {
if (this->disconnect_reason_.has_value()) {
const char *reason_s = nullptr;
switch (*this->disconnect_reason_) {
case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED:
reason_s = "TCP disconnected";
break;
case AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
reason_s = "Unacceptable Protocol Version";
break;
case AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED:
reason_s = "Identifier Rejected";
break;
case AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE:
reason_s = "Server Unavailable";
break;
case AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS:
reason_s = "Malformed Credentials";
break;
case AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED:
reason_s = "Not Authorized";
break;
case AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE:
reason_s = "Not Enough Space";
break;
case AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT:
reason_s = "TLS Bad Fingerprint";
break;
default:
reason_s = "Unknown";
break;
}
if (!network_is_connected()) {
reason_s = "WiFi disconnected";
}
ESP_LOGW(TAG, "MQTT Disconnected: %s.", reason_s);
this->disconnect_reason_.reset();
}
const uint32_t now = millis();
switch (this->state_) {
case MQTT_CLIENT_DISCONNECTED:
if (now - this->connect_begin_ > 5000) {
this->start_dnslookup_();
}
break;
case MQTT_CLIENT_RESOLVING_ADDRESS:
this->check_dnslookup_();
break;
case MQTT_CLIENT_CONNECTING:
this->check_connected();
break;
case MQTT_CLIENT_CONNECTED:
if (!this->mqtt_client_.connected()) {
this->state_ = MQTT_CLIENT_DISCONNECTED;
ESP_LOGW(TAG, "Lost MQTT Client connection!");
this->start_dnslookup_();
} else {
if (!this->birth_message_.topic.empty() && !this->sent_birth_message_) {
this->sent_birth_message_ = this->publish(this->birth_message_);
}
this->last_connected_ = now;
this->resubscribe_subscriptions_();
}
break;
}
if (millis() - this->last_connected_ > this->reboot_timeout_ && this->reboot_timeout_ != 0) {
ESP_LOGE(TAG, "Can't connect to MQTT... Restarting...");
App.reboot();
}
}
float MQTTClientComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
// Subscribe
bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) {
if (!this->is_connected())
return false;
uint16_t ret = this->mqtt_client_.subscribe(topic, qos);
yield();
if (ret != 0) {
ESP_LOGV(TAG, "subscribe(topic='%s')", topic);
} else {
delay(5);
ESP_LOGV(TAG, "Subscribe failed for topic='%s'. Will retry later.", topic);
this->status_momentary_warning("subscribe", 1000);
}
return ret != 0;
}
void MQTTClientComponent::resubscribe_subscription_(MQTTSubscription *sub) {
if (sub->subscribed)
return;
const uint32_t now = millis();
bool do_resub = sub->resubscribe_timeout == 0 || now - sub->resubscribe_timeout > 1000;
if (do_resub) {
sub->subscribed = this->subscribe_(sub->topic.c_str(), sub->qos);
sub->resubscribe_timeout = now;
}
}
void MQTTClientComponent::resubscribe_subscriptions_() {
for (auto &subscription : this->subscriptions_) {
this->resubscribe_subscription_(&subscription);
}
}
void MQTTClientComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
MQTTSubscription subscription{
.topic = topic,
.qos = qos,
.callback = std::move(callback),
.subscribed = false,
.resubscribe_timeout = 0,
};
this->resubscribe_subscription_(&subscription);
this->subscriptions_.push_back(subscription);
}
void MQTTClientComponent::subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos) {
auto f = [callback](const std::string &topic, const std::string &payload) {
json::parse_json(payload, [topic, callback](JsonObject &root) { callback(topic, root); });
};
MQTTSubscription subscription{
.topic = topic,
.qos = qos,
.callback = f,
.subscribed = false,
.resubscribe_timeout = 0,
};
this->resubscribe_subscription_(&subscription);
this->subscriptions_.push_back(subscription);
}
// Publish
bool MQTTClientComponent::publish(const std::string &topic, const std::string &payload, uint8_t qos, bool retain) {
return this->publish(topic, payload.data(), payload.size(), qos, retain);
}
bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos,
bool retain) {
if (!this->is_connected()) {
// critical components will re-transmit their messages
return false;
}
bool logging_topic = topic == this->log_message_.topic;
uint16_t ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length);
yield();
if (ret == 0 && !logging_topic && this->is_connected()) {
delay(5);
ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length);
yield();
}
if (!logging_topic) {
if (ret != 0) {
ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", topic.c_str(), payload, retain);
} else {
ESP_LOGW(TAG, "Publish failed for topic='%s' will retry later..", topic.c_str());
this->status_momentary_warning("publish", 1000);
}
}
return ret != 0;
}
bool MQTTClientComponent::publish(const MQTTMessage &message) {
return this->publish(message.topic, message.payload, message.qos, message.retain);
}
bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos,
bool retain) {
size_t len;
const char *message = json::build_json(f, &len);
return this->publish(topic, message, len, qos, retain);
}
/** Check if the message topic matches the given subscription topic
*
* INFO: MQTT spec mandates that topics must not be empty and must be valid NULL-terminated UTF-8 strings.
*
* @param message The message topic that was received from the MQTT server. Note: this must not contain
* wildcard characters as mandated by the MQTT spec.
* @param subscription The subscription topic we are matching against.
* @param is_normal Is this a "normal" topic - Does the message topic not begin with a "$".
* @param past_separator Are we past the first '/' topic separator.
* @return true if the subscription topic matches the message topic, false otherwise.
*/
static bool topic_match(const char *message, const char *subscription, bool is_normal, bool past_separator) {
// Reached end of both strings at the same time, this means we have a successful match
if (*message == '\0' && *subscription == '\0')
return true;
// Either the message or the subscribe are at the end. This means they don't match.
if (*message == '\0' || *subscription == '\0')
return false;
bool do_wildcards = is_normal || past_separator;
if (*subscription == '+' && do_wildcards) {
// single level wildcard
// consume + from subscription
subscription++;
// consume everything from message until '/' found or end of string
while (*message != '\0' && *message != '/') {
message++;
}
// after this, both pointers will point to a '/' or to the end of the string
return topic_match(message, subscription, is_normal, true);
}
if (*subscription == '#' && do_wildcards) {
// multilevel wildcard - MQTT mandates that this must be at end of subscribe topic
return true;
}
// this handles '/' and normal characters at the same time.
if (*message != *subscription)
return false;
past_separator = past_separator || *subscription == '/';
// consume characters
subscription++;
message++;
return topic_match(message, subscription, is_normal, past_separator);
}
static bool topic_match(const char *message, const char *subscription) {
return topic_match(message, subscription, *message != '\0' && *message != '$', false);
}
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
#ifdef ARDUINO_ARCH_ESP8266
// on ESP8266, this is called in LWiP thread; some components do not like running
// in an ISR.
this->defer([this, topic, payload]() {
#endif
for (auto &subscription : this->subscriptions_)
if (topic_match(topic.c_str(), subscription.topic.c_str()))
subscription.callback(topic, payload);
#ifdef ARDUINO_ARCH_ESP8266
});
#endif
}
// Setters
void MQTTClientComponent::disable_log_message() { this->log_message_.topic = ""; }
bool MQTTClientComponent::is_log_message_enabled() const { return !this->log_message_.topic.empty(); }
void MQTTClientComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { this->children_.push_back(component); }
void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; }
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_client_.setKeepAlive(keep_alive_s); }
void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
void MQTTClientComponent::set_topic_prefix(std::string topic_prefix) { this->topic_prefix_ = std::move(topic_prefix); }
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
void MQTTClientComponent::disable_birth_message() {
this->birth_message_.topic = "";
this->recalculate_availability_();
}
void MQTTClientComponent::disable_shutdown_message() {
this->shutdown_message_.topic = "";
this->recalculate_availability_();
}
bool MQTTClientComponent::is_discovery_enabled() const { return !this->discovery_info_.prefix.empty(); }
const Availability &MQTTClientComponent::get_availability() { return this->availability_; }
void MQTTClientComponent::recalculate_availability_() {
if (this->birth_message_.topic.empty() || this->birth_message_.topic != this->last_will_.topic) {
this->availability_.topic = "";
return;
}
this->availability_.topic = this->birth_message_.topic;
this->availability_.payload_available = this->birth_message_.payload;
this->availability_.payload_not_available = this->last_will_.payload;
}
void MQTTClientComponent::set_last_will(MQTTMessage &&message) {
this->last_will_ = std::move(message);
this->recalculate_availability_();
}
void MQTTClientComponent::set_birth_message(MQTTMessage &&message) {
this->birth_message_ = std::move(message);
this->recalculate_availability_();
}
void MQTTClientComponent::set_shutdown_message(MQTTMessage &&message) { this->shutdown_message_ = std::move(message); }
void MQTTClientComponent::set_discovery_info(std::string &&prefix, bool retain, bool clean) {
this->discovery_info_.prefix = std::move(prefix);
this->discovery_info_.retain = retain;
this->discovery_info_.clean = clean;
}
void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; }
void MQTTClientComponent::disable_discovery() {
this->discovery_info_ = MQTTDiscoveryInfo{.prefix = "", .retain = false};
}
void MQTTClientComponent::on_shutdown() {
if (!this->shutdown_message_.topic.empty()) {
yield();
this->publish(this->shutdown_message_);
yield();
}
this->mqtt_client_.disconnect(true);
}
#if ASYNC_TCP_SSL_ENABLED
void MQTTClientComponent::add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint) {
this->mqtt_client_.setSecure(true);
this->mqtt_client_.addServerFingerprint(fingerprint.data());
}
#endif
MQTTClientComponent *global_mqtt_client = nullptr;
// MQTTMessageTrigger
MQTTMessageTrigger::MQTTMessageTrigger(const std::string &topic) : topic_(topic) {}
void MQTTMessageTrigger::set_qos(uint8_t qos) { this->qos_ = qos; }
void MQTTMessageTrigger::set_payload(const std::string &payload) { this->payload_ = payload; }
void MQTTMessageTrigger::setup() {
global_mqtt_client->subscribe(this->topic_,
[this](const std::string &topic, const std::string &payload) {
if (this->payload_.has_value() && payload != *this->payload_) {
return;
}
this->trigger(payload);
},
this->qos_);
}
void MQTTMessageTrigger::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Message Trigger:");
ESP_LOGCONFIG(TAG, " Topic: '%s'", this->topic_.c_str());
ESP_LOGCONFIG(TAG, " QoS: %u", this->qos_);
}
float MQTTMessageTrigger::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
} // namespace mqtt
} // namespace esphome
+345
View File
@@ -0,0 +1,345 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/automation.h"
#include "esphome/core/log.h"
#include "esphome/components/json/json_util.h"
#include <AsyncMqttClient.h>
#include "lwip/ip_addr.h"
namespace esphome {
namespace mqtt {
/** Callback for MQTT subscriptions.
*
* First parameter is the topic, the second one is the payload.
*/
using mqtt_callback_t = std::function<void(const std::string &, const std::string &)>;
using mqtt_json_callback_t = std::function<void(const std::string &, JsonObject &)>;
/// internal struct for MQTT messages.
struct MQTTMessage {
std::string topic;
std::string payload;
uint8_t qos; ///< QoS. Only for last will testaments.
bool retain;
};
/// internal struct for MQTT subscriptions.
struct MQTTSubscription {
std::string topic;
uint8_t qos;
mqtt_callback_t callback;
bool subscribed;
uint32_t resubscribe_timeout;
};
/// internal struct for MQTT credentials.
struct MQTTCredentials {
std::string address; ///< The address of the server without port number
uint16_t port; ///< The port number of the server.
std::string username;
std::string password;
std::string client_id; ///< The client ID. Will automatically be truncated to 23 characters.
};
/// Simple data struct for Home Assistant component availability.
struct Availability {
std::string topic; ///< Empty means disabled
std::string payload_available;
std::string payload_not_available;
};
/** Internal struct for MQTT Home Assistant discovery
*
* See <a href="https://www.home-assistant.io/docs/mqtt/discovery/">MQTT Discovery</a>.
*/
struct MQTTDiscoveryInfo {
std::string prefix; ///< The Home Assistant discovery prefix. Empty means disabled.
bool retain; ///< Whether to retain discovery messages.
bool clean;
};
enum MQTTClientState {
MQTT_CLIENT_DISCONNECTED = 0,
MQTT_CLIENT_RESOLVING_ADDRESS,
MQTT_CLIENT_CONNECTING,
MQTT_CLIENT_CONNECTED,
};
class MQTTComponent;
class MQTTClientComponent : public Component {
public:
MQTTClientComponent();
/// Set the last will testament message.
void set_last_will(MQTTMessage &&message);
/// Remove the last will testament message.
void disable_last_will();
/// Set the birth message.
void set_birth_message(MQTTMessage &&message);
/// Remove the birth message.
void disable_birth_message();
void set_shutdown_message(MQTTMessage &&message);
void disable_shutdown_message();
/// Set the keep alive time in seconds, every 0.7*keep_alive a ping will be sent.
void set_keep_alive(uint16_t keep_alive_s);
/** Set the Home Assistant discovery info
*
* See <a href="https://www.home-assistant.io/docs/mqtt/discovery/">MQTT Discovery</a>.
* @param prefix The Home Assistant discovery prefix.
* @param retain Whether to retain discovery messages.
*/
void set_discovery_info(std::string &&prefix, bool retain, bool clean = false);
/// Get Home Assistant discovery info.
const MQTTDiscoveryInfo &get_discovery_info() const;
/// Globally disable Home Assistant discovery.
void disable_discovery();
bool is_discovery_enabled() const;
#if ASYNC_TCP_SSL_ENABLED
/** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker.
*
* To use this feature you first have to globally enable the `ASYNC_TCP_SSL_ENABLED` define flag.
* This function can be called multiple times and any certificate that matches any of the provided fingerprints
* will match. Calling this method will also automatically disable all non-ssl connections.
*
* @warning This is *not* secure and *not* how SSL is usually done. You'll have to add
* a separate fingerprint for every certificate you use. Additionally, the hashing
* algorithm used here due to the constraints of the MCU, SHA1, is known to be insecure.
*
* @param fingerprint The SSL fingerprint as a 20 value long std::array.
*/
void add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint);
#endif
const Availability &get_availability();
/** Set the topic prefix that will be prepended to all topics together with "/". This will, in most cases,
* be the name of your Application.
*
* For example, if "livingroom" is passed to this method, all state topics will, by default, look like
* "livingroom/.../state"
*
* @param topic_prefix The topic prefix. The last "/" is appended automatically.
*/
void set_topic_prefix(std::string topic_prefix);
/// Get the topic prefix of this device, using default if necessary
const std::string &get_topic_prefix() const;
/// Manually set the topic used for logging.
void set_log_message_template(MQTTMessage &&message);
void set_log_level(int level);
/// Get the topic used for logging. Defaults to "<topic_prefix>/debug" and the value is cached for speed.
void disable_log_message();
bool is_log_message_enabled() const;
/** Subscribe to an MQTT topic and call callback when a message is received.
*
* @param topic The topic. Wildcards are currently not supported.
* @param callback The callback function.
* @param qos The QoS of this subscription.
*/
void subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos = 0);
/** Subscribe to a MQTT topic and automatically parse JSON payload.
*
* If an invalid JSON payload is received, the callback will not be called.
*
* @param topic The topic. Wildcards are currently not supported.
* @param callback The callback with a parsed JsonObject that will be called when a message with matching topic is
* received.
* @param qos The QoS of this subscription.
*/
void subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos = 0);
/** Publish a MQTTMessage
*
* @param message The message.
*/
bool publish(const MQTTMessage &message);
/** Publish a MQTT message
*
* @param topic The topic.
* @param payload The payload.
* @param retain Whether to retain the message.
*/
bool publish(const std::string &topic, const std::string &payload, uint8_t qos = 0, bool retain = false);
bool publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos = 0,
bool retain = false);
/** Construct and send a JSON MQTT message.
*
* @param topic The topic.
* @param f The Json Message builder.
* @param retain Whether to retain the message.
*/
bool publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos = 0, bool retain = false);
/// Setup the MQTT client, registering a bunch of callbacks and attempting to connect.
void setup() override;
void dump_config() override;
/// Reconnect if required
void loop() override;
/// MQTT client setup priority
float get_setup_priority() const override;
void on_message(const std::string &topic, const std::string &payload);
bool can_proceed() override;
void check_connected();
void set_reboot_timeout(uint32_t reboot_timeout);
void register_mqtt_component(MQTTComponent *component);
bool is_connected();
void on_shutdown() override;
void set_broker_address(const std::string &address) { this->credentials_.address = address; }
void set_broker_port(uint16_t port) { this->credentials_.port = port; }
void set_username(const std::string &username) { this->credentials_.username = username; }
void set_password(const std::string &password) { this->credentials_.password = password; }
void set_client_id(const std::string &client_id) { this->credentials_.client_id = client_id; }
protected:
/// Reconnect to the MQTT broker if not already connected.
void start_connect_();
void start_dnslookup_();
void check_dnslookup_();
#if defined(ARDUINO_ARCH_ESP8266) && LWIP_VERSION_MAJOR == 1
static void dns_found_callback(const char *name, ip_addr_t *ipaddr, void *callback_arg);
#else
static void dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg);
#endif
/// Re-calculate the availability property.
void recalculate_availability_();
bool subscribe_(const char *topic, uint8_t qos);
void resubscribe_subscription_(MQTTSubscription *sub);
void resubscribe_subscriptions_();
MQTTCredentials credentials_;
/// The last will message. Disabled optional denotes it being default and
/// an empty topic denotes the the feature being disabled.
MQTTMessage last_will_;
/// The birth message (e.g. the message that's send on an established connection.
/// See last_will_ for what different values denote.
MQTTMessage birth_message_;
bool sent_birth_message_{false};
MQTTMessage shutdown_message_;
/// Caches availability.
Availability availability_{};
/// The discovery info options for Home Assistant. Undefined optional means
/// default and empty prefix means disabled.
MQTTDiscoveryInfo discovery_info_{
.prefix = "homeassistant",
.retain = true,
.clean = false,
};
std::string topic_prefix_{};
MQTTMessage log_message_;
int log_level_{ESPHOME_LOG_LEVEL};
std::vector<MQTTSubscription> subscriptions_;
AsyncMqttClient mqtt_client_;
MQTTClientState state_{MQTT_CLIENT_DISCONNECTED};
IPAddress ip_;
bool dns_resolved_{false};
bool dns_resolve_error_{false};
std::vector<MQTTComponent *> children_;
uint32_t reboot_timeout_{300000};
uint32_t connect_begin_;
uint32_t last_connected_{0};
optional<AsyncMqttClientDisconnectReason> disconnect_reason_{};
};
extern MQTTClientComponent *global_mqtt_client;
class MQTTMessageTrigger : public Trigger<std::string>, public Component {
public:
explicit MQTTMessageTrigger(const std::string &topic);
void set_qos(uint8_t qos);
void set_payload(const std::string &payload);
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
std::string topic_;
uint8_t qos_{0};
optional<std::string> payload_;
};
class MQTTJsonMessageTrigger : public Trigger<const JsonObject &> {
public:
explicit MQTTJsonMessageTrigger(const std::string &topic, uint8_t qos) {
global_mqtt_client->subscribe_json(
topic, [this](const std::string &topic, JsonObject &root) { this->trigger(root); }, qos);
}
};
template<typename... Ts> class MQTTPublishAction : public Action<Ts...> {
public:
MQTTPublishAction(MQTTClientComponent *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(std::string, topic)
TEMPLATABLE_VALUE(std::string, payload)
TEMPLATABLE_VALUE(uint8_t, qos)
TEMPLATABLE_VALUE(bool, retain)
void play(Ts... x) override {
this->parent_->publish(this->topic_.value(x...), this->payload_.value(x...), this->qos_.value(x...),
this->retain_.value(x...));
this->play_next(x...);
}
protected:
MQTTClientComponent *parent_;
};
template<typename... Ts> class MQTTPublishJsonAction : public Action<Ts...> {
public:
MQTTPublishJsonAction(MQTTClientComponent *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(std::string, topic)
TEMPLATABLE_VALUE(uint8_t, qos)
TEMPLATABLE_VALUE(bool, retain)
void set_payload(std::function<void(Ts..., JsonObject &)> payload) { this->payload_ = payload; }
void play(Ts... x) override {
auto f = std::bind(&MQTTPublishJsonAction<Ts...>::encode_, this, x..., std::placeholders::_1);
auto topic = this->topic_.value(x...);
auto qos = this->qos_.value(x...);
auto retain = this->retain_.value(x...);
this->parent_->publish_json(topic, f, qos, retain);
this->play_next(x...);
}
protected:
void encode_(Ts... x, JsonObject &root) { this->payload_(x..., root); }
std::function<void(Ts..., JsonObject &)> payload_;
MQTTClientComponent *parent_;
};
template<typename... Ts> class MQTTConnectedCondition : public Condition<Ts...> {
public:
MQTTConnectedCondition(MQTTClientComponent *parent) : parent_(parent) {}
bool check(Ts... x) override { return this->parent_->is_connected(); }
protected:
MQTTClientComponent *parent_;
};
} // namespace mqtt
} // namespace esphome
+177
View File
@@ -0,0 +1,177 @@
#include "mqtt_climate.h"
#include "esphome/core/log.h"
#ifdef USE_CLIMATE
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt.climate";
using namespace esphome::climate;
void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) {
auto traits = this->device_->get_traits();
// current_temperature_topic
if (traits.get_supports_current_temperature()) {
root["current_temperature_topic"] = this->get_current_temperature_state_topic();
}
// mode_command_topic
root["mode_command_topic"] = this->get_mode_command_topic();
// mode_state_topic
root["mode_state_topic"] = this->get_mode_state_topic();
// modes
JsonArray &modes = root.createNestedArray("modes");
// sort array for nice UI in HA
if (traits.supports_mode(CLIMATE_MODE_AUTO))
modes.add("auto");
modes.add("off");
if (traits.supports_mode(CLIMATE_MODE_COOL))
modes.add("cool");
if (traits.supports_mode(CLIMATE_MODE_HEAT))
modes.add("heat");
if (traits.get_supports_two_point_target_temperature()) {
// temperature_low_command_topic
root["temperature_low_command_topic"] = this->get_target_temperature_low_command_topic();
// temperature_low_state_topic
root["temperature_low_state_topic"] = this->get_target_temperature_low_state_topic();
// temperature_high_command_topic
root["temperature_high_command_topic"] = this->get_target_temperature_high_command_topic();
// temperature_high_state_topic
root["temperature_high_state_topic"] = this->get_target_temperature_high_state_topic();
} else {
// temperature_command_topic
root["temperature_command_topic"] = this->get_target_temperature_command_topic();
// temperature_state_topic
root["temperature_state_topic"] = this->get_target_temperature_state_topic();
}
// min_temp
root["min_temp"] = traits.get_visual_min_temperature();
// max_temp
root["max_temp"] = traits.get_visual_max_temperature();
// temp_step
root["temp_step"] = traits.get_visual_temperature_step();
if (traits.get_supports_away()) {
// away_mode_command_topic
root["away_mode_command_topic"] = this->get_away_command_topic();
// away_mode_state_topic
root["away_mode_state_topic"] = this->get_away_state_topic();
}
}
void MQTTClimateComponent::setup() {
auto traits = this->device_->get_traits();
this->subscribe(this->get_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto call = this->device_->make_call();
call.set_mode(payload);
call.perform();
});
if (traits.get_supports_two_point_target_temperature()) {
this->subscribe(this->get_target_temperature_low_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_float(payload);
if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
return;
}
auto call = this->device_->make_call();
call.set_target_temperature_low(*val);
call.perform();
});
this->subscribe(this->get_target_temperature_high_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_float(payload);
if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
return;
}
auto call = this->device_->make_call();
call.set_target_temperature_high(*val);
call.perform();
});
} else {
this->subscribe(this->get_target_temperature_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_float(payload);
if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
return;
}
auto call = this->device_->make_call();
call.set_target_temperature(*val);
call.perform();
});
}
if (traits.get_supports_away()) {
this->subscribe(this->get_away_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto onoff = parse_on_off(payload.c_str());
auto call = this->device_->make_call();
switch (onoff) {
case PARSE_ON:
call.set_away(true);
break;
case PARSE_OFF:
call.set_away(false);
break;
case PARSE_TOGGLE:
call.set_away(!this->device_->away);
break;
case PARSE_NONE:
default:
ESP_LOGW(TAG, "Unknown payload '%s'", payload.c_str());
return;
}
call.perform();
});
}
this->device_->add_on_state_callback([this]() { this->publish_state_(); });
}
MQTTClimateComponent::MQTTClimateComponent(Climate *device) : device_(device) {}
bool MQTTClimateComponent::send_initial_state() { return this->publish_state_(); }
bool MQTTClimateComponent::is_internal() { return this->device_->is_internal(); }
std::string MQTTClimateComponent::component_type() const { return "climate"; }
std::string MQTTClimateComponent::friendly_name() const { return this->device_->get_name(); }
bool MQTTClimateComponent::publish_state_() {
auto traits = this->device_->get_traits();
// mode
const char *mode_s = climate_mode_to_string(this->device_->mode);
bool success = true;
if (!this->publish(this->get_mode_state_topic(), mode_s))
success = false;
int8_t accuracy = traits.get_temperature_accuracy_decimals();
if (traits.get_supports_current_temperature()) {
std::string payload = value_accuracy_to_string(this->device_->current_temperature, accuracy);
if (!this->publish(this->get_current_temperature_state_topic(), payload))
success = false;
}
if (traits.get_supports_two_point_target_temperature()) {
std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, accuracy);
if (!this->publish(this->get_target_temperature_low_state_topic(), payload))
success = false;
payload = value_accuracy_to_string(this->device_->target_temperature_high, accuracy);
if (!this->publish(this->get_target_temperature_high_state_topic(), payload))
success = false;
} else {
std::string payload = value_accuracy_to_string(this->device_->target_temperature, accuracy);
if (!this->publish(this->get_target_temperature_state_topic(), payload))
success = false;
}
if (traits.get_supports_away()) {
std::string payload = ONOFF(this->device_->away);
if (!this->publish(this->get_away_state_topic(), payload))
success = false;
}
return success;
}
} // namespace mqtt
} // namespace esphome
#endif
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTClimateComponent : public mqtt::MQTTComponent {
public:
MQTTClimateComponent(climate::Climate *device);
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override;
bool send_initial_state() override;
bool is_internal() override;
std::string component_type() const override;
void setup() override;
MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state)
MQTT_COMPONENT_CUSTOM_TOPIC(mode, state)
MQTT_COMPONENT_CUSTOM_TOPIC(mode, command)
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature, state)
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature, command)
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_low, state)
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_low, command)
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, state)
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, command)
MQTT_COMPONENT_CUSTOM_TOPIC(away, state)
MQTT_COMPONENT_CUSTOM_TOPIC(away, command)
protected:
std::string friendly_name() const override;
bool publish_state_();
climate::Climate *device_;
};
} // namespace mqtt
} // namespace esphome
#endif
+199
View File
@@ -0,0 +1,199 @@
#include "mqtt_component.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt.component";
void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
std::string sanitized_name = sanitize_string_whitelist(App.get_name(), HOSTNAME_CHARACTER_WHITELIST);
return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" +
this->get_default_object_id_() + "/config";
}
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
return global_mqtt_client->get_topic_prefix() + "/" + this->component_type() + "/" + this->get_default_object_id_() +
"/" + suffix;
}
const std::string MQTTComponent::get_state_topic_() const {
if (this->custom_state_topic_.empty())
return this->get_default_topic_for_("state");
return this->custom_state_topic_;
}
const std::string MQTTComponent::get_command_topic_() const {
if (this->custom_command_topic_.empty())
return this->get_default_topic_for_("command");
return this->custom_command_topic_;
}
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
if (topic.empty())
return false;
return global_mqtt_client->publish(topic, payload, 0, this->retain_);
}
bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
if (topic.empty())
return false;
return global_mqtt_client->publish_json(topic, f, 0, this->retain_);
}
bool MQTTComponent::send_discovery_() {
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
if (discovery_info.clean) {
ESP_LOGV(TAG, "'%s': Cleaning discovery...", this->friendly_name().c_str());
return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, 0, true);
}
ESP_LOGV(TAG, "'%s': Sending discovery...", this->friendly_name().c_str());
return global_mqtt_client->publish_json(
this->get_discovery_topic_(discovery_info),
[this](JsonObject &root) {
SendDiscoveryConfig config;
config.state_topic = true;
config.command_topic = true;
this->send_discovery(root, config);
std::string name = this->friendly_name();
root["name"] = name;
if (config.state_topic)
root["state_topic"] = this->get_state_topic_();
if (config.command_topic)
root["command_topic"] = this->get_command_topic_();
if (this->availability_ == nullptr) {
root["availability_topic"] = global_mqtt_client->get_availability().topic;
if (global_mqtt_client->get_availability().payload_available != "online")
root["payload_available"] = global_mqtt_client->get_availability().payload_available;
if (global_mqtt_client->get_availability().payload_not_available != "offline")
root["payload_not_available"] = global_mqtt_client->get_availability().payload_not_available;
} else if (!this->availability_->topic.empty()) {
root["availability_topic"] = this->availability_->topic;
if (this->availability_->payload_available != "online")
root["payload_available"] = this->availability_->payload_available;
if (this->availability_->payload_not_available != "offline")
root["payload_not_available"] = this->availability_->payload_not_available;
}
const std::string &node_name = App.get_name();
std::string unique_id = this->unique_id();
if (!unique_id.empty()) {
root["unique_id"] = unique_id;
} else {
// default to almost-unique ID. It's a hack but the only way to get that
// gorgeous device registry view.
root["unique_id"] = "ESP" + this->component_type() + this->get_default_object_id_();
}
JsonObject &device_info = root.createNestedObject("device");
device_info["identifiers"] = get_mac_address();
device_info["name"] = node_name;
device_info["sw_version"] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time();
#ifdef ARDUINO_BOARD
device_info["model"] = ARDUINO_BOARD;
#endif
device_info["manufacturer"] = "espressif";
},
0, discovery_info.retain);
}
bool MQTTComponent::get_retain() const { return this->retain_; }
bool MQTTComponent::is_discovery_enabled() const {
return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled();
}
std::string MQTTComponent::get_default_object_id_() const {
return sanitize_string_whitelist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_WHITELIST);
}
void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
global_mqtt_client->subscribe(topic, std::move(callback), qos);
}
void MQTTComponent::subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos) {
global_mqtt_client->subscribe_json(topic, std::move(callback), qos);
}
MQTTComponent::MQTTComponent() = default;
float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; }
void MQTTComponent::set_custom_state_topic(const std::string &custom_state_topic) {
this->custom_state_topic_ = custom_state_topic;
}
void MQTTComponent::set_custom_command_topic(const std::string &custom_command_topic) {
this->custom_command_topic_ = custom_command_topic;
}
void MQTTComponent::set_availability(std::string topic, std::string payload_available,
std::string payload_not_available) {
delete this->availability_;
this->availability_ = new Availability();
this->availability_->topic = std::move(topic);
this->availability_->payload_available = std::move(payload_available);
this->availability_->payload_not_available = std::move(payload_not_available);
}
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
void MQTTComponent::call_setup() {
// Call component internal setup.
this->setup_internal_();
if (this->is_internal())
return;
this->setup();
global_mqtt_client->register_mqtt_component(this);
if (!this->is_connected_())
return;
if (this->is_discovery_enabled()) {
if (!this->send_discovery_()) {
this->schedule_resend_state();
}
}
if (!this->send_initial_state()) {
this->schedule_resend_state();
}
}
void MQTTComponent::call_loop() {
this->loop_internal_();
if (this->is_internal())
return;
this->loop();
if (!this->resend_state_ || !this->is_connected_()) {
return;
}
this->resend_state_ = false;
if (this->is_discovery_enabled()) {
if (!this->send_discovery_()) {
this->schedule_resend_state();
}
}
if (!this->send_initial_state()) {
this->schedule_resend_state();
}
}
void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; }
std::string MQTTComponent::unique_id() { return ""; }
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }
} // namespace mqtt
} // namespace esphome
+179
View File
@@ -0,0 +1,179 @@
#pragma once
#include "esphome/core/component.h"
#include "mqtt_client.h"
namespace esphome {
namespace mqtt {
/// Simple Helper struct used for Home Assistant MQTT send_discovery().
struct SendDiscoveryConfig {
bool state_topic{true}; ///< If the state topic should be included. Defaults to true.
bool command_topic{true}; ///< If the command topic should be included. Default to true.
};
#define LOG_MQTT_COMPONENT(state_topic, command_topic) \
if (state_topic) { \
ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \
} \
if (command_topic) { \
ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \
}
#define MQTT_COMPONENT_CUSTOM_TOPIC_(name, type) \
protected: \
std::string custom_##name##_##type##_topic_{}; \
\
public: \
void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \
const std::string get_##name##_##type##_topic() const { \
if (this->custom_##name##_##type##_topic_.empty()) \
return this->get_default_topic_for_(#name "/" #type); \
return this->custom_##name##_##type##_topic_; \
}
#define MQTT_COMPONENT_CUSTOM_TOPIC(name, type) MQTT_COMPONENT_CUSTOM_TOPIC_(name, type)
/** MQTTComponent is the base class for all components that interact with MQTT to expose
* certain functionality or data from actuators or sensors to clients.
*
* Although this class should work with all MQTT solutions, it has been specifically designed for use
* with Home Assistant. For example, this class supports Home Assistant MQTT discovery out of the box.
*
* In order to implement automatic Home Assistant discovery, all sub-classes should:
*
* 1. Implement send_discovery that creates a Home Assistant discovery payload.
* 2. Override component_type() to return the appropriate component type such as "light" or "sensor".
* 3. Subscribe to command topics using subscribe() or subscribe_json() during setup().
*
* In order to best separate the front- and back-end of ESPHome, all sub-classes should
* only parse/send MQTT messages and interact with back-end components via callbacks to ensure
* a clean separation.
*/
class MQTTComponent : public Component {
public:
/// Constructs a MQTTComponent.
explicit MQTTComponent();
/// Override setup_ so that we can call send_discovery() when needed.
void call_setup() override;
void call_loop() override;
/// Send discovery info the Home Assistant, override this.
virtual void send_discovery(JsonObject &root, SendDiscoveryConfig &config) = 0;
virtual bool send_initial_state() = 0;
virtual bool is_internal() = 0;
/// Set whether state message should be retained.
void set_retain(bool retain);
bool get_retain() const;
/// Disable discovery. Sets friendly name to "".
void disable_discovery();
bool is_discovery_enabled() const;
/// Override this method to return the component type (e.g. "light", "sensor", ...)
virtual std::string component_type() const = 0;
/// Set a custom state topic. Set to "" for default behavior.
void set_custom_state_topic(const std::string &custom_state_topic);
/// Set a custom command topic. Set to "" for default behavior.
void set_custom_command_topic(const std::string &custom_command_topic);
/// MQTT_COMPONENT setup priority.
float get_setup_priority() const override;
/** Set the Home Assistant availability data.
*
* See See <a href="https://www.home-assistant.io/components/binary_sensor.mqtt/">Home Assistant</a> for more info.
*/
void set_availability(std::string topic, std::string payload_available, std::string payload_not_available);
void disable_availability();
/// Internal method for the MQTT client base to schedule a resend of the state on reconnect.
void schedule_resend_state();
/** Send a MQTT message.
*
* @param topic The topic.
* @param payload The payload.
*/
bool publish(const std::string &topic, const std::string &payload);
/** Construct and send a JSON MQTT message.
*
* @param topic The topic.
* @param f The Json Message builder.
*/
bool publish_json(const std::string &topic, const json::json_build_t &f);
/** Subscribe to a MQTT topic.
*
* @param topic The topic. Wildcards are currently not supported.
* @param callback The callback that will be called when a message with matching topic is received.
* @param qos The MQTT quality of service. Defaults to 0.
*/
void subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos = 0);
/** Subscribe to a MQTT topic and automatically parse JSON payload.
*
* If an invalid JSON payload is received, the callback will not be called.
*
* @param topic The topic. Wildcards are currently not supported.
* @param callback The callback with a parsed JsonObject that will be called when a message with matching topic is
* received.
* @param qos The MQTT quality of service. Defaults to 0.
*/
void subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos = 0);
protected:
/// Helper method to get the discovery topic for this component.
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
/** Get this components state/command/... topic.
*
* @param suffix The suffix/key such as "state" or "command".
* @return The full topic.
*/
std::string get_default_topic_for_(const std::string &suffix) const;
/// Get the friendly name of this MQTT component.
virtual std::string friendly_name() const = 0;
/** A unique ID for this MQTT component, empty for no unique id. See unique ID requirements:
* https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements
*
* @return The unique id as a string.
*/
virtual std::string unique_id();
/// Get the MQTT topic that new states will be shared to.
const std::string get_state_topic_() const;
/// Get the MQTT topic for listening to commands.
const std::string get_command_topic_() const;
bool is_connected_() const;
/// Internal method to start sending discovery info, this will call send_discovery().
bool send_discovery_();
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name.
std::string get_default_object_id_() const;
protected:
std::string custom_state_topic_{};
std::string custom_command_topic_{};
bool retain_{true};
bool discovery_enabled_{true};
Availability *availability_{nullptr};
bool resend_state_{false};
};
} // namespace mqtt
} // namespace esphome
+111
View File
@@ -0,0 +1,111 @@
#include "mqtt_cover.h"
#include "esphome/core/log.h"
#ifdef USE_COVER
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt.cover";
using namespace esphome::cover;
MQTTCoverComponent::MQTTCoverComponent(Cover *cover) : cover_(cover) {}
void MQTTCoverComponent::setup() {
auto traits = this->cover_->get_traits();
this->cover_->add_on_state_callback([this]() { this->publish_state(); });
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
auto call = this->cover_->make_call();
call.set_command(payload.c_str());
call.perform();
});
if (traits.get_supports_position()) {
this->subscribe(this->get_position_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto value = parse_float(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str());
return;
}
auto call = this->cover_->make_call();
call.set_position(*value / 100.0f);
call.perform();
});
}
if (traits.get_supports_tilt()) {
this->subscribe(this->get_tilt_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto value = parse_float(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str());
return;
}
auto call = this->cover_->make_call();
call.set_tilt(*value / 100.0f);
call.perform();
});
}
}
void MQTTCoverComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
auto traits = this->cover_->get_traits();
// no state topic for position
bool state_topic = !traits.get_supports_position();
LOG_MQTT_COMPONENT(state_topic, true)
if (!state_topic) {
ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic().c_str());
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic().c_str());
}
if (traits.get_supports_tilt()) {
ESP_LOGCONFIG(TAG, " Tilt State Topic: '%s'", this->get_tilt_state_topic().c_str());
ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic().c_str());
}
}
void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) {
auto traits = this->cover_->get_traits();
if (traits.get_is_assumed_state()) {
root["optimistic"] = true;
}
if (traits.get_supports_position()) {
root["position_topic"] = this->get_position_state_topic();
root["set_position_topic"] = this->get_position_command_topic();
}
if (traits.get_supports_tilt()) {
root["tilt_status_topic"] = this->get_tilt_state_topic();
root["tilt_command_topic"] = this->get_tilt_command_topic();
}
}
std::string MQTTCoverComponent::component_type() const { return "cover"; }
std::string MQTTCoverComponent::friendly_name() const { return this->cover_->get_name(); }
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
bool MQTTCoverComponent::is_internal() { return this->cover_->is_internal(); }
bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits();
bool success = true;
if (!traits.get_supports_position()) {
const char *state_s = "unknown";
if (this->cover_->position == COVER_OPEN) {
state_s = "open";
} else if (this->cover_->position == COVER_CLOSED) {
state_s = "closed";
}
if (!this->publish(this->get_state_topic_(), state_s))
success = false;
} else {
std::string pos = value_accuracy_to_string(roundf(this->cover_->position * 100), 0);
if (!this->publish(this->get_position_state_topic(), pos))
success = false;
}
if (traits.get_supports_tilt()) {
std::string pos = value_accuracy_to_string(roundf(this->cover_->tilt * 100), 0);
if (!this->publish(this->get_tilt_state_topic(), pos))
success = false;
}
return success;
}
} // namespace mqtt
} // namespace esphome
#endif
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include "esphome/core/defines.h"
#include "mqtt_component.h"
#ifdef USE_COVER
#include "esphome/components/cover/cover.h"
namespace esphome {
namespace mqtt {
class MQTTCoverComponent : public mqtt::MQTTComponent {
public:
explicit MQTTCoverComponent(cover::Cover *cover);
void setup() override;
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override;
MQTT_COMPONENT_CUSTOM_TOPIC(position, command)
MQTT_COMPONENT_CUSTOM_TOPIC(position, state)
MQTT_COMPONENT_CUSTOM_TOPIC(tilt, command)
MQTT_COMPONENT_CUSTOM_TOPIC(tilt, state)
bool send_initial_state() override;
bool is_internal() override;
bool publish_state();
void dump_config() override;
protected:
std::string component_type() const override;
std::string friendly_name() const override;
cover::Cover *cover_;
};
} // namespace mqtt
} // namespace esphome
#endif
+124
View File
@@ -0,0 +1,124 @@
#include "mqtt_fan.h"
#include "esphome/core/log.h"
#ifdef USE_FAN
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt.fan";
using namespace esphome::fan;
MQTTFanComponent::MQTTFanComponent(FanState *state) : MQTTComponent(), state_(state) {}
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());
switch (val) {
case PARSE_ON:
ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name().c_str());
this->state_->turn_on().perform();
break;
case PARSE_OFF:
ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name().c_str());
this->state_->turn_off().perform();
break;
case PARSE_TOGGLE:
ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name().c_str());
this->state_->toggle().perform();
break;
case PARSE_NONE:
default:
ESP_LOGW(TAG, "Unknown state payload %s", payload.c_str());
this->status_momentary_warning("state", 5000);
break;
}
});
if (this->state_->get_traits().supports_oscillation()) {
this->subscribe(this->get_oscillation_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off");
switch (val) {
case PARSE_ON:
ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name().c_str());
this->state_->make_call().set_oscillating(true).perform();
break;
case PARSE_OFF:
ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name().c_str());
this->state_->make_call().set_oscillating(false).perform();
break;
case PARSE_TOGGLE:
this->state_->make_call().set_oscillating(!this->state_->oscillating).perform();
break;
case PARSE_NONE:
ESP_LOGW(TAG, "Unknown Oscillation Payload %s", payload.c_str());
this->status_momentary_warning("oscillation", 5000);
break;
}
});
}
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().set_speed(payload.c_str()).perform();
});
}
auto f = std::bind(&MQTTFanComponent::publish_state, this);
this->state_->add_on_state_callback([this, f]() { this->defer("send", f); });
}
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) {
if (this->state_->get_traits().supports_oscillation()) {
root["oscillation_command_topic"] = this->get_oscillation_command_topic();
root["oscillation_state_topic"] = this->get_oscillation_state_topic();
}
if (this->state_->get_traits().supports_speed()) {
root["speed_command_topic"] = this->get_speed_command_topic();
root["speed_state_topic"] = this->get_speed_state_topic();
}
}
bool MQTTFanComponent::is_internal() { return this->state_->is_internal(); }
bool MQTTFanComponent::publish_state() {
const char *state_s = this->state_->state ? "ON" : "OFF";
ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s);
this->publish(this->get_state_topic_(), state_s);
bool failed = false;
if (this->state_->get_traits().supports_oscillation()) {
bool success = this->publish(this->get_oscillation_state_topic(),
this->state_->oscillating ? "oscillate_on" : "oscillate_off");
failed = failed || !success;
}
if (this->state_->get_traits().supports_speed()) {
const char *payload;
switch (this->state_->speed) {
case FAN_SPEED_LOW: {
payload = "low";
break;
}
case FAN_SPEED_MEDIUM: {
payload = "medium";
break;
}
default:
case FAN_SPEED_HIGH: {
payload = "high";
break;
}
}
bool success = this->publish(this->get_speed_state_topic(), payload);
failed = failed || !success;
}
return !failed;
}
} // namespace mqtt
} // namespace esphome
#endif
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_FAN
#include "esphome/components/fan/fan_state.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTFanComponent : public mqtt::MQTTComponent {
public:
explicit MQTTFanComponent(fan::FanState *state);
MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, command)
MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, state)
MQTT_COMPONENT_CUSTOM_TOPIC(speed, command)
MQTT_COMPONENT_CUSTOM_TOPIC(speed, state)
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override;
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Setup the fan subscriptions and discovery.
void setup() override;
/// Send the full current state to MQTT.
bool send_initial_state() override;
bool publish_state();
/// 'fan' component type for discovery.
std::string component_type() const override;
fan::FanState *get_state() const;
bool is_internal() override;
protected:
std::string friendly_name() const override;
fan::FanState *state_;
};
} // namespace mqtt
} // namespace esphome
#endif
+60
View File
@@ -0,0 +1,60 @@
#include "mqtt_light.h"
#include "esphome/core/log.h"
#ifdef USE_LIGHT
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt.light";
using namespace esphome::light;
std::string MQTTJSONLightComponent::component_type() const { return "light"; }
void MQTTJSONLightComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject &root) {
this->state_->make_call().parse_json(root).perform();
});
auto f = std::bind(&MQTTJSONLightComponent::publish_state_, this);
this->state_->add_new_remote_values_callback([this, f]() { this->defer("send", f); });
}
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : MQTTComponent(), state_(state) {}
bool MQTTJSONLightComponent::publish_state_() {
return this->publish_json(this->get_state_topic_(), [this](JsonObject &root) { this->state_->dump_json(root); });
}
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
std::string MQTTJSONLightComponent::friendly_name() const { return this->state_->get_name(); }
void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) {
root["schema"] = "json";
auto traits = this->state_->get_traits();
if (traits.get_supports_brightness())
root["brightness"] = true;
if (traits.get_supports_rgb())
root["rgb"] = true;
if (traits.get_supports_color_temperature())
root["color_temp"] = true;
if (traits.get_supports_rgb_white_value())
root["white_value"] = true;
if (this->state_->supports_effects()) {
root["effect"] = true;
JsonArray &effect_list = root.createNestedArray("effect_list");
for (auto *effect : this->state_->get_effects())
effect_list.add(effect->get_name());
effect_list.add("None");
}
}
bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); }
bool MQTTJSONLightComponent::is_internal() { return this->state_->is_internal(); }
void MQTTJSONLightComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Light '%s':", this->state_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
}
} // namespace mqtt
} // namespace esphome
#endif
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_LIGHT
#include "mqtt_component.h"
#include "esphome/components/light/light_state.h"
namespace esphome {
namespace mqtt {
class MQTTJSONLightComponent : public mqtt::MQTTComponent {
public:
explicit MQTTJSONLightComponent(light::LightState *state);
light::LightState *get_state() const;
void setup() override;
void dump_config() override;
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override;
bool send_initial_state() override;
bool is_internal() override;
protected:
std::string friendly_name() const override;
std::string component_type() const override;
bool publish_state_();
light::LightState *state_;
};
} // namespace mqtt
} // namespace esphome
#endif
+77
View File
@@ -0,0 +1,77 @@
#include "mqtt_sensor.h"
#include "esphome/core/log.h"
#ifdef USE_SENSOR
#ifdef USE_DEEP_SLEEP
#include "esphome/components/deep_sleep/deep_sleep_component.h"
#endif
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt.sensor";
using namespace esphome::sensor;
MQTTSensorComponent::MQTTSensorComponent(Sensor *sensor) : MQTTComponent(), sensor_(sensor) {}
void MQTTSensorComponent::setup() {
this->sensor_->add_on_state_callback([this](float state) { this->publish_state(state); });
}
void MQTTSensorComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Sensor '%s':", this->sensor_->get_name().c_str());
if (this->get_expire_after() > 0) {
ESP_LOGCONFIG(TAG, " Expire After: %us", this->get_expire_after() / 1000);
}
LOG_MQTT_COMPONENT(true, false)
}
std::string MQTTSensorComponent::component_type() const { return "sensor"; }
uint32_t MQTTSensorComponent::get_expire_after() const {
if (this->expire_after_.has_value()) {
return *this->expire_after_;
} else {
#ifdef USE_DEEP_SLEEP
if (deep_sleep::global_has_deep_sleep) {
return 0;
}
#endif
return this->sensor_->calculate_expected_filter_update_interval() * 5;
}
}
void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; }
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
std::string MQTTSensorComponent::friendly_name() const { return this->sensor_->get_name(); }
void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) {
if (!this->sensor_->get_unit_of_measurement().empty())
root["unit_of_measurement"] = this->sensor_->get_unit_of_measurement();
if (this->get_expire_after() > 0)
root["expire_after"] = this->get_expire_after() / 1000;
if (!this->sensor_->get_icon().empty())
root["icon"] = this->sensor_->get_icon();
config.command_topic = false;
}
bool MQTTSensorComponent::send_initial_state() {
if (this->sensor_->has_state()) {
return this->publish_state(this->sensor_->state);
} else {
return true;
}
}
bool MQTTSensorComponent::is_internal() { return this->sensor_->is_internal(); }
bool MQTTSensorComponent::publish_state(float value) {
int8_t accuracy = this->sensor_->get_accuracy_decimals();
return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy));
}
std::string MQTTSensorComponent::unique_id() { return this->sensor_->unique_id(); }
} // namespace mqtt
} // namespace esphome
#endif
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTSensorComponent : public mqtt::MQTTComponent {
public:
/** Construct this MQTTSensorComponent instance with the provided friendly_name and sensor
*
* Note the sensor is never stored and is only used for initializing some values of this class.
* If sensor is nullptr, then automatic initialization of these fields is disabled.
*
* @param sensor The sensor, this can be null to disable automatic setup.
*/
explicit MQTTSensorComponent(sensor::Sensor *sensor);
/// Setup an expiry, 0 disables it
void set_expire_after(uint32_t expire_after);
/// Disable Home Assistant value expiry.
void disable_expire_after();
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override;
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Override setup.
void setup() override;
void dump_config() override;
/// Get the expire_after in milliseconds used for Home Assistant discovery, first checks override.
uint32_t get_expire_after() const;
bool publish_state(float value);
bool send_initial_state() override;
bool is_internal() override;
protected:
/// Override for MQTTComponent, returns "sensor".
std::string component_type() const override;
std::string friendly_name() const override;
std::string unique_id() override;
sensor::Sensor *sensor_;
optional<uint32_t> expire_after_; // Override the expire after advertised to Home Assistant
};
} // namespace mqtt
} // namespace esphome
#endif
+60
View File
@@ -0,0 +1,60 @@
#include "mqtt_switch.h"
#include "esphome/core/log.h"
#ifdef USE_SWITCH
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt.switch";
using namespace esphome::switch_;
MQTTSwitchComponent::MQTTSwitchComponent(switch_::Switch *a_switch) : MQTTComponent(), switch_(a_switch) {}
void MQTTSwitchComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
switch (parse_on_off(payload.c_str())) {
case PARSE_ON:
this->switch_->turn_on();
break;
case PARSE_OFF:
this->switch_->turn_off();
break;
case PARSE_TOGGLE:
this->switch_->toggle();
break;
case PARSE_NONE:
default:
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str());
this->status_momentary_warning("state", 5000);
break;
}
});
this->switch_->add_on_state_callback(
[this](bool enabled) { this->defer("send", [this, enabled]() { this->publish_state(enabled); }); });
}
void MQTTSwitchComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Switch '%s': ", this->switch_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true);
}
std::string MQTTSwitchComponent::component_type() const { return "switch"; }
void MQTTSwitchComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) {
if (!this->switch_->get_icon().empty())
root["icon"] = this->switch_->get_icon();
if (this->switch_->assumed_state())
root["optimistic"] = true;
}
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
bool MQTTSwitchComponent::is_internal() { return this->switch_->is_internal(); }
std::string MQTTSwitchComponent::friendly_name() const { return this->switch_->get_name(); }
bool MQTTSwitchComponent::publish_state(bool state) {
const char *state_s = state ? "ON" : "OFF";
return this->publish(this->get_state_topic_(), state_s);
}
} // namespace mqtt
} // namespace esphome
#endif
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTSwitchComponent : public mqtt::MQTTComponent {
public:
explicit MQTTSwitchComponent(switch_::Switch *a_switch);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void setup() override;
void dump_config() override;
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override;
bool send_initial_state() override;
bool is_internal() override;
bool publish_state(bool state);
protected:
std::string friendly_name() const override;
/// "switch" component type.
std::string component_type() const override;
switch_::Switch *switch_;
};
} // namespace mqtt
} // namespace esphome
#endif
@@ -0,0 +1,47 @@
#include "mqtt_text_sensor.h"
#include "esphome/core/log.h"
#ifdef USE_TEXT_SENSOR
namespace esphome {
namespace mqtt {
static const char *TAG = "mqtt.text_sensor";
using namespace esphome::text_sensor;
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : MQTTComponent(), sensor_(sensor) {}
void MQTTTextSensor::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) {
if (!this->sensor_->get_icon().empty())
root["icon"] = this->sensor_->get_icon();
if (!this->sensor_->unique_id().empty())
root["unique_id"] = this->sensor_->unique_id();
config.command_topic = false;
}
void MQTTTextSensor::setup() {
this->sensor_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); });
}
void MQTTTextSensor::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Text Sensor '%s':", this->sensor_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false);
}
bool MQTTTextSensor::publish_state(const std::string &value) { return this->publish(this->get_state_topic_(), value); }
bool MQTTTextSensor::send_initial_state() {
if (this->sensor_->has_state()) {
return this->publish_state(this->sensor_->state);
} else {
return true;
}
}
bool MQTTTextSensor::is_internal() { return this->sensor_->is_internal(); }
std::string MQTTTextSensor::component_type() const { return "sensor"; }
std::string MQTTTextSensor::friendly_name() const { return this->sensor_->get_name(); }
} // namespace mqtt
} // namespace esphome
#endif
@@ -0,0 +1,40 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTTextSensor : public mqtt::MQTTComponent {
public:
explicit MQTTTextSensor(text_sensor::TextSensor *sensor);
void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override;
void setup() override;
void dump_config() override;
bool publish_state(const std::string &value);
bool send_initial_state() override;
bool is_internal() override;
protected:
std::string component_type() const override;
std::string friendly_name() const override;
text_sensor::TextSensor *sensor_;
};
} // namespace mqtt
} // namespace esphome
#endif