gnss as a switch

This commit is contained in:
oarcher 2024-08-11 15:42:08 +02:00
parent 5d8f37aed5
commit 181beb0852
13 changed files with 195 additions and 77 deletions

View file

@ -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

View file

@ -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"]))

View file

@ -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");

View file

@ -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<std::string> 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<void(ModemComponentState, ModemComponentState)> on_state_callback_;

View file

@ -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

View file

@ -29,7 +29,9 @@
}
namespace esphome {
namespace modem_sensor {
namespace modem {
static const char *const TAG = "modem.sensor";
using namespace esp_modem;
@ -40,10 +42,8 @@ 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_();
}
}
}
void ModemSensor::update_signal_sensors_() {
@ -90,7 +90,9 @@ std::map<std::string, std::string> 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<std::string, std::string> 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

View file

@ -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

View file

@ -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]))

View file

@ -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

View file

@ -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

View file

@ -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,
),

View file

@ -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

View file

@ -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