mirror of
https://github.com/Threnklyn/esphome-dev.git
synced 2026-06-13 16:13:32 +02:00
🏗 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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user