From 181beb08523918069161ab18bd1280bb0fcb330e Mon Sep 17 00:00:00 2001 From: oarcher Date: Sun, 11 Aug 2024 15:42:08 +0200 Subject: [PATCH] gnss as a switch --- CODEOWNERS | 1 + esphome/components/modem/__init__.py | 15 ----- esphome/components/modem/modem_component.cpp | 22 ++------ esphome/components/modem/modem_component.h | 9 +-- esphome/components/modem/sensor/__init__.py | 32 ++++++----- .../components/modem/sensor/modem_sensor.cpp | 18 +++--- .../components/modem/sensor/modem_sensor.h | 6 +- esphome/components/modem/switch/__init__.py | 56 +++++++++++++++++++ esphome/components/modem/switch/gnns_switch.h | 37 ++++++++++++ .../components/modem/switch/gnss_switch.cpp | 53 ++++++++++++++++++ .../components/modem/text_sensor/__init__.py | 11 +--- .../modem/text_sensor/modem_text_sensor.cpp | 6 +- .../modem/text_sensor/modem_text_sensor.h | 6 +- 13 files changed, 195 insertions(+), 77 deletions(-) create mode 100644 esphome/components/modem/switch/__init__.py create mode 100644 esphome/components/modem/switch/gnns_switch.h create mode 100644 esphome/components/modem/switch/gnss_switch.cpp diff --git a/CODEOWNERS b/CODEOWNERS index d03c2bb1d1..3dc481664b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -262,6 +262,7 @@ esphome/components/modbus_controller/switch/* @martgras esphome/components/modbus_controller/text_sensor/* @martgras esphome/components/modem/* @oarcher esphome/components/modem/sensor/* @oarcher +esphome/components/modem/switch/* @oarcher esphome/components/modem/text_sensor/* @oarcher esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan esphome/components/mopeka_pro_check/* @spbrogan diff --git a/esphome/components/modem/__init__.py b/esphome/components/modem/__init__.py index edda7753df..ce50e557bd 100644 --- a/esphome/components/modem/__init__.py +++ b/esphome/components/modem/__init__.py @@ -40,7 +40,6 @@ CONF_POWER_PIN = "power_pin" CONF_INIT_AT = "init_at" CONF_ON_NOT_RESPONDING = "on_not_responding" CONF_ENABLE_CMUX = "enable_cmux" -CONF_ENABLE_GNSS = "enable_gnss" MODEM_MODELS = ["BG96", "SIM800", "SIM7000", "SIM7600", "SIM7670", "GENERIC"] MODEM_MODELS_POWER = { @@ -52,10 +51,6 @@ MODEM_MODELS_POWER = { MODEM_MODELS_POWER["SIM7670"] = MODEM_MODELS_POWER["SIM7600"] -# SIM70xx doesn't support AT+CGNSSINFO, so gnss is not available -MODEM_MODELS_GNSS_POWER = {"SIM7600": "AT+CGPS=1", "SIM7670": "AT+CGNSSPWR=1"} - - modem_ns = cg.esphome_ns.namespace("modem") ModemComponent = modem_ns.class_("ModemComponent", cg.Component) ModemComponentState = modem_ns.enum("ModemComponentState") @@ -69,7 +64,6 @@ ModemOnDisconnectTrigger = modem_ns.class_( "ModemOnDisconnectTrigger", automation.Trigger.template() ) - CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -89,7 +83,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ENABLE_CMUX, default=False): cv.boolean, cv.Optional(CONF_DEBUG, default=False): cv.boolean, # needs also - cv.Optional(CONF_ENABLE_GNSS, default=False): cv.boolean, cv.Optional(CONF_ON_NOT_RESPONDING): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -138,11 +131,6 @@ def _final_validate(config): raise cv.Invalid( f"Modem model '{config[CONF_MODEL]}' has no power power specs." ) - if config.get(CONF_ENABLE_GNSS, None): - if config[CONF_MODEL] not in MODEM_MODELS_GNSS_POWER: - raise cv.Invalid( - f"Modem model '{config[CONF_MODEL]}' has no GNSS support with AT+CGNSSINFO." - ) FINAL_VALIDATE_SCHEMA = _final_validate @@ -199,9 +187,6 @@ async def to_code(config): modem_model = config[CONF_MODEL] cg.add(var.set_model(modem_model)) - if config[CONF_ENABLE_GNSS]: - cg.add(var.set_gnss_power_command(MODEM_MODELS_GNSS_POWER[modem_model])) - if power_spec := MODEM_MODELS_POWER.get(modem_model, None): cg.add(var.set_power_ton(power_spec["ton"])) cg.add(var.set_power_tonuart(power_spec["tonuart"])) diff --git a/esphome/components/modem/modem_component.cpp b/esphome/components/modem/modem_component.cpp index 6c94241099..0dcc75836b 100644 --- a/esphome/components/modem/modem_component.cpp +++ b/esphome/components/modem/modem_component.cpp @@ -39,6 +39,8 @@ namespace modem { using namespace esp_modem; +static const char *const TAG = "modem"; + ModemComponent *global_modem_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) ModemComponent::ModemComponent() { @@ -49,7 +51,7 @@ ModemComponent::ModemComponent() { void ModemComponent::enable_debug() { esp_log_level_set("command_lib", ESP_LOG_VERBOSE); } std::string ModemComponent::send_at(const std::string &cmd) { - std::string result = "ERROR"; + std::string result = ""; command_result status = command_result::FAIL; ESP_LOGV(TAG, "Sending command: %s", cmd.c_str()); if (this->modem_ready()) { @@ -57,6 +59,9 @@ std::string ModemComponent::send_at(const std::string &cmd) { ESP_LOGV(TAG, "Result for command %s: %s (status %s)", cmd.c_str(), result.c_str(), command_result_to_string(status).c_str()); } + if (status != command_result::OK) { + result = "ERROR"; + } return result; } @@ -547,19 +552,6 @@ bool ModemComponent::modem_sync_() { // First time the modem is synced, or modem recovered this->internal_state_.modem_synced = true; - if (!this->gnss_power_command_.empty()) { - command_result err; - ESP_LOGD(TAG, "Enabling GNSS with command: %s", this->gnss_power_command_.c_str()); - err = this->dce->at(this->gnss_power_command_, result, this->command_delay_); - if (err == command_result::FAIL) { - // AT+CGPS=1 for SIM7600 or AT+CGNSSPWR=1 for SIM7670 often fail, but seems to be working anyway - ESP_LOGD(TAG, "GNSS power command failed. Ignoring, as the status is often FAIL, while it works later."); - } - } - // ESPMODEM_ERROR_CHECK(this->dce->set_gnss_power_mode(0), "Enabling/disabling GNSS"); - - // delay(200); // NOLINT - if (!this->prepare_sim_()) { // fatal error this->disable(); @@ -567,8 +559,6 @@ bool ModemComponent::modem_sync_() { } this->send_init_at_(); - // ESPMODEM_ERROR_CHECK(this->dce->set_gnss_power_mode(this->gnss_), "Enabling/disabling GNSS"); - ESP_LOGI(TAG, "Modem infos:"); std::string result; ESPMODEM_ERROR_CHECK(this->dce->get_module_name(result), "get_module_name"); diff --git a/esphome/components/modem/modem_component.h b/esphome/components/modem/modem_component.h index c3c8238f0e..9a04660bb1 100644 --- a/esphome/components/modem/modem_component.h +++ b/esphome/components/modem/modem_component.h @@ -28,8 +28,6 @@ namespace modem { using namespace esp_modem; -static const char *const TAG = "modem"; - enum class ModemComponentState { NOT_RESPONDING, DISCONNECTED, @@ -49,7 +47,7 @@ class ModemComponent : public Component { void set_use_address(const std::string &use_address) { this->use_address_ = use_address; } void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; } void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; } - void set_model(const std::string model) { this->model_ = model; } + void set_model(const std::string &model) { this->model_ = model; } void set_power_pin(GPIOPin *power_pin) { this->power_pin_ = power_pin; } void set_power_ton(int ton) { this->power_ton_ = ton; } void set_power_tonuart(int tonuart) { this->power_tonuart_ = tonuart; } @@ -60,8 +58,8 @@ class ModemComponent : public Component { void set_password(const std::string &password) { this->password_ = password; } void set_pin_code(const std::string &pin_code) { this->pin_code_ = pin_code; } void set_apn(const std::string &apn) { this->apn_ = apn; } - void set_gnss_power_command(const std::string &at_command) { this->gnss_power_command_ = at_command; } - std::string get_gnss_power_command() { return this->gnss_power_command_; } + // void set_gnss_power_command(const std::string &at_command) { this->gnss_power_command_ = at_command; } + // std::string get_gnss_power_command() { return this->gnss_power_command_; } void set_not_responding_cb(Trigger<> *not_responding_cb) { this->not_responding_cb_ = not_responding_cb; } void enable_cmux() { this->cmux_ = true; } void enable_debug(); @@ -130,7 +128,6 @@ class ModemComponent : public Component { std::vector init_at_commands_; std::string use_address_; bool cmux_{false}; - std::string gnss_power_command_; // separate handler for `on_not_responding` (we want to know when it's ended) Trigger<> *not_responding_cb_{nullptr}; CallbackManager on_state_callback_; diff --git a/esphome/components/modem/sensor/__init__.py b/esphome/components/modem/sensor/__init__.py index c094e92373..a92408938b 100644 --- a/esphome/components/modem/sensor/__init__.py +++ b/esphome/components/modem/sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_ID, CONF_LATITUDE, CONF_LONGITUDE, + CONF_PLATFORM, CONF_SPEED, DEVICE_CLASS_SIGNAL_STRENGTH, ENTITY_CATEGORY_DIAGNOSTIC, @@ -20,7 +21,7 @@ from esphome.const import ( ) import esphome.final_validate as fv -from .. import CONF_ENABLE_GNSS, CONF_MODEM, final_validate_platform +from .. import CONF_MODEM, final_validate_platform, modem_ns, switch CODEOWNERS = ["@oarcher"] @@ -28,7 +29,6 @@ AUTO_LOAD = [] DEPENDENCIES = ["modem"] -# MULTI_CONF = True IS_PLATFORM_COMPONENT = True CONF_BER = "ber" @@ -43,14 +43,13 @@ ICON_LOCATION_UP = "mdi:map-marker-up" ICON_SPEED = "mdi:speedometer" ICON_SIGNAL_BAR = "mdi:signal" -modem_sensor_ns = cg.esphome_ns.namespace("modem_sensor") -ModemSensorComponent = modem_sensor_ns.class_("ModemSensor", cg.PollingComponent) +ModemSensor = modem_ns.class_("ModemSensor", cg.PollingComponent) CONFIG_SCHEMA = cv.All( cv.Schema( { - cv.GenerateID(): cv.declare_id(ModemSensorComponent), + cv.GenerateID(): cv.declare_id(ModemSensor), cv.Optional(CONF_RSSI): sensor.sensor_schema( unit_of_measurement=UNIT_DECIBEL, accuracy_decimals=0, @@ -108,16 +107,19 @@ CONFIG_SCHEMA = cv.All( def _final_validate_gnss(config): - if ( - config.get(CONF_LATITUDE, None) - or config.get(CONF_LONGITUDE, None) - or config.get(CONF_ALTITUDE, None) - ): - if modem_config := fv.full_config.get().get(CONF_MODEM, None): - if not modem_config[CONF_ENABLE_GNSS]: - raise cv.Invalid( - f"Using GNSS sensors require '{CONF_ENABLE_GNSS}' to be 'true' in '{CONF_MODEM}'." - ) + # GNSS sensors needs GNSS switch + if config.get(CONF_LATITUDE, None) or config.get(CONF_LONGITUDE, None): + gnss = False + if switches := fv.full_config.get().get("switch", None): + modem_switches = filter( + lambda x: x.get(CONF_PLATFORM, None) and x[CONF_PLATFORM] == CONF_MODEM, + switches, + ) + for sw in modem_switches: + if switch.CONF_GNSS in sw: + gnss = True + if not gnss: + raise cv.Invalid("Using GNSS modem sensors require GNSS modem switch.") return config diff --git a/esphome/components/modem/sensor/modem_sensor.cpp b/esphome/components/modem/sensor/modem_sensor.cpp index 9ab8429f25..096a53f6e2 100644 --- a/esphome/components/modem/sensor/modem_sensor.cpp +++ b/esphome/components/modem/sensor/modem_sensor.cpp @@ -29,7 +29,9 @@ } namespace esphome { -namespace modem_sensor { +namespace modem { + +static const char *const TAG = "modem.sensor"; using namespace esp_modem; @@ -40,9 +42,7 @@ void ModemSensor::update() { if (modem::global_modem_component->dce && modem::global_modem_component->modem_ready()) { this->update_signal_sensors_(); App.feed_wdt(); - if (!modem::global_modem_component->get_gnss_power_command().empty()) { - this->update_gnss_sensors_(); - } + this->update_gnss_sensors_(); } } @@ -90,7 +90,9 @@ std::map get_gnssinfo_tokens(const std::string &gnss_i switch (parts.size()) { case 15: - parts.push_back(""); + parts.emplace_back(""); + // NOLINTNEXTLINE(bugprone-branch-clone) + // fall through case 16: gnss_data["mode"] = parts[0]; gnss_data["sat_used_count"] = parts[1]; @@ -112,7 +114,9 @@ std::map get_gnssinfo_tokens(const std::string &gnss_i break; case 17: - parts.push_back(""); + parts.emplace_back(""); + // NOLINTNEXTLINE(bugprone-branch-clone) + // fall through case 18: gnss_data["mode"] = parts[0]; gnss_data["sat_used_count"] = parts[1]; @@ -232,7 +236,7 @@ void ModemSensor::update_gnss_sensors_() { } } -} // namespace modem_sensor +} // namespace modem } // namespace esphome #endif // USE_MODEM diff --git a/esphome/components/modem/sensor/modem_sensor.h b/esphome/components/modem/sensor/modem_sensor.h index d604e73a9d..5f41593768 100644 --- a/esphome/components/modem/sensor/modem_sensor.h +++ b/esphome/components/modem/sensor/modem_sensor.h @@ -11,9 +11,7 @@ #include "esphome/components/sensor/sensor.h" namespace esphome { -namespace modem_sensor { - -static const char *const TAG = "modem_sensor"; +namespace modem { class ModemSensor : public PollingComponent { public: @@ -49,7 +47,7 @@ class ModemSensor : public PollingComponent { void update_gnss_sensors_(); }; -} // namespace modem_sensor +} // namespace modem } // namespace esphome #endif // USE_MODEM diff --git a/esphome/components/modem/switch/__init__.py b/esphome/components/modem/switch/__init__.py new file mode 100644 index 0000000000..8ab043b989 --- /dev/null +++ b/esphome/components/modem/switch/__init__.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import DEVICE_CLASS_SWITCH +import esphome.final_validate as fv + +from .. import CONF_MODEL, CONF_MODEM, final_validate_platform, modem_ns + +CODEOWNERS = ["@oarcher"] + +AUTO_LOAD = [] + +DEPENDENCIES = ["modem"] + +IS_PLATFORM_COMPONENT = True + +CONF_GNSS = "gnss" +CONF_GNSS_COMMAND = "gnss_command" + +ICON_SATELLITE = "mdi:satellite-variant" + +GnssSwitch = modem_ns.class_("GnssSwitch", switch.Switch, cg.Component) + +# SIM70xx doesn't support AT+CGNSSINFO, so gnss is not available +MODEM_MODELS_GNSS_COMMAND = {"SIM7600": "AT+CGPS", "SIM7670": "AT+CGNSSPWR"} + +CONFIG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_GNSS): switch.switch_schema( + GnssSwitch, + block_inverted=True, + device_class=DEVICE_CLASS_SWITCH, + icon=ICON_SATELLITE, + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +def _final_validate_gnss(config): + if config.get(CONF_GNSS, None): + modem_config = fv.full_config.get().get(CONF_MODEM) + modem_model = modem_config.get(CONF_MODEL, None) + if modem_model not in MODEM_MODELS_GNSS_COMMAND: + raise cv.Invalid(f"GNSS not supported for modem '{modem_model}'.") + config[CONF_GNSS_COMMAND] = MODEM_MODELS_GNSS_COMMAND[modem_model] + return config + + +FINAL_VALIDATE_SCHEMA = cv.All(final_validate_platform, _final_validate_gnss) + + +async def to_code(config): + if gnss_config := config.get(CONF_GNSS): + var = await switch.new_switch(gnss_config) + await cg.register_component(var, config) + cg.add(var.set_command(config[CONF_GNSS_COMMAND])) diff --git a/esphome/components/modem/switch/gnns_switch.h b/esphome/components/modem/switch/gnns_switch.h new file mode 100644 index 0000000000..37fcd54daf --- /dev/null +++ b/esphome/components/modem/switch/gnns_switch.h @@ -0,0 +1,37 @@ +#pragma once +#ifdef USE_ESP_IDF + +#include "esphome/core/defines.h" + +#ifdef USE_MODEM +#ifdef USE_SWITCH + +#include "esphome/core/component.h" +#include "../modem_component.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace modem { + +class GnssSwitch : public switch_::Switch, public Component { + public: + void set_command(const std::string &command) { this->command_ = command; } + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + + void dump_config() override; + void setup() override; + void loop() override; + + protected: + std::string command_; + void write_state(bool state) override; + bool modem_state_{false}; +}; + +} // namespace modem +} // namespace esphome + +#endif // USE_MODEM +#endif // USE_SWITCH +#endif // USE_ESP_IDF diff --git a/esphome/components/modem/switch/gnss_switch.cpp b/esphome/components/modem/switch/gnss_switch.cpp new file mode 100644 index 0000000000..74359650d4 --- /dev/null +++ b/esphome/components/modem/switch/gnss_switch.cpp @@ -0,0 +1,53 @@ +#ifdef USE_ESP_IDF + +#include "esphome/core/defines.h" + +#ifdef USE_MODEM +#ifdef USE_SWITCH + +#include "gnns_switch.h" + +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#include "../modem_component.h" + +#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; \ + } + +#define ESPMODEM_ERROR_CHECK(err, message) \ + if ((err) != command_result::OK) { \ + ESP_LOGE(TAG, message ": %s", command_result_to_string(err).c_str()); \ + } + +namespace esphome { +namespace modem { + +using namespace esp_modem; + +static const char *const TAG = "modem.switch"; + +void GnssSwitch::dump_config() { LOG_SWITCH("", "Modem GNSS Switch", this); } + +void GnssSwitch::setup() { this->state = this->get_initial_state_with_restore_mode().value_or(false); } + +void GnssSwitch::loop() { + if ((this->state != this->modem_state_) && global_modem_component->modem_ready()) { + global_modem_component->send_at(this->command_ + (this->state ? "=1" : "=0")); + this->modem_state_ = this->state; + this->publish_state(this->modem_state_); + } +} + +void GnssSwitch::write_state(bool state) { this->state = state; } + +} // namespace modem +} // namespace esphome + +#endif // USE_MODEM +#endif // USE_SWITCH +#endif // USE_ESP_IDF diff --git a/esphome/components/modem/text_sensor/__init__.py b/esphome/components/modem/text_sensor/__init__.py index e9bf8db953..9a3a87dd0f 100644 --- a/esphome/components/modem/text_sensor/__init__.py +++ b/esphome/components/modem/text_sensor/__init__.py @@ -3,7 +3,7 @@ from esphome.components import text_sensor import esphome.config_validation as cv from esphome.const import CONF_ID, DEVICE_CLASS_EMPTY -from .. import final_validate_platform +from .. import final_validate_platform, modem_ns CODEOWNERS = ["@oarcher"] @@ -11,21 +11,16 @@ AUTO_LOAD = [] DEPENDENCIES = ["modem"] -# MULTI_CONF = True IS_PLATFORM_COMPONENT = True CONF_NETWORK_TYPE = "network_type" -modem_text_sensor_ns = cg.esphome_ns.namespace("modem_text_sensor") -ModemTextSensorComponent = modem_text_sensor_ns.class_( - "ModemTextSensor", cg.PollingComponent -) - +ModemTextSensor = modem_ns.class_("ModemTextSensor", cg.PollingComponent) CONFIG_SCHEMA = cv.All( cv.Schema( { - cv.GenerateID(): cv.declare_id(ModemTextSensorComponent), + cv.GenerateID(): cv.declare_id(ModemTextSensor), cv.Optional(CONF_NETWORK_TYPE): text_sensor.text_sensor_schema( device_class=DEVICE_CLASS_EMPTY, ), diff --git a/esphome/components/modem/text_sensor/modem_text_sensor.cpp b/esphome/components/modem/text_sensor/modem_text_sensor.cpp index 5db9887a51..b5fa13ee8b 100644 --- a/esphome/components/modem/text_sensor/modem_text_sensor.cpp +++ b/esphome/components/modem/text_sensor/modem_text_sensor.cpp @@ -25,11 +25,13 @@ } namespace esphome { -namespace modem_text_sensor { +namespace modem { using namespace esp_modem; // using namespace esphome::modem; +static const char *const TAG = "modem.text_sensor"; + void ModemTextSensor::setup() { ESP_LOGI(TAG, "Setting up Modem Sensor..."); } void ModemTextSensor::update() { @@ -112,7 +114,7 @@ void ModemTextSensor::update_network_type_text_sensor_() { } } -} // namespace modem_text_sensor +} // namespace modem } // namespace esphome #endif // USE_MODEM diff --git a/esphome/components/modem/text_sensor/modem_text_sensor.h b/esphome/components/modem/text_sensor/modem_text_sensor.h index e173a06391..4ff714adc6 100644 --- a/esphome/components/modem/text_sensor/modem_text_sensor.h +++ b/esphome/components/modem/text_sensor/modem_text_sensor.h @@ -11,9 +11,7 @@ #include "esphome/components/text_sensor/text_sensor.h" namespace esphome { -namespace modem_text_sensor { - -static const char *const TAG = "modem_text_sensor"; +namespace modem { class ModemTextSensor : public PollingComponent { public: @@ -33,7 +31,7 @@ class ModemTextSensor : public PollingComponent { void update_network_type_text_sensor_(); }; -} // namespace modem_text_sensor +} // namespace modem } // namespace esphome #endif // USE_MODEM