Triggering an action via cellular phone call (or SMS)

My lord, this has taken ages to set up but, after much trial, error and more than a little help from Fable 5.1, I have succeeded in resolving a single outstanding issue with my current config: triggering a Home Assistant action remotely, without Home Assistant or my phone having internet.

I have used the following devices:
M5Stack CoreS3 SE
M5Stack SIM800L
M5GO Battery Bottom3

Like Voltron, they combine to form something magical. Without boring you too much, I’ll dive right in.

Declare 5x secrets:

m5stack_core_s3_3ef978_sim__encryption_key
m5stack_core_s3_3ef978_sim__ota_password
wifi_ssid
wifi_password
authorised_mobile_numbers

The whitelisted numbers were constantly being reformatted, so I went into manual YAML mode in secrets, formatting them thusly:

authorised_mobile_numbers: |-
  {"+1456456456", "+1234234234", "+1123123123"}

Once those secrets have been declared, use this YAML as your base:

esphome:
  name: m5stack-core-s3-3ef978-sim
  friendly_name: M5Stack-Core-S3-3EF978-SIM
  on_boot:
    # AW9523B IO expander (0x58): release touch/LCD resets, enable M-Bus 5V for SIM800L.
    # Proven working -- readback OUTPUT_P0=0x07, OUTPUT_P1=0x83.
    - priority: 600
      then:
        - lambda: |-
            const uint8_t init[][2] = {
              {0x04, 0b00011000},  // CONFIG_P0: P0_3/P0_4 inputs, rest outputs
              {0x05, 0b00001100},  // CONFIG_P1: P1_2/P1_3 inputs, rest outputs
              {0x11, 0b00010000},  // GCR: P0 push-pull
              {0x12, 0xFF},        // LED_MODE_P0: GPIO mode
              {0x13, 0xFF},        // LED_MODE_P1: GPIO mode
              {0x02, 0b00000111},  // OUTPUT_P0: P0_0 TOUCH_RST=1, P0_1 BUS_OUT_EN=1, P0_2 AW_RST=1
              {0x03, 0b10000011},  // OUTPUT_P1: P1_0=1, P1_1 LCD_RST=1, P1_7 BOOST_EN=1
            };
            for (auto &r : init) {
              if (id(bus_internal).write(0x58, r, 2) != i2c::ERROR_OK)
                ESP_LOGW("aw9523", "Write reg 0x%02X failed", r[0]);
            }
            ESP_LOGI("aw9523", "Touch/LCD reset released, M-Bus 5V enabled");

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  flash_size: 16MB
  framework:
    type: esp-idf
    sdkconfig_options:
      CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
      CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
      CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y"
      CONFIG_SPIRAM_RODATA: "y"
      CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y"

psram:
  mode: quad
  speed: 80MHz

logger:

api:
  encryption:
    key: !secret m5stack_core_s3_3ef978_sim__encryption_key
ota:
  - platform: esphome
    password: !secret m5stack_core_s3_3ef978_sim__ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

external_components:
  - source: github://m5stack/esphome-yaml/components
    components: [axp2101]

i2c:
  id: bus_internal
  sda: GPIO12
  scl: GPIO11
  scan: true

spi:
  id: spi_bus
  clk_pin: GPIO36
  mosi_pin: GPIO37

axp2101:
  id: axp2101_pmu
  i2c_id: bus_internal

output:
  - platform: axp2101
    type: range
    channel: DLDO1          # LCD backlight
    id: lcd_backlight_output
    min_voltage: 2600
    max_voltage: 3300
  - platform: axp2101
    channel: ALDO1
    voltage: 1800
  - platform: axp2101
    channel: ALDO2
    voltage: 3300           # touch controller supply
  - platform: axp2101
    channel: BLDO1
    voltage: 2800
  - platform: axp2101
    channel: BLDO2
    voltage: 1500

