From fc465d6d93a82fac4fb6eb7986bd61fc2e001598 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 17 Jun 2019 15:13:52 -0300 Subject: [PATCH] SMS Sender / Receiver (#522) * add sim800l * Increse SoftwareSerial Buffer Size * use auto id on action * lint * lint * add to test3.yaml * lint Co-authored-by: Guillermo Ruffino --- esphome/components/sim800l/__init__.py | 59 ++++++ esphome/components/sim800l/sim800l.cpp | 259 +++++++++++++++++++++++++ esphome/components/sim800l/sim800l.h | 91 +++++++++ esphome/components/uart/uart.h | 2 +- tests/test3.yaml | 10 + 5 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 esphome/components/sim800l/__init__.py create mode 100644 esphome/components/sim800l/sim800l.cpp create mode 100644 esphome/components/sim800l/sim800l.h diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py new file mode 100644 index 0000000000..c64112570a --- /dev/null +++ b/esphome/components/sim800l/__init__.py @@ -0,0 +1,59 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.components import uart + +DEPENDENCIES = ['uart'] + +sim800l_ns = cg.esphome_ns.namespace('sim800l') +Sim800LComponent = sim800l_ns.class_('Sim800LComponent', cg.Component) + +Sim800LReceivedMessageTrigger = sim800l_ns.class_('Sim800LReceivedMessageTrigger', + automation.Trigger.template(cg.std_string, + cg.std_string)) + +# Actions +Sim800LSendSmsAction = sim800l_ns.class_('Sim800LSendSmsAction', automation.Action) + +MULTI_CONF = True + +CONF_ON_SMS_RECEIVED = 'on_sms_received' +CONF_RECIPIENT = 'recipient' +CONF_MESSAGE = 'message' + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(Sim800LComponent), + cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Sim800LReceivedMessageTrigger), + }), +}).extend(cv.polling_component_schema('5s')).extend(uart.UART_DEVICE_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_SMS_RECEIVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [(cg.std_string, 'message'), + (cg.std_string, 'sender')], conf) + + +SIM800L_SEND_SMS_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(Sim800LComponent), + cv.Required(CONF_RECIPIENT): cv.templatable(cv.string_strict), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), +}) + + +@automation.register_action('sim800l.send_sms', Sim800LSendSmsAction, SIM800L_SEND_SMS_SCHEMA) +def sim800l_send_sms_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_RECIPIENT], args, cg.std_string) + cg.add(var.set_recipient(template_)) + template_ = yield cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + yield var diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp new file mode 100644 index 0000000000..646f20833f --- /dev/null +++ b/esphome/components/sim800l/sim800l.cpp @@ -0,0 +1,259 @@ +#include "sim800l.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace sim800l { + +static const char* TAG = "sim800l"; + +const char ASCII_CR = 0x0D; +const char ASCII_LF = 0x0A; + +void Sim800LComponent::update() { + if (this->watch_dog_++ == 2) { + this->state_ = STATE_INIT; + this->write(26); + } + + if (state_ == STATE_INIT) { + if (this->registered_ && this->send_pending_) { + this->send_cmd_("AT+CSCS=\"GSM\""); + this->state_ = STATE_SENDINGSMS1; + } else { + this->send_cmd_("AT"); + this->state_ = STATE_CHECK_AT; + } + this->expect_ack_ = true; + } + if (state_ == STATE_RECEIVEDSMS) { + // Serial Buffer should have flushed. + // Send cmd to delete received sms + char delete_cmd[20]; + sprintf(delete_cmd, "AT+CMGD=%d", this->parse_index_); + this->send_cmd_(delete_cmd); + this->state_ = STATE_CHECK_SMS; + this->expect_ack_ = true; + } +} + +void Sim800LComponent::send_cmd_(std::string message) { + ESP_LOGV(TAG, "S: %s - %d", message.c_str(), this->state_); + this->watch_dog_ = 0; + this->write_str(message.c_str()); + this->write_byte(ASCII_LF); +} + +void Sim800LComponent::parse_cmd_(std::string message) { + ESP_LOGV(TAG, "R: %s - %d", message.c_str(), this->state_); + + if (message.empty()) + return; + + if (this->expect_ack_) { + bool ok = message == "OK"; + this->expect_ack_ = false; + if (!ok) { + if (this->state_ == STATE_CHECK_AT && message == "AT") { + // Expected ack but AT echo received + this->state_ = STATE_DISABLE_ECHO; + this->expect_ack_ = true; + } else { + ESP_LOGW(TAG, "Not ack. %d %s", this->state_, message.c_str()); + this->state_ = STATE_IDLE; // Let it timeout + return; + } + } + } + + switch (this->state_) { + case STATE_INIT: + if (message.compare(0, 6, "+CMTI:") == 0) { + // While we were waiting for update to check for messages, this notifies a message + // is available. Grab it quickly + this->state_ = STATE_CHECK_SMS; + } + break; + case STATE_DISABLE_ECHO: + send_cmd_("ATE0"); + this->state_ = STATE_CHECK_AT; + this->expect_ack_ = true; + break; + case STATE_CHECK_AT: + send_cmd_("AT+CMGF=1"); + this->state_ = STATE_CREG; + this->expect_ack_ = true; + break; + case STATE_CREG: + send_cmd_("AT+CREG?"); + this->state_ = STATE_CREGWAIT; + break; + case STATE_CREGWAIT: { + // Response: "+CREG: 0,1" -- the one there means registered ok + // "+CREG: -,-" means not registered ok + bool registered = message.compare(0, 6, "+CREG:") == 0 && message[9] == '1'; + if (registered) { + if (!this->registered_) + ESP_LOGD(TAG, "Registered OK"); + send_cmd_("AT+CSQ"); + this->state_ = STATE_CSQ; + this->expect_ack_ = true; + } else { + ESP_LOGW(TAG, "Registration Fail"); + if (message[7] == '0') { // Network registration is disable, enable it + send_cmd_("AT+CREG=1"); + this->expect_ack_ = true; + this->state_ = STATE_CHECK_AT; + } else { + // Keep waiting registration + this->state_ = STATE_CREG; + } + } + this->registered_ = registered; + break; + } + case STATE_CSQ: + this->state_ = STATE_CSQ_RESPONSE; + break; + case STATE_CSQ_RESPONSE: + if (message.compare(0, 5, "+CSQ:") == 0) { + size_t comma = message.find(',', 6); + if (comma != 6) { + this->rssi_ = strtol(message.substr(6, comma - 6).c_str(), nullptr, 10); + ESP_LOGD(TAG, "RSSI: %d", this->rssi_); + } + } + this->state_ = STATE_CHECK_SMS; + break; + case STATE_PARSE_SMS: + this->state_ = STATE_PARSE_SMS_RESPONSE; + break; + case STATE_PARSE_SMS_RESPONSE: + if (message.compare(0, 6, "+CMGL:") == 0 && this->parse_index_ == 0) { + size_t start = 7; + size_t end = message.find(',', start); + uint8_t item = 0; + while (end != start) { + item++; + if (item == 1) { // Slot Index + this->parse_index_ = strtol(message.substr(start, end - start).c_str(), nullptr, 10); + } + // item 2 = STATUS, usually "REC UNERAD" + if (item == 3) { // recipient + // Add 1 and remove 2 from substring to get rid of "quotes" + this->sender_ = message.substr(start + 1, end - start - 2); + break; + } + // item 4 = "" + // item 5 = Received timestamp + start = end + 1; + end = message.find(',', start); + } + + if (item < 2) { + ESP_LOGD(TAG, "Invalid message %d %s", this->state_, message.c_str()); + return; + } + this->state_ = STATE_RECEIVESMS; + } + // Otherwise we receive another OK, we do nothing just wait polling to continuously check for SMS + if (message == "OK") + this->state_ = STATE_INIT; + break; + case STATE_RECEIVESMS: + /* Our recipient is set and the message body is in message + kick ESPHome callback now + */ + ESP_LOGD(TAG, "Received SMS from: %s", this->sender_.c_str()); + ESP_LOGD(TAG, "%s", message.c_str()); + this->callback_.call(message, this->sender_); + /* If the message is multiline, next lines will contain message data. + If there were other messages in the list, next line will be +CMGL: ... + At the end of the list the new line and the OK should be received. + To keep this simple just first line of message if considered, then + the next state will swallow all received data and in next poll event + this message index is marked for deletion. + */ + this->state_ = STATE_RECEIVEDSMS; + break; + case STATE_RECEIVEDSMS: + // Let the buffer flush. Next poll will request to delete the parsed index message. + break; + case STATE_SENDINGSMS1: + this->send_cmd_("AT+CMGS=\"" + this->recipient_ + "\""); + this->state_ = STATE_SENDINGSMS2; + break; + case STATE_SENDINGSMS2: + if (message == ">") { + // Send sms body + ESP_LOGD(TAG, "Sending message: '%s'", this->outgoing_message_.c_str()); + this->write_str(this->outgoing_message_.c_str()); + this->write(26); + this->state_ = STATE_SENDINGSMS3; + } else { + this->registered_ = false; + this->state_ = STATE_INIT; + this->send_cmd_("AT+CMEE=2"); + this->write(26); + } + break; + case STATE_SENDINGSMS3: + if (message.compare(0, 6, "+CMGS:") == 0) { + ESP_LOGD(TAG, "SMS Sent OK: %s", message.c_str()); + this->send_pending_ = false; + this->state_ = STATE_CHECK_SMS; + this->expect_ack_ = true; + } + break; + default: + ESP_LOGD(TAG, "Unhandled: %s - %d", message.c_str(), this->state_); + break; + } + if (this->state_ == STATE_CHECK_SMS) { + send_cmd_("AT+CMGL=\"ALL\""); + this->state_ = STATE_PARSE_SMS; + this->parse_index_ = 0; + this->expect_ack_ = true; + } +} + +void Sim800LComponent::loop() { + // Read message + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == SIM800L_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte); // NOLINT + + if (byte == ASCII_CR) + continue; + if (byte >= 0x7F) + byte = '?'; // need to be valid utf8 string for log functions. + this->read_buffer_[this->read_pos_] = byte; + + if (this->state_ == STATE_SENDINGSMS2 && this->read_pos_ == 0 && byte == '>') + this->read_buffer_[++this->read_pos_] = ASCII_LF; + + if (this->read_buffer_[this->read_pos_] == ASCII_LF) { + this->read_buffer_[this->read_pos_] = 0; + this->read_pos_ = 0; + this->parse_cmd_(this->read_buffer_); + } else { + this->read_pos_++; + } + } +} + +void Sim800LComponent::send_sms(std::string recipient, std::string message) { + ESP_LOGD(TAG, "Sending to %s: %s", recipient.c_str(), message.c_str()); + this->recipient_ = recipient; + this->outgoing_message_ = message; + this->send_pending_ = true; + this->update(); +} + +} // namespace sim800l +} // namespace esphome diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h new file mode 100644 index 0000000000..17cd0111fe --- /dev/null +++ b/esphome/components/sim800l/sim800l.h @@ -0,0 +1,91 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +#define SIM800L_READ_BUFFER_LENGTH 255 + +namespace esphome { +namespace sim800l { + +enum State { + STATE_IDLE = 0, + STATE_INIT, + STATE_CHECK_AT, + STATE_CREG, + STATE_CREGWAIT, + STATE_CSQ, + STATE_CSQ_RESPONSE, + STATE_IDLEWAIT, + STATE_SENDINGSMS1, + STATE_SENDINGSMS2, + STATE_SENDINGSMS3, + STATE_CHECK_SMS, + STATE_PARSE_SMS, + STATE_PARSE_SMS_RESPONSE, + STATE_RECEIVESMS, + STATE_READSMS, + STATE_RECEIVEDSMS, + STATE_DELETEDSMS, + STATE_DISABLE_ECHO, + STATE_PARSE_SMS_OK +}; + +class Sim800LComponent : public uart::UARTDevice, public PollingComponent { + public: + /// Retrieve the latest sensor values. This operation takes approximately 16ms. + void update() override; + void loop() override; + void add_on_sms_received_callback(std::function callback) { + this->callback_.add(std::move(callback)); + } + void send_sms(std::string recipient, std::string message); + + protected: + void send_cmd_(std::string); + void parse_cmd_(std::string); + + std::string sender_; + char read_buffer_[SIM800L_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + uint8_t parse_index_{0}; + uint8_t watch_dog_{0}; + bool expect_ack_{false}; + sim800l::State state_{STATE_IDLE}; + bool registered_{false}; + int rssi_{0}; + + std::string recipient_; + std::string outgoing_message_; + bool send_pending_; + + CallbackManager callback_; +}; + +class Sim800LReceivedMessageTrigger : public Trigger { + public: + explicit Sim800LReceivedMessageTrigger(Sim800LComponent *parent) { + parent->add_on_sms_received_callback( + [this](std::string message, std::string sender) { this->trigger(message, sender); }); + } +}; + +template class Sim800LSendSmsAction : public Action { + public: + Sim800LSendSmsAction(Sim800LComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, recipient) + TEMPLATABLE_VALUE(std::string, message) + + void play(Ts... x) { + auto recipient = this->recipient_.value(x...); + auto message = this->message_.value(x...); + this->parent_->send_sms(recipient, message); + } + + protected: + Sim800LComponent *parent_; +}; + +} // namespace sim800l +} // namespace esphome diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 666b8e2fb2..f642d4ee81 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -30,7 +30,7 @@ class ESP8266SoftwareSerial { uint32_t bit_time_{0}; uint8_t *rx_buffer_{nullptr}; - size_t rx_buffer_size_{64}; + size_t rx_buffer_size_{512}; volatile size_t rx_in_pos_{0}; size_t rx_out_pos_{0}; ISRInternalGPIOPin *tx_pin_{nullptr}; diff --git a/tests/test3.yaml b/tests/test3.yaml index 8088b12d03..a37cc47f4a 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -465,3 +465,13 @@ ttp229_lsf: ttp229_bsf: sdo_pin: D0 scl_pin: D1 + +sim800l: + on_sms_received: + - lambda: |- + std::string str; + str = sender; + str = message; + - sim800l.send_sms: + message: 'hello you' + recipient: '+1234'