mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
344
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17f6225436 | ||
|
|
1d866e147e | ||
|
|
025cf4647f | ||
|
|
534496bd89 | ||
|
|
15bca8614a | ||
|
|
7746f5d483 | ||
|
|
2668bb00a4 | ||
|
|
dff224077c | ||
|
|
fe17af632c | ||
|
|
2318e7baed | ||
|
|
114238802a | ||
|
|
b5749249c8 | ||
|
|
b19b09378b | ||
|
|
a115220bf8 | ||
|
|
c7d534f8ca | ||
|
|
ce82777269 | ||
|
|
7a1efc879d | ||
|
|
81e4dd13e1 | ||
|
|
c2b061056e | ||
|
|
5b6d2c26c1 | ||
|
|
8f95e44653 | ||
|
|
1bec107346 | ||
|
|
83eb5f37bf | ||
|
|
a98f0e66be | ||
|
|
397b7b4ec6 | ||
|
|
e3bf673296 | ||
|
|
2f265b92d2 | ||
|
|
5c2379dfd1 | ||
|
|
98e6d18bb3 | ||
|
|
e516cce2c1 | ||
|
|
04097ac08e | ||
|
|
1ba8183cf4 | ||
|
|
addf571824 | ||
|
|
83886e1997 | ||
|
|
1df7986fb1 | ||
|
|
e38a473ac3 | ||
|
|
129427d24c | ||
|
|
89f59efc84 | ||
|
|
afa1b22ca4 | ||
|
|
7ed64156f4 | ||
|
|
689cb4729d | ||
|
|
106d143e4a | ||
|
|
9f832a900d | ||
|
|
9f79961ed1 | ||
|
|
ae106d3550 | ||
|
|
348fe63013 | ||
|
|
7ee10adf5e | ||
|
|
ad3a91b9b0 | ||
|
|
45b66567b2 | ||
|
|
c405ae82ee | ||
|
|
35e9bd9b26 | ||
|
|
c4c26a8ae6 | ||
|
|
b19fd6f91f | ||
|
|
e37833af37 | ||
|
|
8e38fc1d2f | ||
|
|
c531efb5e0 | ||
|
|
ee7bec52b1 | ||
|
|
b47a9887b3 | ||
|
|
d9de2b8848 | ||
|
|
acb9f4817e | ||
|
|
d4f2daa9bd | ||
|
|
1701947ab2 | ||
|
|
6e7fe95f87 | ||
|
|
856bbff6e9 | ||
|
|
c3129c5f48 | ||
|
|
acae3bf94a | ||
|
|
9345718b55 | ||
|
|
bd211f3794 | ||
|
|
e5d7a6b7a6 | ||
|
|
f597a66ca6 | ||
|
|
72e5337ffa | ||
|
|
f4c6ed0339 | ||
|
|
6ee6930eb0 | ||
|
|
965ede31ad | ||
|
|
90dd532a4c | ||
|
|
efc049c0f3 | ||
|
|
cfecf1a538 | ||
|
|
d09333c9e1 | ||
|
|
6499ebe32e | ||
|
|
f70296ef59 | ||
|
|
9446601c83 | ||
|
|
322fed1599 | ||
|
|
24fd685f13 | ||
|
|
75fb0daca7 | ||
|
|
83a9921784 | ||
|
|
e86b12ac84 | ||
|
|
7627377d20 | ||
|
|
fd3e42c1d1 | ||
|
|
bfc8185dc8 | ||
|
|
1d41d8dcd5 | ||
|
|
88e2144421 | ||
|
|
30cfa0f7d6 | ||
|
|
bfbdb57a6e | ||
|
|
b9405a4ddc | ||
|
|
6fe880d2bf | ||
|
|
3fbff42e21 | ||
|
|
026ae244d8 | ||
|
|
1ecd252975 | ||
|
|
202e91a7d0 | ||
|
|
6297be44de | ||
|
|
c8425de4da | ||
|
|
b5c0fd8ec2 | ||
|
|
34189d357a | ||
|
|
583eb2d5b4 | ||
|
|
6a59c30ef9 | ||
|
|
89920ca3d5 | ||
|
|
0d27fc3d9d | ||
|
|
8768cf4681 | ||
|
|
02bd72a75e | ||
|
|
bf4f6501ea | ||
|
|
cef7b289c3 | ||
|
|
eb8d17f133 | ||
|
|
1b7a6f9201 | ||
|
|
10eb3c6e84 | ||
|
|
dfa6a5c098 | ||
|
|
f8517d9347 | ||
|
|
e6a4eeff81 | ||
|
|
3102e22613 | ||
|
|
c77f0f4c1d | ||
|
|
9829a81064 | ||
|
|
346490f9be | ||
|
|
9efbd39dce | ||
|
|
0bac32fb6a | ||
|
|
10d70ea657 | ||
|
|
0f6ecc91b0 | ||
|
|
0521304f7f | ||
|
|
2e83d1bf76 | ||
|
|
e3ca47e8d0 | ||
|
|
1022c43797 | ||
|
|
83e8da545c | ||
|
|
df44741085 | ||
|
|
04ad19827c | ||
|
|
c2044addde | ||
|
|
0fe5a8b8f5 | ||
|
|
d47fb46cd0 | ||
|
|
b88aa4c259 | ||
|
|
73b25cb214 | ||
|
|
e27948e8cb | ||
|
|
94685309a7 | ||
|
|
8f00dd44f7 | ||
|
|
5924e55307 | ||
|
|
a14e52feb5 | ||
|
|
5e5f5bb2c7 | ||
|
|
6ef89a6c68 | ||
|
|
eb2009e730 | ||
|
|
e802fb1b44 | ||
|
|
a279b2d094 | ||
|
|
e164f7a5fe | ||
|
|
3f490d229d | ||
|
|
c2012f2358 | ||
|
|
4800d057b6 | ||
|
|
1f87d08ef7 | ||
|
|
07c205a3e5 | ||
|
|
3c804b0b25 | ||
|
|
95c0a56791 | ||
|
|
0d30ae69b8 | ||
|
|
0f08d5e117 | ||
|
|
de084c5ab7 | ||
|
|
ca82e2d829 | ||
|
|
83d2256ecf | ||
|
|
c304f0b4e2 | ||
|
|
c79272f739 | ||
|
|
ba7e7e7f7b | ||
|
|
f8bf778b82 | ||
|
|
e7be564951 | ||
|
|
bbade6f7e2 | ||
|
|
0780f3f30b | ||
|
|
9827c6839d | ||
|
|
f2b864adc9 | ||
|
|
6f974e908a | ||
|
|
ad5366c91e | ||
|
|
1725245aa2 | ||
|
|
4ebd5ac87a | ||
|
|
35d5fa96c3 | ||
|
|
c5b6210845 | ||
|
|
1ce46cc47c | ||
|
|
f1b26c8304 | ||
|
|
e1210ebe06 | ||
|
|
be2462bc2e | ||
|
|
dc491d5ca0 | ||
|
|
2bdfa915a5 | ||
|
|
e5c2158d47 | ||
|
|
09c2f79605 | ||
|
|
37e8e4c3df | ||
|
|
273a6d12b5 | ||
|
|
dc439ba504 | ||
|
|
a172a9c6c8 | ||
|
|
a159a9f899 | ||
|
|
f2263e62bf | ||
|
|
bc7eb17c0b | ||
|
|
c166e16911 | ||
|
|
f6cc32a39a | ||
|
|
a223f2d66b | ||
|
|
17309fb315 | ||
|
|
8681a5b302 | ||
|
|
def2be4b83 | ||
|
|
6cbd2af6b4 | ||
|
|
352111b556 | ||
|
|
d164ccd12a | ||
|
|
6d1a18b2fa | ||
|
|
24a4845991 | ||
|
|
2c9c6aea30 | ||
|
|
3102f6bfd5 | ||
|
|
77a30830c7 | ||
|
|
412ba4f60f | ||
|
|
0b6708ddbe | ||
|
|
c4705967bd | ||
|
|
5982d11ffb | ||
|
|
faa3b2474e | ||
|
|
00f54725e3 | ||
|
|
3a289c8284 | ||
|
|
67fb79a6e7 | ||
|
|
0b4d2cbe07 | ||
|
|
685deedfce | ||
|
|
36373e68dd | ||
|
|
8824ac14cf | ||
|
|
307880bc05 | ||
|
|
60a7ef7408 | ||
|
|
e1af5bc767 | ||
|
|
116628bdc6 | ||
|
|
6cd0d8f8c2 | ||
|
|
181beb0852 | ||
|
|
5d8f37aed5 | ||
|
|
3120d0d43a | ||
|
|
8f948599fa | ||
|
|
51aca893da | ||
|
|
1aa2ce8608 | ||
|
|
1256bfdd54 | ||
|
|
e5d520363a | ||
|
|
5f5afbab2c | ||
|
|
28dd222283 | ||
|
|
4ec62a2be6 | ||
|
|
9bfde3b3ab | ||
|
|
85324a828e | ||
|
|
3df3c776be | ||
|
|
67cce34779 | ||
|
|
a78885c391 | ||
|
|
d33207bda7 | ||
|
|
9fa6db28eb | ||
|
|
af3b3c93ef | ||
|
|
5b4ccabc17 | ||
|
|
a82e07880a | ||
|
|
490e13c86c | ||
|
|
83c6d8deb0 | ||
|
|
62fb07e595 | ||
|
|
00b9e83d0e | ||
|
|
1b9c9e9ccb | ||
|
|
4b0ae086dc | ||
|
|
5bc6cd8a17 | ||
|
|
72119838d9 | ||
|
|
c9f1ee0a62 | ||
|
|
4f74cec977 | ||
|
|
4e6da9f34d | ||
|
|
e4d6f5e58d | ||
|
|
00aab9ce8d | ||
|
|
8061104496 | ||
|
|
40e6bb16d0 | ||
|
|
8175da82ee | ||
|
|
c4c96e4bc9 | ||
|
|
4d348aae3c | ||
|
|
acd2a9bf54 | ||
|
|
71725ab4c4 | ||
|
|
90ec353d85 | ||
|
|
34d5445e27 | ||
|
|
98ba007de3 | ||
|
|
b8c965b3fc | ||
|
|
1096bc264f | ||
|
|
5c0b7222ec | ||
|
|
d2c26e95bd | ||
|
|
0ada4af5cf | ||
|
|
adad7469e5 | ||
|
|
971fbd6d26 | ||
|
|
7b5fe70cf6 | ||
|
|
ca55ed7b4b | ||
|
|
171213fe38 | ||
|
|
900a306eda | ||
|
|
f6e67462e7 | ||
|
|
d0cefb4a08 | ||
|
|
5321163e6b | ||
|
|
a814ea337f | ||
|
|
6d2e6eb15a | ||
|
|
cd363d104e | ||
|
|
d85c645c23 | ||
|
|
61628f6cdd | ||
|
|
87244f6196 | ||
|
|
492ea38d92 | ||
|
|
a8e43335ae | ||
|
|
290952fd9c | ||
|
|
a702097b1f | ||
|
|
2370d5c83d | ||
|
|
eb37215e9f | ||
|
|
671c71c406 | ||
|
|
7e09c54490 | ||
|
|
6b377ad1f8 | ||
|
|
7b393e71d4 | ||
|
|
521931d0fe | ||
|
|
bd5fd0c12b | ||
|
|
377c30f72e | ||
|
|
8095850e83 | ||
|
|
877fb5a144 | ||
|
|
5bafee43bd | ||
|
|
a6baff9085 | ||
|
|
321594ae3d | ||
|
|
c2d3774762 | ||
|
|
60840a0286 | ||
|
|
5d0d7c84c0 | ||
|
|
b584d2b81e | ||
|
|
89066c374e | ||
|
|
1a72358db2 | ||
|
|
a43eb1563e | ||
|
|
7ab0776302 | ||
|
|
ac1bd3012a | ||
|
|
58dcc3b44c | ||
|
|
e1267aedaf | ||
|
|
a15c02478a | ||
|
|
b2f81eb57c | ||
|
|
0f0c0a79c3 | ||
|
|
0e7029f163 | ||
|
|
ee873ee420 | ||
|
|
9b9f54664a | ||
|
|
87c6c41541 | ||
|
|
2e042ad727 | ||
|
|
d67fa07ee2 | ||
|
|
32f2c9f88e | ||
|
|
064af5aecd | ||
|
|
ab88916988 | ||
|
|
c4cdea0a9f | ||
|
|
ef6382edee | ||
|
|
7260eb6225 | ||
|
|
4f7bd585e9 | ||
|
|
a3ed854b51 | ||
|
|
73ff266096 | ||
|
|
a71a4bf677 | ||
|
|
df6a2e7718 | ||
|
|
0a2f9b2123 | ||
|
|
3a6b6b65d9 | ||
|
|
111dc0e8bd | ||
|
|
5d925f6ef0 | ||
|
|
3f02b4a930 | ||
|
|
d858ca6e46 | ||
|
|
cf24dbb59c | ||
|
|
ccd10c9fd9 | ||
|
|
bff381f6c5 | ||
|
|
0a01d1ef32 |
@@ -0,0 +1 @@
|
||||
b024c362cb4f55a7498b76dfd5602131167411a31e119657a79e909df12c752b
|
||||
@@ -425,6 +425,7 @@ esphome/components/modbus_controller/sensor/* @martgras
|
||||
esphome/components/modbus_controller/switch/* @martgras
|
||||
esphome/components/modbus_controller/text_sensor/* @martgras
|
||||
esphome/components/modbus_server/* @exciton
|
||||
esphome/components/modem/* @oarcher
|
||||
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
||||
esphome/components/mopeka_pro_check/* @spbrogan
|
||||
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.components.const import CONF_RTS_PIN
|
||||
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
|
||||
from esphome.components.text_sensor import new_text_sensor, text_sensor_schema
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BAUD_RATE,
|
||||
CONF_COMMAND,
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_ID,
|
||||
CONF_MODEL,
|
||||
CONF_ON_CONNECT,
|
||||
CONF_ON_DISCONNECT,
|
||||
CONF_REBOOT_TIMEOUT,
|
||||
CONF_RX_BUFFER_SIZE,
|
||||
CONF_RX_PIN,
|
||||
CONF_TX_BUFFER_SIZE,
|
||||
CONF_TX_PIN,
|
||||
CONF_USE_ADDRESS,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
)
|
||||
from esphome.core import coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@oarcher"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
AUTO_LOAD = ["network", "uart", "text_sensor"]
|
||||
CONFLICTS_WITH = ["captive_portal", "ethernet"]
|
||||
DOMAIN = "modem"
|
||||
|
||||
CONF_MODEM = "modem"
|
||||
CONF_MODEM_ID = "modem_id"
|
||||
CONF_PIN_CODE = "pin_code"
|
||||
CONF_APN = "apn"
|
||||
CONF_CTS_PIN = "cts_pin"
|
||||
CONF_INIT_AT = "init_at"
|
||||
CONF_ON_SYNC = "on_sync"
|
||||
CONF_DTE_BUFFER_SIZE = "dte_buffer_size"
|
||||
CONF_URC = "urc"
|
||||
|
||||
MODEM_MODELS = [
|
||||
"BG96",
|
||||
"SIM800",
|
||||
"SIM7000",
|
||||
"SIM7070",
|
||||
"SIM7080",
|
||||
"SIM7600",
|
||||
"SIM7670",
|
||||
"GENERIC",
|
||||
]
|
||||
modem_ns = cg.esphome_ns.namespace("modem")
|
||||
ModemComponent = modem_ns.class_("ModemComponent", cg.Component, uart.UARTComponent)
|
||||
|
||||
ModemComponentState = modem_ns.enum("ModemComponentState", is_class=True)
|
||||
|
||||
MODEM_COMPONENT_SCHEMA = cv.Schema(
|
||||
{cv.GenerateID(CONF_MODEM_ID): cv.use_id(ModemComponent)}
|
||||
)
|
||||
|
||||
# Each trigger fires on one state, and none while the component is failed
|
||||
_NOT_FAILED = "!{parent}->is_failed()"
|
||||
|
||||
|
||||
def _on_state(conf_key: str, state: str) -> automation.CallbackAutomation:
|
||||
return automation.CallbackAutomation(
|
||||
conf_key,
|
||||
"add_on_state_callback",
|
||||
params=[(ModemComponentState, "state")],
|
||||
forward=[],
|
||||
when=f"{_NOT_FAILED} && state == modem::ModemComponentState::{state}",
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
_on_state(CONF_ON_CONNECT, "MODEM_CONNECTED"),
|
||||
_on_state(CONF_ON_DISCONNECT, "MODEM_DISCONNECTING"),
|
||||
_on_state(CONF_ON_SYNC, "MODEM_SYNCED"),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(ModemComponent),
|
||||
cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RTS_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Optional(CONF_CTS_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_BAUD_RATE): cv.positive_int,
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODEM_MODELS, upper=True),
|
||||
cv.Required(CONF_APN): cv.string,
|
||||
cv.Optional(CONF_PIN_CODE): cv.sensitive(cv.string_strict),
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string,
|
||||
cv.Optional(CONF_INIT_AT): cv.All(cv.ensure_list(cv.string)),
|
||||
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_TX_BUFFER_SIZE, default=1024): cv.positive_int,
|
||||
cv.Optional(CONF_RX_BUFFER_SIZE, default=1024): cv.positive_int,
|
||||
cv.Optional(CONF_DTE_BUFFER_SIZE, default=1024): cv.positive_int,
|
||||
cv.Optional(
|
||||
CONF_REBOOT_TIMEOUT, default="10min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_SYNC): automation.validate_automation({}),
|
||||
cv.Optional(CONF_URC): text_sensor_schema(
|
||||
device_class=DEVICE_CLASS_EMPTY,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(MODEM_COMPONENT_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.require_framework_version(
|
||||
esp_idf=cv.Version(4, 0, 0), # 5.2.0 OK
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
MODEM_ID_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(ModemComponent)}
|
||||
)
|
||||
SEND_AT_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(ModemComponent),
|
||||
cv.Required(CONF_COMMAND): cv.templatable(cv.string),
|
||||
},
|
||||
key=CONF_COMMAND,
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"modem.enable", MODEM_ID_SCHEMA, automation.ApplyCall("enable()")
|
||||
)
|
||||
automation.register_apply_action(
|
||||
"modem.disable", MODEM_ID_SCHEMA, automation.ApplyCall("disable()")
|
||||
)
|
||||
automation.register_apply_action(
|
||||
"modem.send_at",
|
||||
SEND_AT_SCHEMA,
|
||||
automation.ApplyCall("send_at({}, 1000, true)", ((CONF_COMMAND, cg.std_string),)),
|
||||
)
|
||||
automation.register_apply_condition(
|
||||
"modem.connected", MODEM_ID_SCHEMA, "is_connected()"
|
||||
)
|
||||
automation.register_apply_condition("modem.enabled", MODEM_ID_SCHEMA, "is_enabled()")
|
||||
|
||||
|
||||
@coroutine_with_priority(60.0)
|
||||
async def to_code(config):
|
||||
add_idf_component(
|
||||
name="esp_modem",
|
||||
repo="https://github.com/espressif/esp-protocols.git",
|
||||
ref="modem-v2.0.0",
|
||||
path="components/esp_modem",
|
||||
)
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_PPP", True)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_PPP_SUPPORT", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_MODEM_CMUX_DELAY_AFTER_DLCI_SETUP", 500)
|
||||
add_idf_sdkconfig_option("CONFIG_PPP_SUPPORT", True)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_MODEM_CMUX_USE_SHORT_PAYLOADS_ONLY", True
|
||||
) # Slower but more reliable
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_MODEM_URC_HANDLER", True)
|
||||
|
||||
# If "Uart queue full" messages appear (e.g., with A7672), these config options might need adjustment
|
||||
# See: https://github.com/espressif/esp-protocols/issues/272#issuecomment-1558682967
|
||||
# add_idf_sdkconfig_option("CONFIG_ESP_MODEM_CMUX_DEFRAGMENT_PAYLOAD", True)
|
||||
# add_idf_sdkconfig_option("CONFIG_ESP_MODEM_USE_INFLATABLE_BUFFER_IF_NEEDED", True)
|
||||
# add_idf_sdkconfig_option("CONFIG_ESP_MODEM_CMUX_USE_SHORT_PAYLOADS_ONLY", False)
|
||||
|
||||
cg.add_define("USE_MODEM")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
cg.add_optional(var.set_use_address, config.get(CONF_USE_ADDRESS))
|
||||
cg.add_optional(var.set_pin_code, config.get(CONF_PIN_CODE))
|
||||
|
||||
if config[CONF_ENABLE_ON_BOOT]:
|
||||
cg.add(var.enable())
|
||||
|
||||
for cmd in config.get(CONF_INIT_AT, []):
|
||||
cg.add(var.add_init_at_command(cmd))
|
||||
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
cg.add(var.set_apn(config[CONF_APN]))
|
||||
|
||||
await cg.add_pin(var.set_tx_pin, config[CONF_TX_PIN])
|
||||
await cg.add_pin(var.set_rx_pin, config[CONF_RX_PIN])
|
||||
await cg.add_pin(var.set_rts_pin, config.get(CONF_RTS_PIN))
|
||||
await cg.add_pin(var.set_cts_pin, config.get(CONF_CTS_PIN))
|
||||
|
||||
cg.add_optional(var.set_baud_rate, config.get(CONF_BAUD_RATE))
|
||||
cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE]))
|
||||
cg.add(var.set_tx_buffer_size(config[CONF_TX_BUFFER_SIZE]))
|
||||
cg.add(var.set_dte_buffer_size(config[CONF_DTE_BUFFER_SIZE]))
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if urc_config := config.get(CONF_URC, None):
|
||||
cg.add_define("USE_MODEM_URC")
|
||||
urc_text_sensor = await new_text_sensor(urc_config)
|
||||
cg.add(var.set_urc_text_sensor(urc_text_sensor))
|
||||
|
||||
await cg.register_component(var, config)
|
||||
@@ -0,0 +1,469 @@
|
||||
#ifdef USE_ESP32
|
||||
#include "modem_component.h"
|
||||
#include "modem_handler.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
|
||||
#include <esp_netif.h>
|
||||
#include <esp_netif_ppp.h>
|
||||
#include <esp_event.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <lwip/dns.h>
|
||||
|
||||
#include <cxx_include/esp_modem_dte.hpp>
|
||||
#include <esp_modem_config.h>
|
||||
#include <cxx_include/esp_modem_api.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
#define ESPHL_ERROR_CHECK(err, message) \
|
||||
if ((err) != ESP_OK) { \
|
||||
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
|
||||
this->mark_failed(); \
|
||||
return; \
|
||||
}
|
||||
|
||||
namespace esphome::modem {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "modem");
|
||||
|
||||
ModemComponent *global_modem_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
ModemComponent::ModemComponent() {
|
||||
assert(global_modem_component == nullptr);
|
||||
global_modem_component = this;
|
||||
this->modem_handler = std::make_unique<ModemHandler>();
|
||||
}
|
||||
|
||||
// Delegated methods
|
||||
AtCommandResult ModemComponent::send_at(const std::string &cmd, uint32_t timeout, bool verbose) {
|
||||
return this->modem_handler->send_at(cmd, timeout, verbose);
|
||||
}
|
||||
|
||||
void ModemComponent::enable() {
|
||||
ESP_LOGI(TAG, "Setting modem target state to CONNECTED");
|
||||
this->target_state_ = ModemComponentState::MODEM_CONNECTED;
|
||||
// State machine in loop() will handle the ascent
|
||||
}
|
||||
|
||||
void ModemComponent::disable() {
|
||||
ESP_LOGI(TAG, "Setting modem target state to DISABLED");
|
||||
this->target_state_ = ModemComponentState::MODEM_DISABLED;
|
||||
// State machine in loop() will handle the descent
|
||||
}
|
||||
|
||||
network::IPAddresses ModemComponent::get_ip_addresses() {
|
||||
network::IPAddresses addresses;
|
||||
if (this->component_state_ == ModemComponentState::MODEM_CONNECTED) {
|
||||
addresses[0] = network::IPAddress(&this->modem_handler->network_infos.ip_info.ip);
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
void ModemComponent::setup() {
|
||||
ESP_LOGI(TAG, "Modem setup...");
|
||||
this->pref_ = global_preferences->make_preference<ModemRestoreState>(76007670UL);
|
||||
this->pref_.load(&this->modem_restore_state_);
|
||||
|
||||
// find an available UART
|
||||
#ifdef UART_NUM_2
|
||||
for (auto p : {UART_NUM_2, UART_NUM_1}) {
|
||||
#else
|
||||
for (auto p : {UART_NUM_1}) {
|
||||
#endif
|
||||
if (!uart_is_driver_installed(p)) {
|
||||
this->modem_handler->uart_port_num = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this->modem_handler->uart_port_num == UART_NUM_MAX) {
|
||||
ESP_LOGE(TAG, "No free UART port for modem");
|
||||
this->mark_failed(LOG_STR("No free UART port for modem"));
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, "Config Modem:");
|
||||
ESP_LOGCONFIG(TAG, " Model : %s", this->modem_handler->model.c_str());
|
||||
ESP_LOGCONFIG(TAG, " APN : %s", this->modem_handler->apn.c_str());
|
||||
ESP_LOGCONFIG(TAG, " PIN code : %s",
|
||||
(this->modem_handler->pin_code.empty()) ? LOG_STR_LITERAL("No") : LOG_STR_LITERAL("Yes (not shown)"));
|
||||
ESP_LOGCONFIG(TAG, " Tx Pin : GPIO%u", this->modem_handler->tx_pin->get_pin());
|
||||
ESP_LOGCONFIG(TAG, " Rx Pin : GPIO%u", this->modem_handler->rx_pin->get_pin());
|
||||
ESP_LOGCONFIG(
|
||||
TAG, " Enabled : %s",
|
||||
(this->component_state_ != ModemComponentState::MODEM_DISABLED) ? LOG_STR_LITERAL("Yes") : LOG_STR_LITERAL("No"));
|
||||
if (this->modem_handler->baud_rate != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Baud rate : %d", this->modem_handler->baud_rate);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " UART port : %d", this->modem_handler->uart_port_num);
|
||||
ESP_LOGCONFIG(TAG, " TX buffer size : %d", this->modem_handler->tx_buffer_size);
|
||||
ESP_LOGCONFIG(TAG, " RX buffer size : %d", this->modem_handler->rx_buffer_size);
|
||||
ESP_LOGCONFIG(TAG, " DTE buffer size : %d", this->modem_handler->dte_buffer_size);
|
||||
|
||||
if (CONFIG_ESP_TASK_WDT_TIMEOUT_S <= 10) {
|
||||
ESP_LOGW(TAG, "WDT timeout (%d s) may be too low for modem. Increase if WDT triggers.",
|
||||
CONFIG_ESP_TASK_WDT_TIMEOUT_S);
|
||||
}
|
||||
|
||||
esp_netif_config_t netif_ppp_config = ESP_NETIF_DEFAULT_PPP();
|
||||
this->modem_handler->ppp_netif = esp_netif_new(&netif_ppp_config);
|
||||
assert(this->modem_handler->ppp_netif);
|
||||
|
||||
esp_err_t err = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &ModemHandler::ip_event_handler,
|
||||
this->modem_handler.get());
|
||||
ESPHL_ERROR_CHECK(err, "IP event handler register failed");
|
||||
|
||||
this->modem_handler->modem_create_dte_dce(this->modem_handler->baud_rate);
|
||||
|
||||
ESP_LOGV(TAG, "Setup complete");
|
||||
}
|
||||
|
||||
void ModemComponent::loop() {
|
||||
if (App.get_loop_component_start_time() < this->next_loop_millis_) {
|
||||
// Some commands require a delay.
|
||||
delay(10);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate next step toward target_state_
|
||||
ModemComponentState next_state = this->compute_next_state_();
|
||||
|
||||
if (next_state != this->component_state_) {
|
||||
this->transition_to_(next_state);
|
||||
}
|
||||
}
|
||||
|
||||
ModemComponentState ModemComponent::compute_next_state_() {
|
||||
// Priority 1: If target = DISABLED and we're not there, descend
|
||||
if (this->target_state_ == ModemComponentState::MODEM_DISABLED) {
|
||||
if (this->component_state_ == ModemComponentState::MODEM_DISABLED) {
|
||||
return ModemComponentState::MODEM_DISABLED; // Stay
|
||||
}
|
||||
if (this->component_state_ != ModemComponentState::MODEM_DISCONNECTING) {
|
||||
return ModemComponentState::MODEM_DISCONNECTING; // Start descent
|
||||
}
|
||||
// Otherwise we're already in DISCONNECTING, let handler manage
|
||||
}
|
||||
|
||||
// Priority 2: Execute current state logic
|
||||
switch (this->component_state_) {
|
||||
case ModemComponentState::MODEM_DISABLED:
|
||||
return this->handle_state_disabled_();
|
||||
case ModemComponentState::MODEM_ENABLING:
|
||||
return this->handle_state_enabling_();
|
||||
case ModemComponentState::MODEM_SYNCED:
|
||||
return this->handle_state_synced_();
|
||||
case ModemComponentState::MODEM_CONNECTING:
|
||||
return this->handle_state_connecting_();
|
||||
case ModemComponentState::MODEM_WAIT_IP:
|
||||
return this->handle_state_wait_ip_();
|
||||
case ModemComponentState::MODEM_CONNECTED:
|
||||
return this->handle_state_connected_();
|
||||
case ModemComponentState::MODEM_DISCONNECTING:
|
||||
return this->handle_state_disconnecting_();
|
||||
}
|
||||
|
||||
return this->component_state_; // Fallback
|
||||
}
|
||||
|
||||
ModemComponentState ModemComponent::handle_state_disabled_() {
|
||||
this->loop_delay_(3000);
|
||||
|
||||
// If target is DISABLED, stay here
|
||||
if (this->target_state_ == ModemComponentState::MODEM_DISABLED) {
|
||||
return ModemComponentState::MODEM_DISABLED;
|
||||
}
|
||||
|
||||
// Otherwise, target is CONNECTED, start ascent
|
||||
return ModemComponentState::MODEM_ENABLING;
|
||||
}
|
||||
|
||||
ModemComponentState ModemComponent::handle_state_enabling_() {
|
||||
// Connect to the modem (testing different baud), and set the modem in a fresh state (disconnect from a previous
|
||||
// connection) the modem will be synced at expected baud rate when exiting this state.
|
||||
|
||||
auto try_autobaud = [&](int baud) {
|
||||
this->modem_handler->modem_create_dte_dce(baud);
|
||||
this->modem_handler->dce->set_mode(esp_modem::modem_mode::AUTODETECT);
|
||||
bool success = this->modem_handler->dce->get_mode() != esp_modem::modem_mode::UNDEF;
|
||||
// Sometimes the modem does not answer autobaud commands,
|
||||
// so try sending an AT command to confirm it's responsive at this baud rate.
|
||||
if (!success) {
|
||||
App.feed_wdt();
|
||||
auto result = this->modem_handler->send_at("AT", 100);
|
||||
success = result.esp_modem_command_result == command_result::OK;
|
||||
}
|
||||
if (success) {
|
||||
this->modem_handler->current_baud_rate = baud;
|
||||
this->modem_restore_state_.baud_rate = baud;
|
||||
this->pref_.save(&this->modem_restore_state_);
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
// possible baud rate to try
|
||||
std::vector<int> bauds = {this->modem_handler->current_baud_rate, this->modem_restore_state_.baud_rate,
|
||||
this->modem_handler->baud_rate, 115200};
|
||||
std::sort(bauds.begin(), bauds.end());
|
||||
bauds.erase(std::unique(bauds.begin(), bauds.end()), bauds.end());
|
||||
|
||||
this->modem_handler->dce->set_mode(esp_modem::modem_mode::AUTODETECT);
|
||||
|
||||
auto mode = this->modem_handler->dce->get_mode();
|
||||
|
||||
ESP_LOGI(TAG, "Autodetect mode (1st): %d", static_cast<int>(mode));
|
||||
|
||||
switch (mode) {
|
||||
case modem_mode::CMUX_MANUAL_MODE:
|
||||
ESP_LOGD(TAG, "Modem warm reboot. Exiting CMUX");
|
||||
this->modem_handler->dce->set_mode(modem_mode::CMUX_MANUAL_EXIT);
|
||||
break;
|
||||
|
||||
case modem_mode::UNDEF: {
|
||||
ESP_LOGD(TAG, "Trying other baud rate");
|
||||
bool responded = false;
|
||||
for (int b : bauds) {
|
||||
if (try_autobaud(b)) {
|
||||
ESP_LOGD(TAG, "Modem responded at baud %d", b);
|
||||
responded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!responded) {
|
||||
ESP_LOGW(TAG, "Modem not responding. Check power, PWRKEY and TX/RX wiring.");
|
||||
this->loop_delay_(1000);
|
||||
return ModemComponentState::MODEM_ENABLING;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case modem_mode::COMMAND_MODE:
|
||||
ESP_LOGD(TAG, "OK: Modem in COMMAND mode.");
|
||||
break;
|
||||
|
||||
case modem_mode::DATA_MODE:
|
||||
// Can block 20s! But no workaround found (with esp_modem 2.0.0)
|
||||
ESP_LOGW(TAG, "Exiting DATA_MODE. (long blocking call, make sur to have a long WDT!)");
|
||||
this->modem_handler->dce->set_mode(modem_mode::COMMAND_MODE);
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGW(TAG, "Modem in unexpected mode %d. Attempting to switch to COMMAND mode.", static_cast<int>(mode));
|
||||
this->modem_handler->dce->set_mode(modem_mode::COMMAND_MODE);
|
||||
this->modem_handler->dce->set_mode(modem_mode::UNDEF);
|
||||
// return ModemComponentState::MODEM_ENABLING;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->modem_handler->dce->sync() != esp_modem::command_result::OK) {
|
||||
ESP_LOGW(TAG, "Waiting SYNC");
|
||||
this->loop_delay_(1000);
|
||||
return ModemComponentState::MODEM_ENABLING;
|
||||
}
|
||||
|
||||
#ifdef USE_MODEM_URC
|
||||
auto urc_handler = [this](const esp_modem::DTE::UrcBufferInfo &buffer_info) {
|
||||
if (!buffer_info.is_command_active) {
|
||||
std::string line(reinterpret_cast<const char *>(buffer_info.new_data_start), buffer_info.new_data_size);
|
||||
// Publish to text sensor if configured
|
||||
if (this->urc_text_sensor != nullptr) {
|
||||
// defer so we won't be in the callback context
|
||||
this->defer([this, line]() { this->urc_text_sensor->publish_state(line); });
|
||||
}
|
||||
}
|
||||
return esp_modem::DTE::UrcConsumeInfo{esp_modem::DTE::UrcConsumeResult::CONSUME_NONE, 0};
|
||||
};
|
||||
this->modem_handler->dce->set_enhanced_urc(urc_handler);
|
||||
#endif
|
||||
|
||||
int radio_state = 1;
|
||||
this->modem_handler->dce->get_radio_state(radio_state);
|
||||
if (radio_state == 1) {
|
||||
// On warm reboot, we need to disconnect modem radio.
|
||||
if (this->modem_handler->dce->set_radio_state(0) != command_result::OK) {
|
||||
ESP_LOGW(TAG, "Failed to disconnect previous network session");
|
||||
}
|
||||
return ModemComponentState::MODEM_ENABLING;
|
||||
}
|
||||
|
||||
if (this->modem_handler->baud_rate != this->modem_handler->current_baud_rate) {
|
||||
ESP_LOGD(TAG, "Setting baud rate: %d -> %d", this->modem_handler->current_baud_rate,
|
||||
this->modem_handler->baud_rate);
|
||||
this->modem_handler->dce->sync();
|
||||
if (this->modem_handler->dce->set_baud(this->modem_handler->baud_rate) == esp_modem::command_result::OK) {
|
||||
ESP_LOGD(TAG, "Modem baud rate set to %d.", this->modem_handler->baud_rate);
|
||||
delay(200); // NOLINT
|
||||
this->modem_handler->modem_create_dte_dce(this->modem_handler->baud_rate);
|
||||
App.feed_wdt();
|
||||
delay(200); // NOLINT
|
||||
this->modem_handler->dce->sync();
|
||||
if (this->modem_handler->dce->sync() == esp_modem::command_result::OK) {
|
||||
ESP_LOGI(TAG, "Modem synced at baud rate %d.", this->modem_handler->current_baud_rate);
|
||||
this->modem_restore_state_.baud_rate = this->modem_handler->current_baud_rate;
|
||||
this->pref_.save(&this->modem_restore_state_);
|
||||
global_preferences->sync();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to sync modem at baud rate %d.", this->modem_handler->baud_rate);
|
||||
return ModemComponentState::MODEM_DISCONNECTING;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to set modem baud rate to %d. Using %d.", this->modem_handler->baud_rate,
|
||||
this->modem_handler->current_baud_rate);
|
||||
this->loop_delay_(1000);
|
||||
return ModemComponentState::MODEM_ENABLING;
|
||||
}
|
||||
}
|
||||
|
||||
return ModemComponentState::MODEM_SYNCED;
|
||||
}
|
||||
|
||||
ModemComponentState ModemComponent::handle_state_synced_() { return ModemComponentState::MODEM_CONNECTING; }
|
||||
|
||||
ModemComponentState ModemComponent::handle_state_connecting_() {
|
||||
// Merges INIT_NETWORK and START_PPP states
|
||||
if (this->modem_handler->dce->sync() != esp_modem::command_result::OK) {
|
||||
ESP_LOGW(TAG, "Modem not synced during network init");
|
||||
return ModemComponentState::MODEM_ENABLING;
|
||||
}
|
||||
|
||||
int attachement_state = 0;
|
||||
this->modem_handler->dce->set_radio_state(1);
|
||||
this->modem_handler->prepare_sim();
|
||||
this->modem_handler->dce->set_network_attachment_state(1);
|
||||
|
||||
this->modem_handler->dce->get_network_attachment_state(attachement_state);
|
||||
|
||||
if (!attachement_state) {
|
||||
ESP_LOGW(TAG, "Modem not yet ready to connect");
|
||||
this->modem_handler->modem_log_status();
|
||||
this->loop_delay_(4000);
|
||||
return ModemComponentState::MODEM_CONNECTING;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Modem initialized and ready, starting PPP");
|
||||
|
||||
if (!this->modem_handler->dce->set_mode(esp_modem::modem_mode::CMUX_MODE)) {
|
||||
ESP_LOGE(TAG, "Failed to enter PPP. Resetting modem.");
|
||||
return ModemComponentState::MODEM_DISCONNECTING;
|
||||
}
|
||||
|
||||
return ModemComponentState::MODEM_WAIT_IP;
|
||||
}
|
||||
|
||||
ModemComponentState ModemComponent::handle_state_wait_ip_() {
|
||||
if (this->modem_handler->network_infos.got_ip) {
|
||||
return ModemComponentState::MODEM_CONNECTED;
|
||||
}
|
||||
if (this->wait_ip_retry_ > 0) {
|
||||
--this->wait_ip_retry_;
|
||||
}
|
||||
if (this->wait_ip_retry_ > 0) {
|
||||
ESP_LOGD(TAG, "Wait IP left retry: %d", this->wait_ip_retry_);
|
||||
this->loop_delay_(this->modem_handler->connect_retry_delay);
|
||||
return ModemComponentState::MODEM_WAIT_IP;
|
||||
}
|
||||
ESP_LOGE(TAG, "Unable to get IP address");
|
||||
return ModemComponentState::MODEM_DISCONNECTING;
|
||||
}
|
||||
|
||||
ModemComponentState ModemComponent::handle_state_connected_() {
|
||||
if (!this->modem_handler->network_infos.got_ip) {
|
||||
ESP_LOGW(TAG, "Lost IP");
|
||||
this->loop_delay_(this->modem_handler->connect_retry_delay);
|
||||
return ModemComponentState::MODEM_DISCONNECTING;
|
||||
}
|
||||
return ModemComponentState::MODEM_CONNECTED;
|
||||
}
|
||||
|
||||
ModemComponentState ModemComponent::handle_state_disconnecting_() {
|
||||
this->modem_handler->dce->set_mode(modem_mode::CMUX_MANUAL_EXIT);
|
||||
|
||||
// turn off radio.It can take up to 2 min to get lost IP event.
|
||||
if (this->modem_handler->dce->set_radio_state(0) == esp_modem::command_result::OK) {
|
||||
ESP_LOGI(TAG, "Modem set to minimal functionality.");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to set modem to minimal functionality.");
|
||||
}
|
||||
return ModemComponentState::MODEM_DISABLED;
|
||||
}
|
||||
|
||||
void ModemComponent::transition_to_(ModemComponentState next_state) {
|
||||
if (next_state == this->component_state_) {
|
||||
return;
|
||||
}
|
||||
ModemComponentState previous_state = this->component_state_;
|
||||
this->component_state_ = next_state;
|
||||
ESP_LOGD(TAG, "State change: %d -> %d", static_cast<int>(previous_state), static_cast<int>(next_state));
|
||||
this->on_state_callback_.call(next_state);
|
||||
this->on_enter_state_(next_state);
|
||||
}
|
||||
|
||||
void ModemComponent::on_enter_state_(ModemComponentState state) {
|
||||
// State invariants (entry establishes these):
|
||||
// - MODEM_WAIT_IP: waiting for PPP got IP, retry counter initialized.
|
||||
// - MODEM_CONNECTED: got_ip expected, connect timeout cancelled, params dumped.
|
||||
// - DISABLED: no modem timeouts active, no retries pending.
|
||||
switch (state) {
|
||||
case ModemComponentState::MODEM_ENABLING:
|
||||
this->set_timeout(CONNECT_TIMEOUT_ID, this->timeout_,
|
||||
[this]() { this->abort_("Modem was not able to connect (timeout)"); });
|
||||
break;
|
||||
case ModemComponentState::MODEM_WAIT_IP:
|
||||
this->wait_ip_retry_ = 10;
|
||||
break;
|
||||
case ModemComponentState::MODEM_CONNECTED:
|
||||
this->cancel_timeout(CONNECT_TIMEOUT_ID);
|
||||
this->status_clear_warning();
|
||||
this->dump_connect_params_();
|
||||
break;
|
||||
case ModemComponentState::MODEM_DISCONNECTING:
|
||||
ESP_LOGI(TAG, "Disconnecting modem");
|
||||
this->cancel_timeout(CONNECT_TIMEOUT_ID);
|
||||
this->loop_delay_(this->modem_handler->command_delay);
|
||||
break;
|
||||
case ModemComponentState::MODEM_DISABLED:
|
||||
this->cancel_timeout(CONNECT_TIMEOUT_ID);
|
||||
this->wait_ip_retry_ = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ModemComponent::abort_(const char *message) {
|
||||
ESP_LOGE(TAG, "Aborting: %s.", message);
|
||||
this->pref_.save(&this->modem_restore_state_);
|
||||
App.reboot();
|
||||
}
|
||||
|
||||
void ModemComponent::loop_delay_(uint32_t delay_ms) {
|
||||
this->next_loop_millis_ = App.get_loop_component_start_time() + delay_ms;
|
||||
}
|
||||
|
||||
void ModemComponent::dump_connect_params_() {
|
||||
char buffer[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
if (this->component_state_ != ModemComponentState::MODEM_CONNECTED) {
|
||||
ESP_LOGCONFIG(TAG, "Modem connection: Not connected.");
|
||||
return;
|
||||
}
|
||||
esp_netif_ip_info_t ip = this->modem_handler->network_infos.ip_info;
|
||||
esp_netif_dns_info_t dns_main = this->modem_handler->network_infos.dns_main;
|
||||
esp_netif_dns_info_t dns_backup = this->modem_handler->network_infos.dns_backup;
|
||||
|
||||
ESP_LOGCONFIG(TAG, "Modem connection:");
|
||||
ESP_LOGCONFIG(TAG, " IP Address : %s", network::IPAddress(&ip.ip).str_to(buffer));
|
||||
ESP_LOGCONFIG(TAG, " Hostname : '%s'", App.get_name().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Subnet : %s", network::IPAddress(&ip.netmask).str_to(buffer));
|
||||
ESP_LOGCONFIG(TAG, " Gateway : %s", network::IPAddress(&ip.gw).str_to(buffer));
|
||||
ESP_LOGCONFIG(TAG, " DNS main : %s", network::IPAddress(&dns_main.ip.u_addr.ip4).str_to(buffer));
|
||||
ESP_LOGCONFIG(TAG, " DNS backup : %s", network::IPAddress(&dns_backup.ip.u_addr.ip4).str_to(buffer));
|
||||
}
|
||||
|
||||
} // namespace esphome::modem
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#ifdef USE_MODEM_URC
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include "modem_handler.h"
|
||||
namespace esphome::modem {
|
||||
|
||||
enum class ModemComponentState {
|
||||
MODEM_DISABLED,
|
||||
MODEM_ENABLING,
|
||||
MODEM_SYNCED,
|
||||
MODEM_CONNECTING, // Merges INIT_NETWORK + START_PPP
|
||||
MODEM_WAIT_IP,
|
||||
MODEM_CONNECTED,
|
||||
MODEM_DISCONNECTING, // Renamed from DISABLING
|
||||
};
|
||||
|
||||
struct ModemRestoreState {
|
||||
int baud_rate{0};
|
||||
} __attribute__((packed));
|
||||
|
||||
class ModemComponent : public Component {
|
||||
public:
|
||||
void set_reboot_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
// Setters now modify the handler's attributes
|
||||
void set_rx_pin(InternalGPIOPin *rx_pin) { this->modem_handler->rx_pin = rx_pin; }
|
||||
void set_tx_pin(InternalGPIOPin *tx_pin) { this->modem_handler->tx_pin = tx_pin; }
|
||||
void set_rts_pin(InternalGPIOPin *rts_pin) { this->modem_handler->rts_pin = rts_pin; }
|
||||
void set_cts_pin(InternalGPIOPin *cts_pin) { this->modem_handler->cts_pin = cts_pin; }
|
||||
void set_baud_rate(int baud_rate) { this->modem_handler->baud_rate = baud_rate; }
|
||||
void set_model(const std::string &model) { this->modem_handler->model = model; }
|
||||
void set_pin_code(const std::string &pin_code) { this->modem_handler->pin_code = pin_code; }
|
||||
void set_tx_buffer_size(uint16_t tx_buffer_size) { this->modem_handler->tx_buffer_size = tx_buffer_size; }
|
||||
void set_rx_buffer_size(uint16_t rx_buffer_size) { this->modem_handler->rx_buffer_size = rx_buffer_size; }
|
||||
void set_dte_buffer_size(uint16_t dte_buffer_size) { this->modem_handler->dte_buffer_size = dte_buffer_size; }
|
||||
void set_apn(const std::string &apn) { this->modem_handler->apn = apn; }
|
||||
void add_init_at_command(const std::string &cmd) { this->modem_handler->init_at_commands.push_back(cmd); }
|
||||
#ifdef USE_MODEM_URC
|
||||
void set_urc_text_sensor(text_sensor::TextSensor *urc_text_sensor) { this->urc_text_sensor = urc_text_sensor; }
|
||||
text_sensor::TextSensor *urc_text_sensor{nullptr};
|
||||
#endif
|
||||
bool is_connected() { return this->component_state_ == ModemComponentState::MODEM_CONNECTED; }
|
||||
bool is_disabled() {
|
||||
return this->component_state_ == ModemComponentState::MODEM_DISABLED &&
|
||||
this->target_state_ == ModemComponentState::MODEM_DISABLED;
|
||||
}
|
||||
bool is_enabled() { return !is_disabled(); }
|
||||
|
||||
// Delegated methods
|
||||
AtCommandResult send_at(const std::string &cmd, uint32_t timeout = 0, bool verbose = false);
|
||||
void enable();
|
||||
void disable();
|
||||
|
||||
network::IPAddresses get_ip_addresses();
|
||||
const char *get_use_address() const { return this->use_address_; };
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases, you won't need these)
|
||||
|
||||
ModemComponent();
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
|
||||
void dump_config() override { this->dump_connect_params_(); }
|
||||
float get_setup_priority() const override { return setup_priority::WIFI + 1; } // Just before Wi-Fi
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->on_state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
std::unique_ptr<ModemHandler> modem_handler{nullptr};
|
||||
|
||||
protected:
|
||||
// ===== State handler methods =====
|
||||
ModemComponentState handle_state_disabled_();
|
||||
ModemComponentState handle_state_enabling_();
|
||||
ModemComponentState handle_state_synced_();
|
||||
ModemComponentState handle_state_connecting_(); // Merges INIT_NETWORK + START_PPP
|
||||
ModemComponentState handle_state_wait_ip_();
|
||||
ModemComponentState handle_state_connected_();
|
||||
ModemComponentState handle_state_disconnecting_();
|
||||
|
||||
void transition_to_(ModemComponentState next_state);
|
||||
void on_enter_state_(ModemComponentState state);
|
||||
|
||||
// target_state_ model helpers
|
||||
ModemComponentState compute_next_state_();
|
||||
bool is_going_up_() const { return target_state_ == ModemComponentState::MODEM_CONNECTED; }
|
||||
bool is_going_down_() const { return target_state_ == ModemComponentState::MODEM_DISABLED; }
|
||||
|
||||
void abort_(const char *message);
|
||||
void loop_delay_(uint32_t delay_ms);
|
||||
void dump_connect_params_();
|
||||
|
||||
// Scheduler id of the connect timeout armed while enabling
|
||||
static constexpr uint32_t CONNECT_TIMEOUT_ID = 0;
|
||||
|
||||
// Attributes from YAML config
|
||||
uint32_t timeout_;
|
||||
CallbackManager<void(ModemComponentState)> on_state_callback_;
|
||||
|
||||
// Changes will trigger user callback
|
||||
ModemComponentState component_state_{ModemComponentState::MODEM_DISABLED};
|
||||
ModemComponentState target_state_{ModemComponentState::MODEM_DISABLED}; // Stable target state
|
||||
|
||||
uint32_t next_loop_millis_{0};
|
||||
uint8_t wait_ip_retry_{0};
|
||||
|
||||
ModemRestoreState modem_restore_state_{};
|
||||
ESPPreferenceObject pref_;
|
||||
|
||||
private:
|
||||
// Stores a pointer to a string literal (static storage duration).
|
||||
// ONLY set from Python-generated code with string literals - never dynamic strings.
|
||||
const char *use_address_{""};
|
||||
};
|
||||
|
||||
extern ModemComponent *global_modem_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome::modem
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,189 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "modem_handler.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::modem {
|
||||
|
||||
using namespace esp_modem;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "modem_handler");
|
||||
|
||||
void ModemHandler::modem_create_dte_dce(int baud_rate) {
|
||||
this->current_baud_rate = baud_rate;
|
||||
|
||||
esp_modem_dte_config_t dte_config = ESP_MODEM_DTE_DEFAULT_CONFIG();
|
||||
|
||||
dte_config.uart_config.tx_io_num = this->tx_pin->get_pin();
|
||||
dte_config.uart_config.rx_io_num = this->rx_pin->get_pin();
|
||||
dte_config.uart_config.rx_buffer_size = this->rx_buffer_size;
|
||||
dte_config.uart_config.tx_buffer_size = this->tx_buffer_size;
|
||||
dte_config.uart_config.event_queue_size = this->uart_event_queue_size;
|
||||
dte_config.uart_config.port_num = this->uart_port_num;
|
||||
if (baud_rate != 0) {
|
||||
ESP_LOGD(TAG, "DTE baud rate: %d", baud_rate);
|
||||
dte_config.uart_config.baud_rate = baud_rate;
|
||||
}
|
||||
if (this->rts_pin != nullptr && this->cts_pin != nullptr) {
|
||||
ESP_LOGD(TAG, "Using RTS/CTS hardware flow control");
|
||||
dte_config.uart_config.rts_io_num = this->rts_pin->get_pin();
|
||||
dte_config.uart_config.cts_io_num = this->cts_pin->get_pin();
|
||||
dte_config.uart_config.flow_control = ESP_MODEM_FLOW_CONTROL_HW;
|
||||
} else {
|
||||
dte_config.uart_config.flow_control = ESP_MODEM_FLOW_CONTROL_NONE;
|
||||
}
|
||||
dte_config.task_stack_size = this->uart_event_task_stack_size;
|
||||
dte_config.task_priority = this->uart_event_task_priority;
|
||||
dte_config.dte_buffer_size = this->dte_buffer_size;
|
||||
|
||||
this->dce.reset();
|
||||
this->dte.reset();
|
||||
this->dte = create_uart_dte(&dte_config);
|
||||
|
||||
esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG(this->apn.c_str());
|
||||
|
||||
if (this->model == "GENERIC") {
|
||||
this->dce = create_generic_dce(&dce_config, this->dte, this->ppp_netif);
|
||||
} else if (this->model == "BG96") {
|
||||
this->dce = create_BG96_dce(&dce_config, this->dte, this->ppp_netif);
|
||||
} else if (this->model == "SIM800") {
|
||||
this->dce = create_SIM800_dce(&dce_config, this->dte, this->ppp_netif);
|
||||
} else if (this->model == "SIM7000" || this->model == "SIM7080") {
|
||||
this->dce = create_SIM7000_dce(&dce_config, this->dte, this->ppp_netif);
|
||||
} else if (this->model == "SIM7070") {
|
||||
this->dce = create_SIM7070_dce(&dce_config, this->dte, this->ppp_netif);
|
||||
} else if (this->model == "SIM7600" || this->model == "SIM7670") {
|
||||
this->dce = create_SIM7600_dce(&dce_config, this->dte, this->ppp_netif);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid model %s. DCE not created.", this->model.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "DTE/DCE created.");
|
||||
}
|
||||
|
||||
AtCommandResult ModemHandler::send_at(const std::string &cmd, uint32_t timeout, bool verbose) {
|
||||
const uint32_t cmd_timeout = timeout == 0 ? this->command_delay : timeout;
|
||||
AtCommandResult at_command_result;
|
||||
at_command_result.success = false;
|
||||
at_command_result.esp_modem_command_result = command_result::TIMEOUT;
|
||||
if (this->dce) {
|
||||
at_command_result.esp_modem_command_result = this->dce->at(cmd, at_command_result.output, cmd_timeout);
|
||||
at_command_result.success = at_command_result.esp_modem_command_result == command_result::OK;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Modem DCE not ready, cannot send AT command: %s", cmd.c_str());
|
||||
at_command_result.esp_modem_command_result = command_result::FAIL;
|
||||
}
|
||||
return at_command_result;
|
||||
}
|
||||
|
||||
bool ModemHandler::get_signal_quality(float &out_rssi, float &out_ber) {
|
||||
out_rssi = NAN;
|
||||
out_ber = NAN;
|
||||
int modem_rssi = 99;
|
||||
int modem_ber = 99;
|
||||
if (this->dce && this->dce->sync() == command_result::OK &&
|
||||
(this->dce->get_signal_quality(modem_rssi, modem_ber) == command_result::OK)) {
|
||||
if (modem_rssi != 99)
|
||||
out_rssi = -113.0f + (modem_rssi * 2);
|
||||
if (modem_ber != 99)
|
||||
out_ber = 0.1f * (modem_ber * modem_ber);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ModemHandler::modem_log_status() {
|
||||
bool synced = false;
|
||||
int cfun = -1;
|
||||
int attached = 0;
|
||||
int network_mode = 0;
|
||||
int registration_state = 0;
|
||||
float rssi = NAN, ber = NAN;
|
||||
std::string sim_status = "UNKNOWN";
|
||||
if (this->dce && this->dce->sync() == esp_modem::command_result::OK) {
|
||||
synced = true;
|
||||
auto res = this->send_at("AT+CPIN?");
|
||||
if (res) {
|
||||
auto &out = res.output;
|
||||
sim_status = out.starts_with("+CPIN: ") ? out.substr(7) : out;
|
||||
}
|
||||
this->dce->get_radio_state(cfun);
|
||||
this->dce->get_network_attachment_state(attached);
|
||||
this->dce->get_network_registration_state(registration_state);
|
||||
this->get_signal_quality(rssi, ber);
|
||||
this->dce->get_network_system_mode(network_mode);
|
||||
}
|
||||
bool connected =
|
||||
synced && network_mode != 0 && attached && !std::isnan(rssi) && sim_status.find("READY") != std::string::npos;
|
||||
char cfun_str[16];
|
||||
if (cfun == 1) {
|
||||
snprintf(cfun_str, sizeof(cfun_str), "OK");
|
||||
} else {
|
||||
snprintf(cfun_str, sizeof(cfun_str), "NOK(%d)", cfun);
|
||||
}
|
||||
ESP_LOGI(TAG,
|
||||
"Modem status: %s, attached: %s, registration state: %d, radio function: %s, SIM: %s, type: %d, ber: %.0f "
|
||||
"%%, rssi: %.0f dB",
|
||||
connected ? LOG_STR_LITERAL("Good") : (synced ? LOG_STR_LITERAL("BAD") : LOG_STR_LITERAL("No SYNC")),
|
||||
attached ? LOG_STR_LITERAL("Yes") : LOG_STR_LITERAL("NO"), registration_state, cfun_str, sim_status.c_str(),
|
||||
(int) network_mode, ber * 100.0f, rssi);
|
||||
}
|
||||
|
||||
bool ModemHandler::prepare_sim() {
|
||||
App.feed_wdt();
|
||||
delay(200); // NOLINT
|
||||
AtCommandResult result = this->send_at("AT+CPIN?");
|
||||
if (!result.success) {
|
||||
ESP_LOGW(TAG, "Failed to check pin: %s", result.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string current_sim_status = result.output.starts_with("+CPIN: ") ? result.output.substr(7) : result.output;
|
||||
ESP_LOGD(TAG, "SIM: %s", current_sim_status.c_str());
|
||||
|
||||
if (current_sim_status.find("READY") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this->pin_code.empty()) {
|
||||
ESP_LOGE(TAG, "SIM PIN empty, but SIM locked.");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Unlocking SIM with PIN...");
|
||||
if (this->dce->set_pin(this->pin_code) == command_result::OK) {
|
||||
ESP_LOGD(TAG, "PIN set. (But not yet checked).");
|
||||
return true;
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "Failed to set PIN.");
|
||||
return false;
|
||||
}
|
||||
|
||||
void ModemHandler::ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) {
|
||||
auto *handler = static_cast<ModemHandler *>(arg);
|
||||
|
||||
switch (event_id) {
|
||||
case IP_EVENT_PPP_GOT_IP: {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
|
||||
ESP_LOGD(TAG, "PPP IP event: Got IP " IPSTR, IP2STR(&event->ip_info.ip));
|
||||
handler->network_infos.ip_info = event->ip_info;
|
||||
esp_netif_get_dns_info(event->esp_netif, ESP_NETIF_DNS_MAIN, &handler->network_infos.dns_main);
|
||||
esp_netif_get_dns_info(event->esp_netif, ESP_NETIF_DNS_BACKUP, &handler->network_infos.dns_backup);
|
||||
handler->network_infos.got_ip = true;
|
||||
break;
|
||||
}
|
||||
case IP_EVENT_PPP_LOST_IP:
|
||||
if (handler->network_infos.got_ip) {
|
||||
ESP_LOGD(TAG, "PPP IP event: Lost IP.");
|
||||
handler->network_infos.got_ip = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::modem
|
||||
#endif
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <memory>
|
||||
#include "esphome/core/gpio.h"
|
||||
#include <cxx_include/esp_modem_api.hpp>
|
||||
#include <esp_modem_config.h>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::modem {
|
||||
|
||||
using namespace esp_modem;
|
||||
|
||||
struct AtCommandResult {
|
||||
std::string output{};
|
||||
bool success{false};
|
||||
command_result esp_modem_command_result{command_result::TIMEOUT};
|
||||
operator bool() const { return success; }
|
||||
const char *c_str() const { return output.c_str(); }
|
||||
};
|
||||
|
||||
struct NetworkInfos {
|
||||
esp_netif_ip_info_t ip_info{};
|
||||
esp_netif_dns_info_t dns_main{};
|
||||
esp_netif_dns_info_t dns_backup{};
|
||||
bool got_ip{false};
|
||||
};
|
||||
|
||||
class ModemHandler {
|
||||
public:
|
||||
// Attributes from YAML config
|
||||
InternalGPIOPin *tx_pin;
|
||||
InternalGPIOPin *rx_pin;
|
||||
InternalGPIOPin *rts_pin{nullptr};
|
||||
InternalGPIOPin *cts_pin{nullptr};
|
||||
std::string model;
|
||||
uint16_t tx_buffer_size = 512;
|
||||
uint16_t rx_buffer_size = 512;
|
||||
uint16_t dte_buffer_size = 512;
|
||||
std::string pin_code;
|
||||
std::string apn;
|
||||
std::vector<std::string> init_at_commands;
|
||||
int baud_rate = 0;
|
||||
|
||||
// esp_modem objects
|
||||
std::unique_ptr<DCE> dce{nullptr};
|
||||
std::shared_ptr<DTE> dte{nullptr};
|
||||
esp_netif_t *ppp_netif{nullptr};
|
||||
|
||||
// Internal state attributes
|
||||
int current_baud_rate{0};
|
||||
NetworkInfos network_infos{};
|
||||
|
||||
// UART config
|
||||
uint8_t uart_event_queue_size = 10;
|
||||
size_t uart_event_task_stack_size = 4096;
|
||||
uint8_t uart_event_task_priority = 5;
|
||||
uint32_t command_delay = 1000;
|
||||
uint32_t connect_retry_delay = 5000;
|
||||
uart_port_t uart_port_num = UART_NUM_MAX;
|
||||
|
||||
// Methods
|
||||
void modem_create_dte_dce(int baud_rate);
|
||||
AtCommandResult send_at(const std::string &cmd, uint32_t timeout = 0, bool verbose = false);
|
||||
bool get_signal_quality(float &out_rssi, float &out_ber);
|
||||
static void ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
|
||||
void modem_log_status();
|
||||
bool prepare_sim();
|
||||
};
|
||||
|
||||
} // namespace esphome::modem
|
||||
#endif
|
||||
@@ -25,6 +25,8 @@ dependencies:
|
||||
version: 1.1.2
|
||||
espressif/esp32-camera:
|
||||
version: 2.1.7
|
||||
espressif/esp_modem:
|
||||
version: 2.0.2
|
||||
espressif/mdns:
|
||||
version: 1.14.0
|
||||
espressif/esp_wifi_remote:
|
||||
|
||||
@@ -26,3 +26,7 @@ CONFIG_BT_ENABLED=y
|
||||
|
||||
# esp32_camera
|
||||
CONFIG_SPIRAM=y
|
||||
|
||||
# modem
|
||||
CONFIG_PPP_SUPPORT=y
|
||||
CONFIG_ESP_MODEM_URC_HANDLER=y
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- modem.disable
|
||||
- modem.enable
|
||||
- if:
|
||||
condition:
|
||||
modem.enabled:
|
||||
then:
|
||||
modem.send_at: "AT+CGSN"
|
||||
else:
|
||||
modem.enable:
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
modem.connected:
|
||||
then:
|
||||
logger.log: modem not connected
|
||||
@@ -0,0 +1,21 @@
|
||||
logger:
|
||||
|
||||
modem:
|
||||
id: atmodem
|
||||
rx_pin: GPIO26
|
||||
tx_pin: GPIO27
|
||||
baud_rate: 115200
|
||||
model: SIM7600
|
||||
apn: orange
|
||||
pin_code: "0000"
|
||||
enable_on_boot: True
|
||||
reboot_timeout: 5min
|
||||
on_connect:
|
||||
logger.log: "modem connected"
|
||||
on_disconnect:
|
||||
logger.log: "modem disconnected"
|
||||
on_sync:
|
||||
logger.log: "modem synced - ready for AT commands"
|
||||
|
||||
packages:
|
||||
modem: !include common.yaml
|
||||
Reference in New Issue
Block a user