light:
  - platform: monochromatic
    id: lcd_backlight
    name: "LCD Backlight"
    icon: "mdi:television"
    entity_category: config
    output: lcd_backlight_output
    restore_mode: RESTORE_DEFAULT_ON
    default_transition_length: 250ms

font:
  - file: "gfonts://Major Mono Display"
    id: font_status
    size: 16

globals:
  - id: allowed_numbers
    type: std::vector<std::string>
    initial_value: !secret authorised_mobile_numbers
  - id: last_touch_ms
    type: uint32_t
    initial_value: '0'
  - id: screen_dimmed
    type: bool
    initial_value: 'false'
  - id: scroll_offset
    type: int
    initial_value: '0'

interval:
  - interval: 1s
    then:
      - lambda: |-
          if (!id(screen_dimmed) && millis() - id(last_touch_ms) > 30000) {
            id(screen_dimmed) = true;
            auto call = id(lcd_backlight).turn_on();
            call.set_brightness(0.15);
            call.perform();
          }
  - interval: 5min
    then:
      - lambda: |-
          id(scroll_offset) = (id(scroll_offset) + 12) % 40;   // small position drift, 0-39px

display:
  - platform: mipi_spi
    model: M5CORE
    id: main_lcd
    spi_id: spi_bus
    cs_pin: GPIO3
    dc_pin: GPIO35
    data_rate: 40MHz
    invert_colors: true
    rotation: 180
    update_interval: 2s
    lambda: |-
      int y = 10 + id(scroll_offset);
      it.fill(Color::BLACK);
      it.printf(10, y, id(font_status), Color::WHITE, "Battery: %.0f%%", id(battery_pct).state);
      it.printf(10, y + 30, id(font_status), Color::WHITE, "Signal: %.0f", id(signal_rssi).state);
      it.printf(10, y + 60, id(font_status), Color::WHITE, "Caller: %s", id(last_caller).state.c_str());
      it.printf(10, y + 90, id(font_status), Color::WHITE, "SMS from: %s", id(sms_sender).state.c_str());

touchscreen:
  - platform: ft63x6
    id: touch
    i2c_id: bus_internal
    transform:
      mirror_x: true
      mirror_y: true
    calibration:
      x_min: 0
      x_max: 320
      y_min: 0
      y_max: 240
    on_touch:
      - lambda: |-
          id(last_touch_ms) = millis();
          if (id(screen_dimmed)) {
            id(screen_dimmed) = false;
            auto call = id(lcd_backlight).turn_on();
            call.set_brightness(1.0);
            call.perform();
          }

sensor:
  - platform: template
    name: "Battery Level"
    id: battery_pct
    unit_of_measurement: "%"
    device_class: battery
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 15s
    lambda: |-
      uint8_t reg1 = 0x00;   // STATUS1
      uint8_t status1 = 0;
      id(bus_internal).write(0x34, &reg1, 1, false);
      id(bus_internal).read(0x34, &status1, 1);
      bool battery_connected = (status1 >> 3) & 1;
      if (!battery_connected) {
        return {};   // publishes "Unknown" -- correctly reflects no battery
      }

      uint8_t reg2 = 0xA4;   // BAT_PERCENT_DATA
      uint8_t percent = 0;
      bool ok = id(bus_internal).write(0x34, &reg2, 1, false) == i2c::ERROR_OK;
      ok = ok && id(bus_internal).read(0x34, &percent, 1) == i2c::ERROR_OK;
      if (!ok) {
        return {};
      }
      return (float) percent;

  - platform: sim800l
    rssi:
      name: "Signal RSSI"
      id: signal_rssi

binary_sensor:
  - platform: template
    name: "Placeholder (axp2101 dependency)"
    id: axp2101_dep_placeholder
    internal: true      # hides it from Home Assistant entity list
    lambda: |-
      return false;
  - platform: template
    name: "Running on Battery"
    device_class: battery
    lambda: |-
      return !id(axp2101_pmu).isVbusGood();

