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, ®1, 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, ®2, 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.