uart:
  id: uart_sim800
  tx_pin: GPIO17
  rx_pin: GPIO18
  baud_rate: 9600

sim800l:
  id: modem
  uart_id: uart_sim800

  on_sms_received:
    - lambda: |-
        id(sms_sender).publish_state(sender);
        id(sms_message).publish_state(message);
    - if:
        condition:
          lambda: |-
            for (auto &n : id(allowed_numbers)) if (sender == n) return true;
            ESP_LOGW("sms", "Ignoring SMS from unknown %s", sender.c_str());
            return false;
        then:
          - homeassistant.event:
              event: esphome.sim800l_sms
              data:
                sender: !lambda 'return sender;'
                message: !lambda 'return message;'

  on_incoming_call:
    - lambda: |-
        id(last_caller).publish_state(caller_id);
        ESP_LOGI("call", "Incoming call from '%s'", caller_id.c_str());
    - if:
        condition:
          lambda: |-
            for (auto &n : id(allowed_numbers)) if (caller_id == n) return true;
            ESP_LOGW("call", "Ignoring call from unknown '%s'", caller_id.c_str());
            return false;
        then:
          - homeassistant.action:
              action: script.do_something # Home Assistant action to perform
          - homeassistant.event:
              event: esphome.sim800l_call
              data:
                caller: !lambda 'return caller_id;'
    - sim800l.disconnect

text_sensor:
  - platform: template
    id: sms_sender
    name: "Last SMS Sender"
  - platform: template
    id: sms_message
    name: "Last SMS Message"
  - platform: template
    id: last_caller
    name: "Last Caller"

# button:
#  - platform: template
#    name: "Send test SMS"
#    on_press:
#      - sim800l.send_sms:
#          recipient: "+1XXXXXXXXX"
#          message: "Hello from M5Stack"

#### add bluetooth tracker
esp32_ble_tracker:
  scan_parameters:
    # We currently use the defaults to ensure Bluetooth
    # can co-exist with WiFi In the future we may be able to
    # enable the built-in coexistence logic in ESP-IDF
    active: true
    interval: 1100ms # testing…
    window: 1100ms # testing…

bluetooth_proxy:
  active: true
  cache_services: true

What’s configured:
M5Stack CoreS3 SE initiates the 800L with required voltages/parameters. This was tricky.
BEWARE
SIM card installs backwards. Not upside down, backwards. If your SIM won’t initialise (red LED flashing 1/s), this is likely your issue. Rotate it 180°.

Next, we initialise the display to output:

  • Battery level
  • Cellular signal strength
  • Last caller ID
  • Last SMS sender and message

We specify what to do when any of the whitelisted callers call with this section:

  on_incoming_call:
    - lambda: |-
        id(last_caller).publish_state(caller_id);
        ESP_LOGI("call", "Incoming call from '%s'", caller_id.c_str());
    - if:
        condition:
          lambda: |-
            for (auto &n : id(allowed_numbers)) if (caller_id == n) return true;
            ESP_LOGW("call", "Ignoring call from unknown '%s'", caller_id.c_str());
            return false;
        then:
          - homeassistant.action:
              action: script.do_something # Home Assistant action to perform

As a bonus, I added Bluetooth proxy functionality, because why not? Don’t need it? Remove:

#### add bluetooth tracker
esp32_ble_tracker:
  scan_parameters:
    # We currently use the defaults to ensure Bluetooth
    # can co-exist with WiFi In the future we may be able to
    # enable the built-in coexistence logic in ESP-IDF
    active: true
    interval: 1100ms # testing…
    window: 1100ms # testing…

bluetooth_proxy:
  active: true
  cache_services: true

That’s it! Now, even IF both Home Assistant AND my phone do not have active internet connections, I can, from a whitelisted device, perform a specific HA action.
Neato.

I will not be providing support here. Do with this what you will.
Be nice.

1 Like