Merge pull request #3408 from esphome/bump-2022.4.0

2022.4.0
This commit is contained in:
Jesse Hills 2022-04-21 07:42:58 +12:00 committed by GitHub
commit 993044c870
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
160 changed files with 7204 additions and 1171 deletions

View file

@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/ambv/black - repo: https://github.com/ambv/black
rev: 22.1.0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
args: args:
@ -26,7 +26,7 @@ repos:
- --branch=release - --branch=release
- --branch=beta - --branch=beta
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.31.0 rev: v2.31.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]

View file

@ -82,6 +82,7 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/homeassistant/* @OttoWinter esphome/components/homeassistant/* @OttoWinter
esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp/* @RubyBailey
esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/i2c/* @esphome/core esphome/components/i2c/* @esphome/core
esphome/components/improv_serial/* @esphome/core esphome/components/improv_serial/* @esphome/core
esphome/components/ina260/* @MrEditor97 esphome/components/ina260/* @MrEditor97
@ -151,6 +152,7 @@ esphome/components/preferences/* @esphome/core
esphome/components/psram/* @esphome/core esphome/components/psram/* @esphome/core
esphome/components/pulse_meter/* @cstaahl @stevebaxter esphome/components/pulse_meter/* @cstaahl @stevebaxter
esphome/components/pvvx_mithermometer/* @pasiz esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje esphome/components/qr_code/* @wjtje
esphome/components/radon_eye_ble/* @jeffeb3 esphome/components/radon_eye_ble/* @jeffeb3
esphome/components/radon_eye_rd200/* @jeffeb3 esphome/components/radon_eye_rd200/* @jeffeb3
@ -168,13 +170,16 @@ esphome/components/sdm_meter/* @jesserockz @polyfaces
esphome/components/sdp3x/* @Azimath esphome/components/sdp3x/* @Azimath
esphome/components/selec_meter/* @sourabhjaiswal esphome/components/selec_meter/* @sourabhjaiswal
esphome/components/select/* @esphome/core esphome/components/select/* @esphome/core
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core esphome/components/sensor/* @esphome/core
esphome/components/sgp40/* @SenexCrenshaw esphome/components/sgp40/* @SenexCrenshaw
esphome/components/shelly_dimmer/* @edge90 @rnauber
esphome/components/sht4x/* @sjtrny esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/shutdown/* @esphome/core @jsuanet
esphome/components/sim800l/* @glmnet esphome/components/sim800l/* @glmnet
esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/sm2135/* @BoukeHaarsma23
esphome/components/socket/* @esphome/core esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/spi/* @esphome/core esphome/components/spi/* @esphome/core
esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_base/* @kbx81
esphome/components/ssd1322_spi/* @kbx81 esphome/components/ssd1322_spi/* @kbx81
@ -222,4 +227,5 @@ esphome/components/whirlpool/* @glmnet
esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xpt2046/* @numo68 esphome/components/xpt2046/* @numo68

View file

@ -6,13 +6,13 @@
ARG BASEIMGTYPE=docker ARG BASEIMGTYPE=docker
# https://github.com/hassio-addons/addon-debian-base/releases # https://github.com/hassio-addons/addon-debian-base/releases
FROM ghcr.io/hassio-addons/debian-base/amd64:5.2.3 AS base-hassio-amd64 FROM ghcr.io/hassio-addons/debian-base/amd64:5.3.0 AS base-hassio-amd64
FROM ghcr.io/hassio-addons/debian-base/aarch64:5.2.3 AS base-hassio-arm64 FROM ghcr.io/hassio-addons/debian-base/aarch64:5.3.0 AS base-hassio-arm64
FROM ghcr.io/hassio-addons/debian-base/armv7:5.2.3 AS base-hassio-armv7 FROM ghcr.io/hassio-addons/debian-base/armv7:5.3.0 AS base-hassio-armv7
# https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye # https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye
FROM debian:bullseye-20220125-slim AS base-docker-amd64 FROM debian:bullseye-20220328-slim AS base-docker-amd64
FROM debian:bullseye-20220125-slim AS base-docker-arm64 FROM debian:bullseye-20220328-slim AS base-docker-arm64
FROM debian:bullseye-20220125-slim AS base-docker-armv7 FROM debian:bullseye-20220328-slim AS base-docker-armv7
# Use TARGETARCH/TARGETVARIANT defined by docker # Use TARGETARCH/TARGETVARIANT defined by docker
# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
@ -23,7 +23,7 @@ RUN \
# Use pinned versions so that we get updates with build caching # Use pinned versions so that we get updates with build caching
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
python3=3.9.2-3 \ python3=3.9.2-3 \
python3-pip=20.3.4-4 \ python3-pip=20.3.4-4+deb11u1 \
python3-setuptools=52.0.0-4 \ python3-setuptools=52.0.0-4 \
python3-pil=8.1.2+dfsg-0.3+deb11u1 \ python3-pil=8.1.2+dfsg-0.3+deb11u1 \
python3-cryptography=3.3.2-1 \ python3-cryptography=3.3.2-1 \

View file

@ -262,21 +262,16 @@ async def repeat_action_to_code(config, action_id, template_arg, args):
return var return var
def validate_wait_until(value): _validate_wait_until = cv.maybe_simple_value(
schema = cv.Schema( {
{ cv.Required(CONF_CONDITION): validate_potentially_and_condition,
cv.Required(CONF_CONDITION): validate_potentially_and_condition, cv.Optional(CONF_TIMEOUT): cv.templatable(cv.positive_time_period_milliseconds),
cv.Optional(CONF_TIMEOUT): cv.templatable( },
cv.positive_time_period_milliseconds key=CONF_CONDITION,
), )
}
)
if isinstance(value, dict) and CONF_CONDITION in value:
return schema(value)
return validate_wait_until({CONF_CONDITION: value})
@register_action("wait_until", WaitUntilAction, validate_wait_until) @register_action("wait_until", WaitUntilAction, _validate_wait_until)
async def wait_until_action_to_code(config, action_id, template_arg, args): async def wait_until_action_to_code(config, action_id, template_arg, args):
conditions = await build_condition(config[CONF_CONDITION], template_arg, args) conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, conditions) var = cg.new_Pvariable(action_id, template_arg, conditions)

View file

@ -195,6 +195,20 @@ class ProtoWriteBuffer {
this->write((value >> 16) & 0xFF); this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF); this->write((value >> 24) & 0xFF);
} }
void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 5);
this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF);
this->write((value >> 32) & 0xFF);
this->write((value >> 40) & 0xFF);
this->write((value >> 48) & 0xFF);
this->write((value >> 56) & 0xFF);
}
template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) { template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) {
this->encode_uint32(field_id, static_cast<uint32_t>(value), force); this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
} }
@ -229,6 +243,15 @@ class ProtoWriteBuffer {
} }
this->encode_uint32(field_id, uvalue, force); this->encode_uint32(field_id, uvalue, force);
} }
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
uint64_t uvalue;
if (value < 0) {
uvalue = ~(value << 1);
} else {
uvalue = value << 1;
}
this->encode_uint64(field_id, uvalue, force);
}
template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) { template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
this->encode_field_raw(field_id, 2); this->encode_field_raw(field_id, 2);
size_t begin = this->buffer_->size(); size_t begin = this->buffer_->size();

View file

@ -38,7 +38,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
const auto &data = service_data.data; const auto &data = service_data.data;
const uint8_t protocol_version = data[0] >> 4; const uint8_t protocol_version = data[0] >> 4;
if (protocol_version != 1) { if (protocol_version != 1 && protocol_version != 2) {
ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version); ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version);
return false; return false;
} }
@ -57,9 +57,15 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
uint16_t battery_millivolt = data[2] << 8 | data[3]; uint16_t battery_millivolt = data[2] << 8 | data[3];
float battery_voltage = battery_millivolt / 1000.0f; float battery_voltage = battery_millivolt / 1000.0f;
// Temperature in 1000 * Celsius. // Temperature in 1000 * Celsius (protocol v1) or 100 * Celsius (protocol v2).
uint16_t temp_millicelcius = data[4] << 8 | data[5]; float temp_celsius;
float temp_celcius = temp_millicelcius / 1000.0f; if (protocol_version == 1) {
uint16_t temp_millicelsius = data[4] << 8 | data[5];
temp_celsius = temp_millicelsius / 1000.0f;
} else {
int16_t temp_centicelsius = data[4] << 8 | data[5];
temp_celsius = temp_centicelsius / 100.0f;
}
// Relative air humidity in the range [0, 2^16). // Relative air humidity in the range [0, 2^16).
uint16_t humidity = data[6] << 8 | data[7]; uint16_t humidity = data[6] << 8 | data[7];
@ -76,7 +82,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
battery_voltage_->publish_state(battery_voltage); battery_voltage_->publish_state(battery_voltage);
} }
if (temperature_ != nullptr) { if (temperature_ != nullptr) {
temperature_->publish_state(temp_celcius); temperature_->publish_state(temp_celsius);
} }
if (humidity_ != nullptr) { if (humidity_ != nullptr) {
humidity_->publish_state(humidity_percent); humidity_->publish_state(humidity_percent);

View file

@ -118,16 +118,21 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
this->set_states_(espbt::ClientState::IDLE); this->set_states_(espbt::ClientState::IDLE);
break; break;
} }
this->conn_id = param->open.conn_id; break;
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id); }
case ESP_GATTC_CONNECT_EVT: {
ESP_LOGV(TAG, "[%s] ESP_GATTC_CONNECT_EVT", this->address_str().c_str());
this->conn_id = param->connect.conn_id;
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->connect.conn_id);
if (ret) { if (ret) {
ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%d", ret); ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret);
} }
break; break;
} }
case ESP_GATTC_CFG_MTU_EVT: { case ESP_GATTC_CFG_MTU_EVT: {
if (param->cfg_mtu.status != ESP_GATT_OK) { if (param->cfg_mtu.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "cfg_mtu to %s failed, status %d", this->address_str().c_str(), param->cfg_mtu.status); ESP_LOGW(TAG, "cfg_mtu to %s failed, mtu %d, status %d", this->address_str().c_str(), param->cfg_mtu.mtu,
param->cfg_mtu.status);
this->set_states_(espbt::ClientState::IDLE); this->set_states_(espbt::ClientState::IDLE);
break; break;
} }
@ -139,7 +144,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) { if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) {
return; return;
} }
ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str()); ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->address_str().c_str(), param->disconnect.reason);
for (auto &svc : this->services_) for (auto &svc : this->services_)
delete svc; // NOLINT(cppcoreguidelines-owning-memory) delete svc; // NOLINT(cppcoreguidelines-owning-memory)
this->services_.clear(); this->services_.clear();
@ -201,6 +206,32 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
} }
} }
void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
// This event is sent by the server when it requests security
case ESP_GAP_BLE_SEC_REQ_EVT:
ESP_LOGV(TAG, "ESP_GAP_BLE_SEC_REQ_EVT %x", event);
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break;
// This event is sent once authentication has completed
case ESP_GAP_BLE_AUTH_CMPL_EVT:
esp_bd_addr_t bd_addr;
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
ESP_LOGI(TAG, "auth complete. remote BD_ADDR: %s", format_hex(bd_addr, 6).c_str());
if (!param->ble_security.auth_cmpl.success) {
ESP_LOGE(TAG, "auth fail reason = 0x%x", param->ble_security.auth_cmpl.fail_reason);
} else {
ESP_LOGV(TAG, "auth success. address type = %d auth mode = %d", param->ble_security.auth_cmpl.addr_type,
param->ble_security.auth_cmpl.auth_mode);
}
break;
// There are other events we'll want to implement at some point to support things like pass key
// https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md
default:
break;
}
}
// Parse GATT values into a float for a sensor. // Parse GATT values into a float for a sensor.
// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ // Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/
float BLEClient::parse_char_value(uint8_t *value, uint16_t length) { float BLEClient::parse_char_value(uint8_t *value, uint16_t length) {

View file

@ -11,6 +11,7 @@
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gattc_api.h> #include <esp_gattc_api.h>
#include <esp_bt_defs.h> #include <esp_bt_defs.h>
#include <esp_gatt_common_api.h>
namespace esphome { namespace esphome {
namespace ble_client { namespace ble_client {
@ -86,6 +87,7 @@ class BLEClient : public espbt::ESPBTClient, public Component {
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override; esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
bool parse_device(const espbt::ESPBTDevice &device) override; bool parse_device(const espbt::ESPBTDevice &device) override;
void on_scan_end() override {} void on_scan_end() override {}
void connect() override; void connect() override;

View file

@ -10,6 +10,7 @@ IS_PLATFORM_COMPONENT = True
CONF_CAN_ID = "can_id" CONF_CAN_ID = "can_id"
CONF_CAN_ID_MASK = "can_id_mask" CONF_CAN_ID_MASK = "can_id_mask"
CONF_USE_EXTENDED_ID = "use_extended_id" CONF_USE_EXTENDED_ID = "use_extended_id"
CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request"
CONF_CANBUS_ID = "canbus_id" CONF_CANBUS_ID = "canbus_id"
CONF_BIT_RATE = "bit_rate" CONF_BIT_RATE = "bit_rate"
CONF_ON_FRAME = "on_frame" CONF_ON_FRAME = "on_frame"
@ -122,6 +123,7 @@ async def register_canbus(var, config):
cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent), cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent),
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean,
cv.Required(CONF_DATA): cv.templatable(validate_raw_data), cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
}, },
validate_id, validate_id,
@ -140,6 +142,11 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
) )
cg.add(var.set_use_extended_id(use_extended_id)) cg.add(var.set_use_extended_id(use_extended_id))
remote_transmission_request = await cg.templatable(
config[CONF_REMOTE_TRANSMISSION_REQUEST], args, bool
)
cg.add(var.set_remote_transmission_request(remote_transmission_request))
data = config[CONF_DATA] data = config[CONF_DATA]
if isinstance(data, bytes): if isinstance(data, bytes):
data = [int(x) for x in data] data = [int(x) for x in data]

View file

@ -22,20 +22,22 @@ void Canbus::dump_config() {
} }
} }
void Canbus::send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) { void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const std::vector<uint8_t> &data) {
struct CanFrame can_message; struct CanFrame can_message;
uint8_t size = static_cast<uint8_t>(data.size()); uint8_t size = static_cast<uint8_t>(data.size());
if (use_extended_id) { if (use_extended_id) {
ESP_LOGD(TAG, "send extended id=0x%08x size=%d", can_id, size); ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
} else { } else {
ESP_LOGD(TAG, "send extended id=0x%03x size=%d", can_id, size); ESP_LOGD(TAG, "send extended id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
} }
if (size > CAN_MAX_DATA_LENGTH) if (size > CAN_MAX_DATA_LENGTH)
size = CAN_MAX_DATA_LENGTH; size = CAN_MAX_DATA_LENGTH;
can_message.can_data_length_code = size; can_message.can_data_length_code = size;
can_message.can_id = can_id; can_message.can_id = can_id;
can_message.use_extended_id = use_extended_id; can_message.use_extended_id = use_extended_id;
can_message.remote_transmission_request = remote_transmission_request;
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
can_message.data[i] = data[i]; can_message.data[i] = data[i];

View file

@ -62,7 +62,12 @@ class Canbus : public Component {
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override; void loop() override;
void send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data); void send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const std::vector<uint8_t> &data);
void send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) {
// for backwards compatibility only
this->send_data(can_id, use_extended_id, false, data);
}
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; } void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
@ -96,21 +101,26 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
void set_remote_transmission_request(bool remote_transmission_request) {
this->remote_transmission_request_ = remote_transmission_request;
}
void play(Ts... x) override { void play(Ts... x) override {
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
auto use_extended_id = auto use_extended_id =
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
if (this->static_) { if (this->static_) {
this->parent_->send_data(can_id, use_extended_id, this->data_static_); this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_);
} else { } else {
auto val = this->data_func_(x...); auto val = this->data_func_(x...);
this->parent_->send_data(can_id, use_extended_id, val); this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val);
} }
} }
protected: protected:
optional<uint32_t> can_id_{}; optional<uint32_t> can_id_{};
optional<bool> use_extended_id_{}; optional<bool> use_extended_id_{};
bool remote_transmission_request_{false};
bool static_{false}; bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{}; std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{}; std::vector<uint8_t> data_static_{};

View file

@ -76,7 +76,7 @@ enum ClimateSwingMode : uint8_t {
CLIMATE_SWING_HORIZONTAL = 3, CLIMATE_SWING_HORIZONTAL = 3,
}; };
/// Enum for all modes a climate swing can be in /// Enum for all preset modes
enum ClimatePreset : uint8_t { enum ClimatePreset : uint8_t {
/// No preset is active /// No preset is active
CLIMATE_PRESET_NONE = 0, CLIMATE_PRESET_NONE = 0,
@ -108,7 +108,7 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode mode);
/// Convert the given ClimateSwingMode to a human-readable string. /// Convert the given ClimateSwingMode to a human-readable string.
const LogString *climate_swing_mode_to_string(ClimateSwingMode mode); const LogString *climate_swing_mode_to_string(ClimateSwingMode mode);
/// Convert the given ClimateSwingMode to a human-readable string. /// Convert the given PresetMode to a human-readable string.
const LogString *climate_preset_to_string(ClimatePreset preset); const LogString *climate_preset_to_string(ClimatePreset preset);
} // namespace climate } // namespace climate

View file

@ -98,6 +98,8 @@ CELL_VOLTAGE_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT, unit_of_measurement=UNIT_VOLT,
device_class=DEVICE_CLASS_VOLTAGE, device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_FLASH,
accuracy_decimals=3,
) )
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(

View file

@ -1,13 +1,18 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import time
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import pins, automation from esphome import pins, automation
from esphome.const import ( from esphome.const import (
CONF_HOUR,
CONF_ID, CONF_ID,
CONF_MINUTE,
CONF_MODE, CONF_MODE,
CONF_NUMBER, CONF_NUMBER,
CONF_PINS, CONF_PINS,
CONF_RUN_DURATION, CONF_RUN_DURATION,
CONF_SECOND,
CONF_SLEEP_DURATION, CONF_SLEEP_DURATION,
CONF_TIME_ID,
CONF_WAKEUP_PIN, CONF_WAKEUP_PIN,
) )
@ -15,6 +20,7 @@ from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32S2,
) )
WAKEUP_PINS = { WAKEUP_PINS = {
@ -39,6 +45,30 @@ WAKEUP_PINS = {
39, 39,
], ],
VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5], VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5],
VARIANT_ESP32S2: [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
],
} }
@ -87,6 +117,7 @@ CONF_TOUCH_WAKEUP = "touch_wakeup"
CONF_DEFAULT = "default" CONF_DEFAULT = "default"
CONF_GPIO_WAKEUP_REASON = "gpio_wakeup_reason" CONF_GPIO_WAKEUP_REASON = "gpio_wakeup_reason"
CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason" CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason"
CONF_UNTIL = "until"
WAKEUP_CAUSES_SCHEMA = cv.Schema( WAKEUP_CAUSES_SCHEMA = cv.Schema(
{ {
@ -177,13 +208,19 @@ async def to_code(config):
cg.add_define("USE_DEEP_SLEEP") cg.add_define("USE_DEEP_SLEEP")
DEEP_SLEEP_ENTER_SCHEMA = automation.maybe_simple_id( DEEP_SLEEP_ENTER_SCHEMA = cv.All(
{ automation.maybe_simple_id(
cv.GenerateID(): cv.use_id(DeepSleepComponent), {
cv.Optional(CONF_SLEEP_DURATION): cv.templatable( cv.GenerateID(): cv.use_id(DeepSleepComponent),
cv.positive_time_period_milliseconds cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
), cv.positive_time_period_milliseconds
} ),
# Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
cv.Exclusive(CONF_UNTIL, "time"): cv.All(cv.only_on_esp32, cv.time_of_day),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
),
cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
) )
@ -203,6 +240,14 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
if CONF_SLEEP_DURATION in config: if CONF_SLEEP_DURATION in config:
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32) template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32)
cg.add(var.set_sleep_duration(template_)) cg.add(var.set_sleep_duration(template_))
if CONF_UNTIL in config:
until = config[CONF_UNTIL]
cg.add(var.set_until(until[CONF_HOUR], until[CONF_MINUTE], until[CONF_SECOND]))
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time(time_))
return var return var

View file

@ -1,6 +1,7 @@
#include "deep_sleep_component.h" #include "deep_sleep_component.h"
#include "esphome/core/log.h" #include <cinttypes>
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h"
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#include <Esp.h> #include <Esp.h>
@ -101,6 +102,8 @@ void DeepSleepComponent::begin_sleep(bool manual) {
#endif #endif
ESP_LOGI(TAG, "Beginning Deep Sleep"); ESP_LOGI(TAG, "Beginning Deep Sleep");
if (this->sleep_duration_.has_value())
ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_);
App.run_safe_shutdown_hooks(); App.run_safe_shutdown_hooks();

View file

@ -9,6 +9,10 @@
#include <esp_sleep.h> #include <esp_sleep.h>
#endif #endif
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
namespace esphome { namespace esphome {
namespace deep_sleep { namespace deep_sleep {
@ -116,15 +120,71 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {}
TEMPLATABLE_VALUE(uint32_t, sleep_duration); TEMPLATABLE_VALUE(uint32_t, sleep_duration);
#ifdef USE_TIME
void set_until(uint8_t hour, uint8_t minute, uint8_t second) {
this->hour_ = hour;
this->minute_ = minute;
this->second_ = second;
}
void set_time(time::RealTimeClock *time) { this->time_ = time; }
#endif
void play(Ts... x) override { void play(Ts... x) override {
if (this->sleep_duration_.has_value()) { if (this->sleep_duration_.has_value()) {
this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...));
} }
#ifdef USE_TIME
if (this->hour_.has_value()) {
auto time = this->time_->now();
const uint32_t timestamp_now = time.timestamp;
bool after_time = false;
if (time.hour > this->hour_) {
after_time = true;
} else {
if (time.hour == this->hour_) {
if (time.minute > this->minute_) {
after_time = true;
} else {
if (time.minute == this->minute_) {
if (time.second > this->second_) {
after_time = true;
}
}
}
}
}
time.hour = *this->hour_;
time.minute = *this->minute_;
time.second = *this->second_;
time.recalc_timestamp_utc();
time_t timestamp = time.timestamp; // timestamp in local time zone
if (after_time)
timestamp += 60 * 60 * 24;
int32_t offset = time::ESPTime::timezone_offset();
timestamp -= offset; // Change timestamp to utc
const uint32_t ms_left = (timestamp - timestamp_now) * 1000;
this->deep_sleep_->set_sleep_duration(ms_left);
}
#endif
this->deep_sleep_->begin_sleep(true); this->deep_sleep_->begin_sleep(true);
} }
protected: protected:
DeepSleepComponent *deep_sleep_; DeepSleepComponent *deep_sleep_;
#ifdef USE_TIME
optional<uint8_t> hour_;
optional<uint8_t> minute_;
optional<uint8_t> second_;
time::RealTimeClock *time_;
#endif
}; };
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...> { template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...> {

View file

@ -143,37 +143,37 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional("power_delivered_l1"): sensor.sensor_schema( cv.Optional("power_delivered_l1"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT, unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3, accuracy_decimals=3,
device_class=DEVICE_CLASS_CURRENT, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional("power_delivered_l2"): sensor.sensor_schema( cv.Optional("power_delivered_l2"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT, unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3, accuracy_decimals=3,
device_class=DEVICE_CLASS_CURRENT, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional("power_delivered_l3"): sensor.sensor_schema( cv.Optional("power_delivered_l3"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT, unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3, accuracy_decimals=3,
device_class=DEVICE_CLASS_CURRENT, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional("power_returned_l1"): sensor.sensor_schema( cv.Optional("power_returned_l1"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT, unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3, accuracy_decimals=3,
device_class=DEVICE_CLASS_CURRENT, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional("power_returned_l2"): sensor.sensor_schema( cv.Optional("power_returned_l2"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT, unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3, accuracy_decimals=3,
device_class=DEVICE_CLASS_CURRENT, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional("power_returned_l3"): sensor.sensor_schema( cv.Optional("power_returned_l3"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT, unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3, accuracy_decimals=3,
device_class=DEVICE_CLASS_CURRENT, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema(

View file

@ -12,6 +12,7 @@ using namespace esphome::cover;
CoverTraits EndstopCover::get_traits() { CoverTraits EndstopCover::get_traits() {
auto traits = CoverTraits(); auto traits = CoverTraits();
traits.set_supports_position(true); traits.set_supports_position(true);
traits.set_supports_toggle(true);
traits.set_is_assumed_state(false); traits.set_is_assumed_state(false);
return traits; return traits;
} }
@ -20,6 +21,20 @@ void EndstopCover::control(const CoverCall &call) {
this->start_direction_(COVER_OPERATION_IDLE); this->start_direction_(COVER_OPERATION_IDLE);
this->publish_state(); this->publish_state();
} }
if (call.get_toggle().has_value()) {
if (this->current_operation != COVER_OPERATION_IDLE) {
this->start_direction_(COVER_OPERATION_IDLE);
this->publish_state();
} else {
if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
this->target_position_ = COVER_OPEN;
this->start_direction_(COVER_OPERATION_OPENING);
} else {
this->target_position_ = COVER_CLOSED;
this->start_direction_(COVER_OPERATION_CLOSING);
}
}
}
if (call.get_position().has_value()) { if (call.get_position().has_value()) {
auto pos = *call.get_position(); auto pos = *call.get_position();
if (pos == this->position) { if (pos == this->position) {
@ -125,9 +140,11 @@ void EndstopCover::start_direction_(CoverOperation dir) {
trig = this->stop_trigger_; trig = this->stop_trigger_;
break; break;
case COVER_OPERATION_OPENING: case COVER_OPERATION_OPENING:
this->last_operation_ = dir;
trig = this->open_trigger_; trig = this->open_trigger_;
break; break;
case COVER_OPERATION_CLOSING: case COVER_OPERATION_CLOSING:
this->last_operation_ = dir;
trig = this->close_trigger_; trig = this->close_trigger_;
break; break;
default: default:

View file

@ -51,6 +51,7 @@ class EndstopCover : public cover::Cover, public Component {
uint32_t start_dir_time_{0}; uint32_t start_dir_time_{0};
uint32_t last_publish_time_{0}; uint32_t last_publish_time_{0};
float target_position_{0}; float target_position_{0};
cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
}; };
} // namespace endstop } // namespace endstop

View file

@ -262,6 +262,9 @@ void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_
default: default:
break; break;
} }
for (auto *client : global_esp32_ble_tracker->clients_) {
client->gap_event_handler(event, param);
}
} }
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) { void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) {

View file

@ -155,6 +155,7 @@ class ESPBTClient : public ESPBTDeviceListener {
public: public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) = 0; esp_ble_gattc_cb_param_t *param) = 0;
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
virtual void connect() = 0; virtual void connect() = 0;
void set_state(ClientState st) { this->state_ = st; } void set_state(ClientState st) { this->state_ = st; }
ClientState state() const { return state_; } ClientState state() const { return state_; }

View file

@ -1,12 +1,29 @@
import functools import functools
from pathlib import Path
import hashlib
import re
import requests
from esphome import core from esphome import core
from esphome.components import display from esphome.components import display
import esphome.config_validation as cv import esphome.config_validation as cv
import esphome.codegen as cg import esphome.codegen as cg
from esphome.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_RAW_DATA_ID, CONF_SIZE from esphome.const import (
CONF_FAMILY,
CONF_FILE,
CONF_GLYPHS,
CONF_ID,
CONF_RAW_DATA_ID,
CONF_TYPE,
CONF_SIZE,
CONF_PATH,
CONF_WEIGHT,
)
from esphome.core import CORE, HexInt from esphome.core import CORE, HexInt
DOMAIN = "font"
DEPENDENCIES = ["display"] DEPENDENCIES = ["display"]
MULTI_CONF = True MULTI_CONF = True
@ -71,6 +88,128 @@ def validate_truetype_file(value):
return cv.file_(value) return cv.file_(value)
def _compute_gfonts_local_path(value) -> Path:
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
h = hashlib.new("sha256")
h.update(name.encode())
return base_dir / h.hexdigest()[:8] / "font.ttf"
TYPE_LOCAL = "local"
TYPE_GFONTS = "gfonts"
LOCAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_PATH): validate_truetype_file,
}
)
CONF_ITALIC = "italic"
FONT_WEIGHTS = {
"thin": 100,
"extra-light": 200,
"light": 300,
"regular": 400,
"medium": 500,
"semi-bold": 600,
"bold": 700,
"extra-bold": 800,
"black": 900,
}
def validate_weight_name(value):
return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)]
def download_gfonts(value):
wght = value[CONF_WEIGHT]
if value[CONF_ITALIC]:
wght = f"1,{wght}"
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}"
url = f"https://fonts.googleapis.com/css2?family={value[CONF_FAMILY]}:wght@{wght}"
path = _compute_gfonts_local_path(value)
if path.is_file():
return value
try:
req = requests.get(url)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(
f"Could not download font for {name}, please check the fonts exists "
f"at google fonts ({e})"
)
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
if match is None:
raise cv.Invalid(
f"Could not extract ttf file from gfonts response for {name}, "
f"please report this."
)
ttf_url = match.group(1)
try:
req = requests.get(ttf_url)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}")
path.parent.mkdir(exist_ok=True, parents=True)
path.write_bytes(req.content)
return value
GFONTS_SCHEMA = cv.All(
{
cv.Required(CONF_FAMILY): cv.string_strict,
cv.Optional(CONF_WEIGHT, default="regular"): cv.Any(
cv.int_, validate_weight_name
),
cv.Optional(CONF_ITALIC, default=False): cv.boolean,
},
download_gfonts,
)
def validate_file_shorthand(value):
value = cv.string_strict(value)
if value.startswith("gfonts://"):
match = re.match(r"^gfonts://([^@]+)(@.+)?$", value)
if match is None:
raise cv.Invalid("Could not parse gfonts shorthand syntax, please check it")
family = match.group(1)
weight = match.group(2)
data = {
CONF_TYPE: TYPE_GFONTS,
CONF_FAMILY: family,
}
if weight is not None:
data[CONF_WEIGHT] = weight[1:]
return FILE_SCHEMA(data)
return FILE_SCHEMA(
{
CONF_TYPE: TYPE_LOCAL,
CONF_PATH: value,
}
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
TYPE_LOCAL: LOCAL_SCHEMA,
TYPE_GFONTS: GFONTS_SCHEMA,
}
)
def _file_schema(value):
if isinstance(value, str):
return validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
FILE_SCHEMA = cv.Schema(_file_schema)
DEFAULT_GLYPHS = ( DEFAULT_GLYPHS = (
' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
) )
@ -79,7 +218,7 @@ CONF_RAW_GLYPH_ID = "raw_glyph_id"
FONT_SCHEMA = cv.Schema( FONT_SCHEMA = cv.Schema(
{ {
cv.Required(CONF_ID): cv.declare_id(Font), cv.Required(CONF_ID): cv.declare_id(Font),
cv.Required(CONF_FILE): validate_truetype_file, cv.Required(CONF_FILE): FILE_SCHEMA,
cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
@ -93,9 +232,13 @@ CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA)
async def to_code(config): async def to_code(config):
from PIL import ImageFont from PIL import ImageFont
path = CORE.relative_config_path(config[CONF_FILE]) conf = config[CONF_FILE]
if conf[CONF_TYPE] == TYPE_LOCAL:
path = CORE.relative_config_path(conf[CONF_PATH])
elif conf[CONF_TYPE] == TYPE_GFONTS:
path = _compute_gfonts_local_path(conf)
try: try:
font = ImageFont.truetype(path, config[CONF_SIZE]) font = ImageFont.truetype(str(path), config[CONF_SIZE])
except Exception as e: except Exception as e:
raise core.EsphomeError(f"Could not load truetype file {path}: {e}") raise core.EsphomeError(f"Could not load truetype file {path}: {e}")

View file

@ -7,9 +7,11 @@ namespace growatt_solar {
static const char *const TAG = "growatt_solar"; static const char *const TAG = "growatt_solar";
static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04;
static const uint8_t MODBUS_REGISTER_COUNT = 33; static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion
void GrowattSolar::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } void GrowattSolar::update() {
this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]);
}
void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) { void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) {
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void {
@ -27,37 +29,76 @@ void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) {
sensor->publish_state(value); sensor->publish_state(value);
}; };
publish_1_reg_sensor_state(this->inverter_status_, 0, 1); switch (this->protocol_version_) {
case RTU: {
publish_1_reg_sensor_state(this->inverter_status_, 0, 1);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT); publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT); publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT); publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT); publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT); publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT); publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT); publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT); publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT); publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT); publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT);
break;
}
case RTU2: {
publish_1_reg_sensor_state(this->inverter_status_, 0, 1);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, 35, 36, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->grid_frequency_sensor_, 37, TWO_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 38, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 39, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 40, 41, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 42, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 43, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 44, 45, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 46, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 47, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 48, 49, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, 53, 54, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, 55, 56, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->inverter_module_temp_, 93, ONE_DEC_UNIT);
break;
}
}
} }
void GrowattSolar::dump_config() { void GrowattSolar::dump_config() {

View file

@ -10,12 +10,19 @@ namespace growatt_solar {
static const float TWO_DEC_UNIT = 0.01; static const float TWO_DEC_UNIT = 0.01;
static const float ONE_DEC_UNIT = 0.1; static const float ONE_DEC_UNIT = 0.1;
enum GrowattProtocolVersion {
RTU = 0,
RTU2,
};
class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
public: public:
void update() override; void update() override;
void on_modbus_data(const std::vector<uint8_t> &data) override; void on_modbus_data(const std::vector<uint8_t> &data) override;
void dump_config() override; void dump_config() override;
void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; }
void set_inverter_status_sensor(sensor::Sensor *sensor) { this->inverter_status_ = sensor; } void set_inverter_status_sensor(sensor::Sensor *sensor) { this->inverter_status_ = sensor; }
void set_grid_frequency_sensor(sensor::Sensor *sensor) { this->grid_frequency_sensor_ = sensor; } void set_grid_frequency_sensor(sensor::Sensor *sensor) { this->grid_frequency_sensor_ = sensor; }
@ -67,6 +74,7 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
sensor::Sensor *today_production_{nullptr}; sensor::Sensor *today_production_{nullptr};
sensor::Sensor *total_energy_production_{nullptr}; sensor::Sensor *total_energy_production_{nullptr};
sensor::Sensor *inverter_module_temp_{nullptr}; sensor::Sensor *inverter_module_temp_{nullptr};
GrowattProtocolVersion protocol_version_;
}; };
} // namespace growatt_solar } // namespace growatt_solar

View file

@ -39,7 +39,7 @@ UNIT_MILLIAMPERE = "mA"
CONF_INVERTER_STATUS = "inverter_status" CONF_INVERTER_STATUS = "inverter_status"
CONF_PV_ACTIVE_POWER = "pv_active_power" CONF_PV_ACTIVE_POWER = "pv_active_power"
CONF_INVERTER_MODULE_TEMP = "inverter_module_temp" CONF_INVERTER_MODULE_TEMP = "inverter_module_temp"
CONF_PROTOCOL_VERSION = "protocol_version"
AUTO_LOAD = ["modbus"] AUTO_LOAD = ["modbus"]
CODEOWNERS = ["@leeuwte"] CODEOWNERS = ["@leeuwte"]
@ -95,10 +95,20 @@ PV_SCHEMA = cv.Schema(
{cv.Optional(sensor): schema for sensor, schema in PV_SENSORS.items()} {cv.Optional(sensor): schema for sensor, schema in PV_SENSORS.items()}
) )
GrowattProtocolVersion = growatt_solar_ns.enum("GrowattProtocolVersion")
PROTOCOL_VERSIONS = {
"RTU": GrowattProtocolVersion.RTU,
"RTU2": GrowattProtocolVersion.RTU2,
}
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(GrowattSolar), cv.GenerateID(): cv.declare_id(GrowattSolar),
cv.Optional(CONF_PROTOCOL_VERSION, default="RTU"): cv.enum(
PROTOCOL_VERSIONS, upper=True
),
cv.Optional(CONF_PHASE_A): PHASE_SCHEMA, cv.Optional(CONF_PHASE_A): PHASE_SCHEMA,
cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, cv.Optional(CONF_PHASE_B): PHASE_SCHEMA,
cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): PHASE_SCHEMA,
@ -152,6 +162,8 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await modbus.register_modbus_device(var, config) await modbus.register_modbus_device(var, config)
cg.add(var.set_protocol_version(config[CONF_PROTOCOL_VERSION]))
if CONF_INVERTER_STATUS in config: if CONF_INVERTER_STATUS in config:
sens = await sensor.new_sensor(config[CONF_INVERTER_STATUS]) sens = await sensor.new_sensor(config[CONF_INVERTER_STATUS])
cg.add(var.set_inverter_status_sensor(sens)) cg.add(var.set_inverter_status_sensor(sens))

View file

@ -25,6 +25,7 @@ PROTOCOLS = {
"daikin_arc417": Protocol.PROTOCOL_DAIKIN_ARC417, "daikin_arc417": Protocol.PROTOCOL_DAIKIN_ARC417,
"daikin_arc480": Protocol.PROTOCOL_DAIKIN_ARC480, "daikin_arc480": Protocol.PROTOCOL_DAIKIN_ARC480,
"daikin": Protocol.PROTOCOL_DAIKIN, "daikin": Protocol.PROTOCOL_DAIKIN,
"electroluxyal": Protocol.PROTOCOL_ELECTROLUXYAL,
"fuego": Protocol.PROTOCOL_FUEGO, "fuego": Protocol.PROTOCOL_FUEGO,
"fujitsu_awyz": Protocol.PROTOCOL_FUJITSU_AWYZ, "fujitsu_awyz": Protocol.PROTOCOL_FUJITSU_AWYZ,
"gree": Protocol.PROTOCOL_GREE, "gree": Protocol.PROTOCOL_GREE,
@ -112,6 +113,4 @@ def to_code(config):
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
# PIO isn't updating releases, so referencing the release tag directly. See: cg.add_library("tonia/HeatpumpIR", "1.0.20")
# https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd
cg.add_library("", "", "https://github.com/ToniA/arduino-heatpumpir.git#1.0.18")

View file

@ -20,6 +20,7 @@ const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT {PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT {PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT {PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT {PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT {PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT {PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT

View file

@ -20,6 +20,7 @@ enum Protocol {
PROTOCOL_DAIKIN_ARC417, PROTOCOL_DAIKIN_ARC417,
PROTOCOL_DAIKIN_ARC480, PROTOCOL_DAIKIN_ARC480,
PROTOCOL_DAIKIN, PROTOCOL_DAIKIN,
PROTOCOL_ELECTROLUXYAL,
PROTOCOL_FUEGO, PROTOCOL_FUEGO,
PROTOCOL_FUJITSU_AWYZ, PROTOCOL_FUJITSU_AWYZ,
PROTOCOL_GREE, PROTOCOL_GREE,

View file

@ -7,7 +7,7 @@ namespace hm3301 {
class AbstractAQICalculator { class AbstractAQICalculator {
public: public:
virtual uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
}; };
} // namespace hm3301 } // namespace hm3301

View file

@ -7,7 +7,7 @@ namespace hm3301 {
class AQICalculator : public AbstractAQICalculator { class AQICalculator : public AbstractAQICalculator {
public: public:
uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_);
int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_);

View file

@ -8,7 +8,7 @@ namespace hm3301 {
class CAQICalculator : public AbstractAQICalculator { class CAQICalculator : public AbstractAQICalculator {
public: public:
uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_);
int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_);

View file

@ -62,7 +62,7 @@ void HM3301Component::update() {
pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX);
} }
int8_t aqi_value = -1; int16_t aqi_value = -1;
if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) {
AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value);

View file

@ -1,4 +1,20 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@OttoWinter"]
homeassistant_ns = cg.esphome_ns.namespace("homeassistant") homeassistant_ns = cg.esphome_ns.namespace("homeassistant")
HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema(
{
cv.Required(CONF_ENTITY_ID): cv.entity_id,
cv.Optional(CONF_ATTRIBUTE): cv.string,
cv.Optional(CONF_INTERNAL, default=True): cv.boolean,
}
)
def setup_home_assistant_entity(var, config):
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
if CONF_ATTRIBUTE in config:
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))

View file

@ -1,30 +1,24 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor from esphome.components import binary_sensor
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID
from .. import homeassistant_ns from .. import (
HOME_ASSISTANT_IMPORT_SCHEMA,
homeassistant_ns,
setup_home_assistant_entity,
)
DEPENDENCIES = ["api"] DEPENDENCIES = ["api"]
HomeassistantBinarySensor = homeassistant_ns.class_( HomeassistantBinarySensor = homeassistant_ns.class_(
"HomeassistantBinarySensor", binary_sensor.BinarySensor, cg.Component "HomeassistantBinarySensor", binary_sensor.BinarySensor, cg.Component
) )
CONFIG_SCHEMA = ( CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).extend(
binary_sensor.binary_sensor_schema(HomeassistantBinarySensor) HOME_ASSISTANT_IMPORT_SCHEMA
.extend(
{
cv.Required(CONF_ENTITY_ID): cv.entity_id,
cv.Optional(CONF_ATTRIBUTE): cv.string,
}
)
.extend(cv.COMPONENT_SCHEMA)
) )
async def to_code(config): async def to_code(config):
var = await binary_sensor.new_binary_sensor(config) var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
setup_home_assistant_entity(var, config)
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
if CONF_ATTRIBUTE in config:
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))

View file

@ -1,12 +1,11 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor from esphome.components import sensor
from esphome.const import (
CONF_ATTRIBUTE, from .. import (
CONF_ENTITY_ID, HOME_ASSISTANT_IMPORT_SCHEMA,
CONF_ID, homeassistant_ns,
setup_home_assistant_entity,
) )
from .. import homeassistant_ns
DEPENDENCIES = ["api"] DEPENDENCIES = ["api"]
@ -14,19 +13,12 @@ HomeassistantSensor = homeassistant_ns.class_(
"HomeassistantSensor", sensor.Sensor, cg.Component "HomeassistantSensor", sensor.Sensor, cg.Component
) )
CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1,).extend( CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).extend(
{ HOME_ASSISTANT_IMPORT_SCHEMA
cv.Required(CONF_ENTITY_ID): cv.entity_id,
cv.Optional(CONF_ATTRIBUTE): cv.string,
}
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await sensor.new_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await sensor.register_sensor(var, config) setup_home_assistant_entity(var, config)
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
if CONF_ATTRIBUTE in config:
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))

View file

@ -1,9 +1,11 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor from esphome.components import text_sensor
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID
from .. import homeassistant_ns from .. import (
HOME_ASSISTANT_IMPORT_SCHEMA,
homeassistant_ns,
setup_home_assistant_entity,
)
DEPENDENCIES = ["api"] DEPENDENCIES = ["api"]
@ -11,19 +13,12 @@ HomeassistantTextSensor = homeassistant_ns.class_(
"HomeassistantTextSensor", text_sensor.TextSensor, cg.Component "HomeassistantTextSensor", text_sensor.TextSensor, cg.Component
) )
CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend( CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend(
{ HOME_ASSISTANT_IMPORT_SCHEMA
cv.GenerateID(): cv.declare_id(HomeassistantTextSensor),
cv.Required(CONF_ENTITY_ID): cv.entity_id,
cv.Optional(CONF_ATTRIBUTE): cv.string,
}
) )
async def to_code(config): async def to_code(config):
var = await text_sensor.new_text_sensor(config) var = await text_sensor.new_text_sensor(config)
await cg.register_component(var, config) await cg.register_component(var, config)
setup_home_assistant_entity(var, config)
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
if CONF_ATTRIBUTE in config:
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))

View file

@ -0,0 +1,11 @@
import esphome.codegen as cg
from esphome.components import uart
CODEOWNERS = ["@functionpointer"]
DEPENDENCIES = ["uart"]
hydreon_rgxx_ns = cg.esphome_ns.namespace("hydreon_rgxx")
RGModel = hydreon_rgxx_ns.enum("RGModel")
HydreonRGxxComponent = hydreon_rgxx_ns.class_(
"HydreonRGxxComponent", cg.PollingComponent, uart.UARTDevice
)

View file

@ -0,0 +1,36 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import (
CONF_ID,
DEVICE_CLASS_COLD,
)
from . import hydreon_rgxx_ns, HydreonRGxxComponent
CONF_HYDREON_RGXX_ID = "hydreon_rgxx_id"
CONF_TOO_COLD = "too_cold"
HydreonRGxxBinarySensor = hydreon_rgxx_ns.class_(
"HydreonRGxxBinaryComponent", cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(HydreonRGxxBinarySensor),
cv.GenerateID(CONF_HYDREON_RGXX_ID): cv.use_id(HydreonRGxxComponent),
cv.Optional(CONF_TOO_COLD): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_COLD
),
}
)
async def to_code(config):
main_sensor = await cg.get_variable(config[CONF_HYDREON_RGXX_ID])
bin_component = cg.new_Pvariable(config[CONF_ID], main_sensor)
await cg.register_component(bin_component, config)
if CONF_TOO_COLD in config:
tc = await binary_sensor.new_binary_sensor(config[CONF_TOO_COLD])
cg.add(main_sensor.set_too_cold_sensor(tc))

View file

@ -0,0 +1,211 @@
#include "hydreon_rgxx.h"
#include "esphome/core/log.h"
namespace esphome {
namespace hydreon_rgxx {
static const char *const TAG = "hydreon_rgxx.sensor";
static const int MAX_DATA_LENGTH_BYTES = 80;
static const uint8_t ASCII_LF = 0x0A;
#define HYDREON_RGXX_COMMA ,
static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREON_RGXX_COMMA)};
void HydreonRGxxComponent::dump_config() {
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
if (this->is_failed()) {
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
}
LOG_UPDATE_INTERVAL(this);
int i = 0;
#define HYDREON_RGXX_LOG_SENSOR(s) \
if (this->sensors_[i++] != nullptr) { \
LOG_SENSOR(" ", #s, this->sensors_[i - 1]); \
}
HYDREON_RGXX_PROTOCOL_LIST(HYDREON_RGXX_LOG_SENSOR, );
}
void HydreonRGxxComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up hydreon_rgxx...");
while (this->available() != 0) {
this->read();
}
this->schedule_reboot_();
}
bool HydreonRGxxComponent::sensor_missing_() {
if (this->sensors_received_ == -1) {
// no request sent yet, don't check
return false;
} else {
if (this->sensors_received_ == 0) {
ESP_LOGW(TAG, "No data at all");
return true;
}
for (int i = 0; i < NUM_SENSORS; i++) {
if (this->sensors_[i] == nullptr) {
continue;
}
if ((this->sensors_received_ >> i & 1) == 0) {
ESP_LOGW(TAG, "Missing %s", PROTOCOL_NAMES[i]);
return true;
}
}
return false;
}
}
void HydreonRGxxComponent::update() {
if (this->boot_count_ > 0) {
if (this->sensor_missing_()) {
this->no_response_count_++;
ESP_LOGE(TAG, "data missing %d times", this->no_response_count_);
if (this->no_response_count_ > 15) {
ESP_LOGE(TAG, "asking sensor to reboot");
for (auto &sensor : this->sensors_) {
if (sensor != nullptr) {
sensor->publish_state(NAN);
}
}
this->schedule_reboot_();
return;
}
} else {
this->no_response_count_ = 0;
}
this->write_str("R\n");
#ifdef USE_BINARY_SENSOR
if (this->too_cold_sensor_ != nullptr) {
this->too_cold_sensor_->publish_state(this->too_cold_);
}
#endif
this->too_cold_ = false;
this->sensors_received_ = 0;
}
}
void HydreonRGxxComponent::loop() {
uint8_t data;
while (this->available() > 0) {
if (this->read_byte(&data)) {
buffer_ += (char) data;
if (this->buffer_.back() == static_cast<char>(ASCII_LF) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) {
// complete line received
this->process_line_();
this->buffer_.clear();
}
}
}
}
/**
* Communication with the sensor is asynchronous.
* We send requests and let esphome continue doing its thing.
* Once we have received a complete line, we process it.
*
* Catching communication failures is done in two layers:
*
* 1. We check if all requested data has been received
* before we send out the next request. If data keeps
* missing, we escalate.
* 2. Request the sensor to reboot. We retry based on
* a timeout. If the sensor does not respond after
* several boot attempts, we give up.
*/
void HydreonRGxxComponent::schedule_reboot_() {
this->boot_count_ = 0;
this->set_interval("reboot", 5000, [this]() {
if (this->boot_count_ < 0) {
ESP_LOGW(TAG, "hydreon_rgxx failed to boot %d times", -this->boot_count_);
}
this->boot_count_--;
this->write_str("K\n");
if (this->boot_count_ < -5) {
ESP_LOGE(TAG, "hydreon_rgxx can't boot, giving up");
for (auto &sensor : this->sensors_) {
if (sensor != nullptr) {
sensor->publish_state(NAN);
}
}
this->mark_failed();
}
});
}
bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) {
return this->buffer_starts_with_(prefix.c_str());
}
bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; }
void HydreonRGxxComponent::process_line_() {
ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
if (buffer_[0] == ';') {
ESP_LOGI(TAG, "Comment: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
return;
}
if (this->buffer_starts_with_("PwrDays")) {
if (this->boot_count_ <= 0) {
this->boot_count_ = 1;
} else {
this->boot_count_++;
}
this->cancel_interval("reboot");
this->no_response_count_ = 0;
ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
this->write_str("P\nH\nM\n"); // set sensor to polling mode, high res mode, metric mode
return;
}
if (this->buffer_starts_with_("SW")) {
std::string::size_type majend = this->buffer_.find('.');
std::string::size_type endversion = this->buffer_.find(' ', 3);
if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) {
ESP_LOGW(TAG, "invalid version string: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
}
int major = strtol(this->buffer_.substr(3, majend - 3).c_str(), nullptr, 10);
int minor = strtol(this->buffer_.substr(majend + 1, endversion - (majend + 1)).c_str(), nullptr, 10);
if (major > 10 || minor >= 1000 || minor < 0 || major < 0) {
ESP_LOGW(TAG, "invalid version: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
}
this->sw_version_ = major * 1000 + minor;
ESP_LOGI(TAG, "detected sw version %i", this->sw_version_);
return;
}
bool is_data_line = false;
for (int i = 0; i < NUM_SENSORS; i++) {
if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) {
is_data_line = true;
break;
}
}
if (is_data_line) {
std::string::size_type tc = this->buffer_.find("TooCold");
this->too_cold_ |= tc != std::string::npos;
if (this->too_cold_) {
ESP_LOGD(TAG, "Received TooCold");
}
for (int i = 0; i < NUM_SENSORS; i++) {
if (this->sensors_[i] == nullptr) {
continue;
}
std::string::size_type n = this->buffer_.find(PROTOCOL_NAMES[i]);
if (n == std::string::npos) {
continue;
}
int data = strtol(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr, 10);
this->sensors_[i]->publish_state(data);
ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state());
this->sensors_received_ |= (1 << i);
}
} else {
ESP_LOGI(TAG, "Got unknown line: %s", this->buffer_.c_str());
}
}
float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; }
} // namespace hydreon_rgxx
} // namespace esphome

View file

@ -0,0 +1,76 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/components/sensor/sensor.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace hydreon_rgxx {
enum RGModel {
RG9 = 1,
RG15 = 2,
};
#ifdef HYDREON_RGXX_NUM_SENSORS
static const uint8_t NUM_SENSORS = HYDREON_RGXX_NUM_SENSORS;
#else
static const uint8_t NUM_SENSORS = 1;
#endif
#ifndef HYDREON_RGXX_PROTOCOL_LIST
#define HYDREON_RGXX_PROTOCOL_LIST(F, SEP) F("")
#endif
class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice {
public:
void set_sensor(sensor::Sensor *sensor, int index) { this->sensors_[index] = sensor; }
#ifdef USE_BINARY_SENSOR
void set_too_cold_sensor(binary_sensor::BinarySensor *sensor) { this->too_cold_sensor_ = sensor; }
#endif
void set_model(RGModel model) { model_ = model; }
/// Schedule data readings.
void update() override;
/// Read data once available
void loop() override;
/// Setup the sensor and test for a connection.
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
void process_line_();
void schedule_reboot_();
bool buffer_starts_with_(const std::string &prefix);
bool buffer_starts_with_(const char *prefix);
bool sensor_missing_();
sensor::Sensor *sensors_[NUM_SENSORS] = {nullptr};
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *too_cold_sensor_ = nullptr;
#endif
int16_t boot_count_ = 0;
int16_t no_response_count_ = 0;
std::string buffer_;
RGModel model_ = RG9;
int sw_version_ = 0;
bool too_cold_ = false;
// bit field showing which sensors we have received data for
int sensors_received_ = -1;
};
class HydreonRGxxBinaryComponent : public Component {
public:
HydreonRGxxBinaryComponent(HydreonRGxxComponent *parent) {}
};
} // namespace hydreon_rgxx
} // namespace esphome

View file

@ -0,0 +1,119 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart, sensor
from esphome.const import (
CONF_ID,
CONF_MODEL,
CONF_MOISTURE,
DEVICE_CLASS_HUMIDITY,
STATE_CLASS_MEASUREMENT,
)
from . import RGModel, HydreonRGxxComponent
UNIT_INTENSITY = "intensity"
UNIT_MILLIMETERS = "mm"
UNIT_MILLIMETERS_PER_HOUR = "mm/h"
CONF_ACC = "acc"
CONF_EVENT_ACC = "event_acc"
CONF_TOTAL_ACC = "total_acc"
CONF_R_INT = "r_int"
RG_MODELS = {
"RG_9": RGModel.RG9,
"RG_15": RGModel.RG15,
# https://rainsensors.com/wp-content/uploads/sites/3/2020/07/rg-15_instructions_sw_1.000.pdf
# https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2020.08.25-rg-9_instructions.pdf
# https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf
}
SUPPORTED_SENSORS = {
CONF_ACC: ["RG_15"],
CONF_EVENT_ACC: ["RG_15"],
CONF_TOTAL_ACC: ["RG_15"],
CONF_R_INT: ["RG_15"],
CONF_MOISTURE: ["RG_9"],
}
PROTOCOL_NAMES = {
CONF_MOISTURE: "R",
CONF_ACC: "Acc",
CONF_R_INT: "Rint",
CONF_EVENT_ACC: "EventAcc",
CONF_TOTAL_ACC: "TotalAcc",
}
def _validate(config):
for conf, models in SUPPORTED_SENSORS.items():
if conf in config:
if config[CONF_MODEL] not in models:
raise cv.Invalid(
f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}"
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HydreonRGxxComponent),
cv.Required(CONF_MODEL): cv.enum(
RG_MODELS,
upper=True,
space="_",
),
cv.Optional(CONF_ACC): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETERS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_EVENT_ACC): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETERS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TOTAL_ACC): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETERS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_R_INT): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETERS_PER_HOUR,
accuracy_decimals=2,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
unit_of_measurement=UNIT_INTENSITY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA),
_validate,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add_define(
"HYDREON_RGXX_PROTOCOL_LIST(F, sep)",
cg.RawExpression(
" sep ".join([f'F("{name}")' for name in PROTOCOL_NAMES.values()])
),
)
cg.add_define("HYDREON_RGXX_NUM_SENSORS", len(PROTOCOL_NAMES))
for i, conf in enumerate(PROTOCOL_NAMES):
if conf in config:
sens = await sensor.new_sensor(config[conf])
cg.add(var.set_sensor(sens, i))

View file

@ -46,21 +46,21 @@ class I2CDevice {
I2CRegister reg(uint8_t a_register) { return {this, a_register}; } I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); }
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len) { ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true) {
ErrorCode err = this->write(&a_register, 1); ErrorCode err = this->write(&a_register, 1, stop);
if (err != ERROR_OK) if (err != ERROR_OK)
return err; return err;
return this->read(data, len); return this->read(data, len);
} }
ErrorCode write(const uint8_t *data, uint8_t len) { return bus_->write(address_, data, len); } ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); }
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) { ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true) {
WriteBuffer buffers[2]; WriteBuffer buffers[2];
buffers[0].data = &a_register; buffers[0].data = &a_register;
buffers[0].len = 1; buffers[0].len = 1;
buffers[1].data = data; buffers[1].data = data;
buffers[1].len = len; buffers[1].len = len;
return bus_->writev(address_, buffers, 2); return bus_->writev(address_, buffers, 2, stop);
} }
// Compat APIs // Compat APIs
@ -93,7 +93,9 @@ class I2CDevice {
return true; return true;
} }
bool read_byte(uint8_t a_register, uint8_t *data) { return read_register(a_register, data, 1) == ERROR_OK; } bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) {
return read_register(a_register, data, 1, stop) == ERROR_OK;
}
optional<uint8_t> read_byte(uint8_t a_register) { optional<uint8_t> read_byte(uint8_t a_register) {
uint8_t data; uint8_t data;
@ -104,8 +106,8 @@ class I2CDevice {
bool read_byte_16(uint8_t a_register, uint16_t *data) { return read_bytes_16(a_register, data, 1); } bool read_byte_16(uint8_t a_register, uint16_t *data) { return read_bytes_16(a_register, data, 1); }
bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop = true) {
return write_register(a_register, data, len) == ERROR_OK; return write_register(a_register, data, len, stop) == ERROR_OK;
} }
bool write_bytes(uint8_t a_register, const std::vector<uint8_t> &data) { bool write_bytes(uint8_t a_register, const std::vector<uint8_t> &data) {
@ -118,7 +120,9 @@ class I2CDevice {
bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len);
bool write_byte(uint8_t a_register, uint8_t data) { return write_bytes(a_register, &data, 1); } bool write_byte(uint8_t a_register, uint8_t data, bool stop = true) {
return write_bytes(a_register, &data, 1, stop);
}
bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); }

View file

@ -15,6 +15,7 @@ enum ErrorCode {
ERROR_NOT_INITIALIZED = 4, ERROR_NOT_INITIALIZED = 4,
ERROR_TOO_LARGE = 5, ERROR_TOO_LARGE = 5,
ERROR_UNKNOWN = 6, ERROR_UNKNOWN = 6,
ERROR_CRC = 7,
}; };
struct ReadBuffer { struct ReadBuffer {
@ -36,12 +37,18 @@ class I2CBus {
} }
virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) = 0; virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) = 0;
virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) {
return write(address, buffer, len, true);
}
virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) {
WriteBuffer buf; WriteBuffer buf;
buf.data = buffer; buf.data = buffer;
buf.len = len; buf.len = len;
return writev(address, &buf, 1); return writev(address, &buf, 1, stop);
} }
virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) = 0; virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) {
return writev(address, buffers, cnt, true);
}
virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) = 0;
protected: protected:
void i2c_scan_() { void i2c_scan_() {

View file

@ -104,7 +104,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt)
return ERROR_OK; return ERROR_OK;
} }
ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) {
// logging is only enabled with vv level, if warnings are shown the caller // logging is only enabled with vv level, if warnings are shown the caller
// should log them // should log them
if (!initialized_) { if (!initialized_) {
@ -139,7 +139,7 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn
return ERROR_UNKNOWN; return ERROR_UNKNOWN;
} }
} }
uint8_t status = wire_->endTransmission(true); uint8_t status = wire_->endTransmission(stop);
if (status == 0) { if (status == 0) {
return ERROR_OK; return ERROR_OK;
} else if (status == 1) { } else if (status == 1) {

View file

@ -20,7 +20,7 @@ class ArduinoI2CBus : public I2CBus, public Component {
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override;
ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override; ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override;
float get_setup_priority() const override { return setup_priority::BUS; } float get_setup_priority() const override { return setup_priority::BUS; }
void set_scan(bool scan) { scan_ = scan; } void set_scan(bool scan) { scan_ = scan; }

View file

@ -142,7 +142,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
return ERROR_OK; return ERROR_OK;
} }
ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) {
// logging is only enabled with vv level, if warnings are shown the caller // logging is only enabled with vv level, if warnings are shown the caller
// should log them // should log them
if (!initialized_) { if (!initialized_) {

View file

@ -20,7 +20,7 @@ class IDFI2CBus : public I2CBus, public Component {
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override;
ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override; ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override;
float get_setup_priority() const override { return setup_priority::BUS; } float get_setup_priority() const override { return setup_priority::BUS; }
void set_scan(bool scan) { scan_ = scan; } void set_scan(bool scan) { scan_ = scan; }

View file

@ -23,13 +23,13 @@ std::string build_json(const json_build_t &f) {
#ifdef USE_ESP8266 #ifdef USE_ESP8266
const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance)
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
#endif #endif
const size_t request_size = std::min(free_heap - 2048, (size_t) 5120); const size_t request_size = std::min(free_heap, (size_t) 512);
DynamicJsonDocument json_document(request_size); DynamicJsonDocument json_document(request_size);
if (json_document.memoryPool().buffer() == nullptr) { if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
request_size, free_heap); request_size, free_heap);
return "{}"; return "{}";
@ -37,7 +37,7 @@ std::string build_json(const json_build_t &f) {
JsonObject root = json_document.to<JsonObject>(); JsonObject root = json_document.to<JsonObject>();
f(root); f(root);
json_document.shrinkToFit(); json_document.shrinkToFit();
ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
std::string output; std::string output;
serializeJson(json_document, output); serializeJson(json_document, output);
return output; return output;
@ -51,13 +51,13 @@ void parse_json(const std::string &data, const json_parse_t &f) {
#ifdef USE_ESP8266 #ifdef USE_ESP8266
const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance)
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
#endif #endif
bool pass = false; bool pass = false;
size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); size_t request_size = std::min(free_heap, (size_t)(data.size() * 1.5));
do { do {
DynamicJsonDocument json_document(request_size); DynamicJsonDocument json_document(request_size);
if (json_document.memoryPool().buffer() == nullptr) { if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size,
free_heap); free_heap);
return; return;

View file

@ -1,7 +1,9 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import display from esphome.components import display
from esphome.const import CONF_DIMENSIONS from esphome.const import CONF_DIMENSIONS, CONF_POSITION, CONF_DATA
CONF_USER_CHARACTERS = "user_characters"
lcd_base_ns = cg.esphome_ns.namespace("lcd_base") lcd_base_ns = cg.esphome_ns.namespace("lcd_base")
LCDDisplay = lcd_base_ns.class_("LCDDisplay", cg.PollingComponent) LCDDisplay = lcd_base_ns.class_("LCDDisplay", cg.PollingComponent)
@ -16,9 +18,35 @@ def validate_lcd_dimensions(value):
return value return value
def validate_user_characters(value):
positions = set()
for conf in value:
if conf[CONF_POSITION] in positions:
raise cv.Invalid(
f"Duplicate user defined character at position {conf[CONF_POSITION]}"
)
positions.add(conf[CONF_POSITION])
return value
LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend(
{ {
cv.Required(CONF_DIMENSIONS): validate_lcd_dimensions, cv.Required(CONF_DIMENSIONS): validate_lcd_dimensions,
cv.Optional(CONF_USER_CHARACTERS): cv.All(
cv.ensure_list(
cv.Schema(
{
cv.Required(CONF_POSITION): cv.int_range(min=0, max=7),
cv.Required(CONF_DATA): cv.All(
cv.ensure_list(cv.int_range(min=0, max=31)),
cv.Length(min=8, max=8),
),
}
),
),
cv.Length(max=8),
validate_user_characters,
),
} }
).extend(cv.polling_component_schema("1s")) ).extend(cv.polling_component_schema("1s"))
@ -27,3 +55,6 @@ async def setup_lcd_display(var, config):
await cg.register_component(var, config) await cg.register_component(var, config)
await display.register_display(var, config) await display.register_display(var, config)
cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])) cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]))
if CONF_USER_CHARACTERS in config:
for usr in config[CONF_USER_CHARACTERS]:
cg.add(var.set_user_defined_char(usr[CONF_POSITION], usr[CONF_DATA]))

View file

@ -65,6 +65,13 @@ void LCDDisplay::setup() {
this->command_(LCD_DISPLAY_COMMAND_FUNCTION_SET | display_function); this->command_(LCD_DISPLAY_COMMAND_FUNCTION_SET | display_function);
} }
// store user defined characters
for (auto &user_defined_char : this->user_defined_chars_) {
this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (user_defined_char.first << 3));
for (auto data : user_defined_char.second)
this->send(data, true);
}
this->command_(LCD_DISPLAY_COMMAND_FUNCTION_SET | display_function); this->command_(LCD_DISPLAY_COMMAND_FUNCTION_SET | display_function);
uint8_t display_control = LCD_DISPLAY_DISPLAY_ON; uint8_t display_control = LCD_DISPLAY_DISPLAY_ON;
this->command_(LCD_DISPLAY_COMMAND_DISPLAY_CONTROL | display_control); this->command_(LCD_DISPLAY_COMMAND_DISPLAY_CONTROL | display_control);
@ -160,6 +167,13 @@ void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time:
} }
void LCDDisplay::strftime(const char *format, time::ESPTime time) { this->strftime(0, 0, format, time); } void LCDDisplay::strftime(const char *format, time::ESPTime time) { this->strftime(0, 0, format, time); }
#endif #endif
void LCDDisplay::loadchar(uint8_t location, uint8_t charmap[]) {
location &= 0x7; // we only have 8 locations 0-7
this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (location << 3));
for (int i = 0; i < 8; i++) {
this->send(charmap[i], true);
}
}
} // namespace lcd_base } // namespace lcd_base
} // namespace esphome } // namespace esphome

View file

@ -7,6 +7,8 @@
#include "esphome/components/time/real_time_clock.h" #include "esphome/components/time/real_time_clock.h"
#endif #endif
#include <map>
namespace esphome { namespace esphome {
namespace lcd_base { namespace lcd_base {
@ -19,6 +21,8 @@ class LCDDisplay : public PollingComponent {
this->rows_ = rows; this->rows_ = rows;
} }
void set_user_defined_char(uint8_t pos, const std::vector<uint8_t> &data) { this->user_defined_chars_[pos] = data; }
void setup() override; void setup() override;
float get_setup_priority() const override; float get_setup_priority() const override;
void update() override; void update() override;
@ -47,6 +51,9 @@ class LCDDisplay : public PollingComponent {
void strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); void strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0)));
#endif #endif
/// Load custom char to given location
void loadchar(uint8_t location, uint8_t charmap[]);
protected: protected:
virtual bool is_four_bit_mode() = 0; virtual bool is_four_bit_mode() = 0;
virtual void write_n_bits(uint8_t value, uint8_t n) = 0; virtual void write_n_bits(uint8_t value, uint8_t n) = 0;
@ -58,6 +65,7 @@ class LCDDisplay : public PollingComponent {
uint8_t columns_; uint8_t columns_;
uint8_t rows_; uint8_t rows_;
uint8_t *buffer_{nullptr}; uint8_t *buffer_{nullptr};
std::map<uint8_t, std::vector<uint8_t> > user_defined_chars_;
}; };
} // namespace lcd_base } // namespace lcd_base

View file

@ -203,15 +203,6 @@ async def to_code(config):
) )
def maybe_simple_message(schema):
def validator(value):
if isinstance(value, dict):
return cv.Schema(schema)(value)
return cv.Schema(schema)({CONF_FORMAT: value})
return validator
def validate_printf(value): def validate_printf(value):
# https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python
cfmt = r""" cfmt = r"""
@ -234,7 +225,7 @@ def validate_printf(value):
CONF_LOGGER_LOG = "logger.log" CONF_LOGGER_LOG = "logger.log"
LOGGER_LOG_ACTION_SCHEMA = cv.All( LOGGER_LOG_ACTION_SCHEMA = cv.All(
maybe_simple_message( cv.maybe_simple_value(
{ {
cv.Required(CONF_FORMAT): cv.string, cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_), cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
@ -242,9 +233,10 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All(
*LOG_LEVEL_TO_ESP_LOG, upper=True *LOG_LEVEL_TO_ESP_LOG, upper=True
), ),
cv.Optional(CONF_TAG, default="main"): cv.string, cv.Optional(CONF_TAG, default="main"): cv.string,
} },
), validate_printf,
validate_printf, key=CONF_FORMAT,
)
) )

View file

@ -20,7 +20,7 @@ void MCP3204::dump_config() {
} }
float MCP3204::read_data(uint8_t pin) { float MCP3204::read_data(uint8_t pin) {
uint8_t adc_primary_config = 0b00000110 & 0b00000111; uint8_t adc_primary_config = 0b00000110 | (pin >> 2);
uint8_t adc_secondary_config = pin << 6; uint8_t adc_secondary_config = pin << 6;
this->enable(); this->enable();
this->transfer_byte(adc_primary_config); this->transfer_byte(adc_primary_config);

View file

@ -17,7 +17,7 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(
{ {
cv.GenerateID(): cv.declare_id(MCP3204Sensor), cv.GenerateID(): cv.declare_id(MCP3204Sensor),
cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204),
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=3), cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7),
} }
).extend(cv.polling_component_schema("60s")) ).extend(cv.polling_component_schema("60s"))

View file

@ -71,9 +71,9 @@ SENSOR_VALUE_TYPE = {
"S_DWORD": SensorValueType.S_DWORD, "S_DWORD": SensorValueType.S_DWORD,
"S_DWORD_R": SensorValueType.S_DWORD_R, "S_DWORD_R": SensorValueType.S_DWORD_R,
"U_QWORD": SensorValueType.U_QWORD, "U_QWORD": SensorValueType.U_QWORD,
"U_QWORDU_R": SensorValueType.U_QWORD_R, "U_QWORD_R": SensorValueType.U_QWORD_R,
"S_QWORD": SensorValueType.S_QWORD, "S_QWORD": SensorValueType.S_QWORD,
"U_QWORD_R": SensorValueType.S_QWORD_R, "S_QWORD_R": SensorValueType.S_QWORD_R,
"FP32": SensorValueType.FP32, "FP32": SensorValueType.FP32,
"FP32_R": SensorValueType.FP32_R, "FP32_R": SensorValueType.FP32_R,
} }
@ -87,9 +87,9 @@ TYPE_REGISTER_MAP = {
"S_DWORD": 2, "S_DWORD": 2,
"S_DWORD_R": 2, "S_DWORD_R": 2,
"U_QWORD": 4, "U_QWORD": 4,
"U_QWORDU_R": 4,
"S_QWORD": 4,
"U_QWORD_R": 4, "U_QWORD_R": 4,
"S_QWORD": 4,
"S_QWORD_R": 4,
"FP32": 2, "FP32": 2,
"FP32_R": 2, "FP32_R": 2,
} }

View file

@ -455,6 +455,28 @@ ModbusCommandItem ModbusCommandItem::create_custom_command(
return cmd; return cmd;
} }
ModbusCommandItem ModbusCommandItem::create_custom_command(
ModbusController *modbusdevice, const std::vector<uint16_t> &values,
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
&&handler) {
ModbusCommandItem cmd = {};
cmd.modbusdevice = modbusdevice;
cmd.function_code = ModbusFunctionCode::CUSTOM;
if (handler == nullptr) {
cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
ESP_LOGI(TAG, "Custom Command sent");
};
} else {
cmd.on_data_func = handler;
}
for (auto v : values) {
cmd.payload.push_back((v >> 8) & 0xFF);
cmd.payload.push_back(v & 0xFF);
}
return cmd;
}
bool ModbusCommandItem::send() { bool ModbusCommandItem::send() {
if (this->function_code != ModbusFunctionCode::CUSTOM) { if (this->function_code != ModbusFunctionCode::CUSTOM) {
modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(), modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(),

View file

@ -2,12 +2,12 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/modbus/modbus.h" #include "esphome/components/modbus/modbus.h"
#include "esphome/core/automation.h"
#include <list> #include <list>
#include <set>
#include <queue> #include <queue>
#include <set>
#include <vector> #include <vector>
namespace esphome { namespace esphome {
@ -374,8 +374,8 @@ class ModbusCommandItem {
const std::vector<bool> &values); const std::vector<bool> &values);
/** Create custom modbus command /** Create custom modbus command
* @param modbusdevice pointer to the device to execute the command * @param modbusdevice pointer to the device to execute the command
* @param values byte vector of data to be sent to the device. The compplete payload must be provided with the * @param values byte vector of data to be sent to the device. The complete payload must be provided with the
* exception of the crc codess * exception of the crc codes
* @param handler function called when the response is received. Default is just logging a response * @param handler function called when the response is received. Default is just logging a response
* @return ModbusCommandItem with the prepared command * @return ModbusCommandItem with the prepared command
*/ */
@ -383,6 +383,18 @@ class ModbusCommandItem {
ModbusController *modbusdevice, const std::vector<uint8_t> &values, ModbusController *modbusdevice, const std::vector<uint8_t> &values,
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)> std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
&&handler = nullptr); &&handler = nullptr);
/** Create custom modbus command
* @param modbusdevice pointer to the device to execute the command
* @param values word vector of data to be sent to the device. The complete payload must be provided with the
* exception of the crc codes
* @param handler function called when the response is received. Default is just logging a response
* @return ModbusCommandItem with the prepared command
*/
static ModbusCommandItem create_custom_command(
ModbusController *modbusdevice, const std::vector<uint16_t> &values,
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
&&handler = nullptr);
}; };
/** Modbus controller class. /** Modbus controller class.

View file

@ -26,6 +26,7 @@ void ModbusNumber::parse_and_publish(const std::vector<uint8_t> &data) {
} }
void ModbusNumber::control(float value) { void ModbusNumber::control(float value) {
ModbusCommandItem write_cmd;
std::vector<uint16_t> data; std::vector<uint16_t> data;
float write_value = value; float write_value = value;
// Is there are lambda configured? // Is there are lambda configured?
@ -45,33 +46,39 @@ void ModbusNumber::control(float value) {
write_value = multiply_by_ * write_value; write_value = multiply_by_ * write_value;
} }
// lambda didn't set payload if (!data.empty()) {
if (data.empty()) { ESP_LOGV(TAG, "Modbus Number write raw: %s", format_hex_pretty(data).c_str());
data = float_to_payload(write_value, this->sensor_value_type); write_cmd = ModbusCommandItem::create_custom_command(
} this->parent_, data,
[this, write_cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
ESP_LOGD(TAG, this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data);
"Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", });
this->get_name().c_str(), this->start_address, this->register_count, value, write_value);
// Create and send the write command
ModbusCommandItem write_cmd;
if (this->register_count == 1 && !this->use_write_multiple_) {
// since offset is in bytes and a register is 16 bits we get the start by adding offset/2
write_cmd =
ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, data[0]);
} else { } else {
write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2, data = float_to_payload(write_value, this->sensor_value_type);
this->register_count, data);
ESP_LOGD(TAG,
"Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)",
this->get_name().c_str(), this->start_address, this->register_count, value, write_value);
// Create and send the write command
if (this->register_count == 1 && !this->use_write_multiple_) {
// since offset is in bytes and a register is 16 bits we get the start by adding offset/2
write_cmd =
ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, data[0]);
} else {
write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2,
this->register_count, data);
}
// publish new value
write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address,
const std::vector<uint8_t> &data) {
// gets called when the write command is ack'd from the device
parent_->on_write_register_response(write_cmd.register_type, start_address, data);
this->publish_state(value);
};
} }
// publish new value
write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address,
const std::vector<uint8_t> &data) {
// gets called when the write command is ack'd from the device
parent_->on_write_register_response(write_cmd.register_type, start_address, data);
this->publish_state(value);
};
parent_->queue_command(write_cmd); parent_->queue_command(write_cmd);
this->publish_state(value);
} }
void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); } void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); }

View file

@ -2,7 +2,6 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import select from esphome.components import select
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
from esphome.jsonschema import jschema_composite
from .. import ( from .. import (
SENSOR_VALUE_TYPE, SENSOR_VALUE_TYPE,
@ -30,7 +29,6 @@ ModbusSelect = modbus_controller_ns.class_(
) )
@jschema_composite
def ensure_option_map(): def ensure_option_map():
def validator(value): def validator(value):
cv.check_not_templatable(value) cv.check_not_templatable(value)

View file

@ -9,6 +9,7 @@ from esphome.const import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_BIRTH_MESSAGE, CONF_BIRTH_MESSAGE,
CONF_BROKER, CONF_BROKER,
CONF_CERTIFICATE_AUTHORITY,
CONF_CLIENT_ID, CONF_CLIENT_ID,
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_COMMAND_RETAIN, CONF_COMMAND_RETAIN,
@ -42,9 +43,14 @@ from esphome.const import (
CONF_WILL_MESSAGE, CONF_WILL_MESSAGE,
) )
from esphome.core import coroutine_with_priority, CORE from esphome.core import coroutine_with_priority, CORE
from esphome.components.esp32 import add_idf_sdkconfig_option
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["json", "async_tcp"]
AUTO_LOAD = ["json"]
CONF_IDF_SEND_ASYNC = "idf_send_async"
CONF_SKIP_CERT_CN_CHECK = "skip_cert_cn_check"
def validate_message_just_topic(value): def validate_message_just_topic(value):
@ -163,6 +169,15 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_USERNAME, default=""): cv.string, cv.Optional(CONF_USERNAME, default=""): cv.string,
cv.Optional(CONF_PASSWORD, default=""): cv.string, cv.Optional(CONF_PASSWORD, default=""): cv.string,
cv.Optional(CONF_CLIENT_ID): cv.string, cv.Optional(CONF_CLIENT_ID): cv.string,
cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32_idf=False): cv.All(
cv.boolean, cv.only_with_esp_idf
),
cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.All(
cv.string, cv.only_with_esp_idf
),
cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32_idf=False): cv.All(
cv.boolean, cv.only_with_esp_idf
),
cv.Optional(CONF_DISCOVERY, default=True): cv.Any( cv.Optional(CONF_DISCOVERY, default=True): cv.Any(
cv.boolean, cv.one_of("CLEAN", upper=True) cv.boolean, cv.one_of("CLEAN", upper=True)
), ),
@ -217,7 +232,6 @@ CONFIG_SCHEMA = cv.All(
} }
), ),
validate_config, validate_config,
cv.only_with_arduino,
) )
@ -238,9 +252,11 @@ def exp_mqtt_message(config):
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
# Add required libraries for arduino
if CORE.using_arduino:
# https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json
cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6")
# https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json
cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6")
cg.add_define("USE_MQTT") cg.add_define("USE_MQTT")
cg.add_global(mqtt_ns.using) cg.add_global(mqtt_ns.using)
@ -321,6 +337,19 @@ async def to_code(config):
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
# esp-idf only
if CONF_CERTIFICATE_AUTHORITY in config:
cg.add(var.set_ca_certificate(config[CONF_CERTIFICATE_AUTHORITY]))
cg.add(var.set_skip_cert_cn_check(config[CONF_SKIP_CERT_CN_CHECK]))
# prevent error -0x428e
# See https://github.com/espressif/esp-idf/issues/139
add_idf_sdkconfig_option("CONFIG_MBEDTLS_HARDWARE_MPI", False)
if CONF_IDF_SEND_ASYNC in config and config[CONF_IDF_SEND_ASYNC]:
cg.add_define("USE_MQTT_IDF_ENQUEUE")
# end esp-idf
for conf in config.get(CONF_ON_MESSAGE, []): for conf in config.get(CONF_ON_MESSAGE, []):
trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC]) trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC])
cg.add(trig.set_qos(conf[CONF_QOS])) cg.add(trig.set_qos(conf[CONF_QOS]))

View file

@ -0,0 +1,69 @@
#pragma once
#include <string>
#include <map>
#include "esphome/components/network/ip_address.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace mqtt {
enum class MQTTClientDisconnectReason : int8_t {
TCP_DISCONNECTED = 0,
MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1,
MQTT_IDENTIFIER_REJECTED = 2,
MQTT_SERVER_UNAVAILABLE = 3,
MQTT_MALFORMED_CREDENTIALS = 4,
MQTT_NOT_AUTHORIZED = 5,
ESP8266_NOT_ENOUGH_SPACE = 6,
TLS_BAD_FINGERPRINT = 7
};
/// internal struct for MQTT messages.
struct MQTTMessage {
std::string topic;
std::string payload;
uint8_t qos; ///< QoS. Only for last will testaments.
bool retain;
};
class MQTTBackend {
public:
using on_connect_callback_t = void(bool session_present);
using on_disconnect_callback_t = void(MQTTClientDisconnectReason reason);
using on_subscribe_callback_t = void(uint16_t packet_id, uint8_t qos);
using on_unsubscribe_callback_t = void(uint16_t packet_id);
using on_message_callback_t = void(const char *topic, const char *payload, size_t len, size_t index, size_t total);
using on_publish_user_callback_t = void(uint16_t packet_id);
virtual void set_keep_alive(uint16_t keep_alive) = 0;
virtual void set_client_id(const char *client_id) = 0;
virtual void set_clean_session(bool clean_session) = 0;
virtual void set_credentials(const char *username, const char *password) = 0;
virtual void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) = 0;
virtual void set_server(network::IPAddress ip, uint16_t port) = 0;
virtual void set_server(const char *host, uint16_t port) = 0;
virtual void set_on_connect(std::function<on_connect_callback_t> &&callback) = 0;
virtual void set_on_disconnect(std::function<on_disconnect_callback_t> &&callback) = 0;
virtual void set_on_subscribe(std::function<on_subscribe_callback_t> &&callback) = 0;
virtual void set_on_unsubscribe(std::function<on_unsubscribe_callback_t> &&callback) = 0;
virtual void set_on_message(std::function<on_message_callback_t> &&callback) = 0;
virtual void set_on_publish(std::function<on_publish_user_callback_t> &&callback) = 0;
virtual bool connected() const = 0;
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual bool subscribe(const char *topic, uint8_t qos) = 0;
virtual bool unsubscribe(const char *topic) = 0;
virtual bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) = 0;
virtual bool publish(const MQTTMessage &message) {
return publish(message.topic.c_str(), message.payload.c_str(), message.payload.length(), message.qos,
message.retain);
}
// called from MQTTClient::loop()
virtual void loop() {}
};
} // namespace mqtt
} // namespace esphome

View file

@ -0,0 +1,74 @@
#pragma once
#ifdef USE_ARDUINO
#include "mqtt_backend.h"
#include <AsyncMqttClient.h>
namespace esphome {
namespace mqtt {
class MQTTBackendArduino final : public MQTTBackend {
public:
void set_keep_alive(uint16_t keep_alive) final { mqtt_client_.setKeepAlive(keep_alive); }
void set_client_id(const char *client_id) final { mqtt_client_.setClientId(client_id); }
void set_clean_session(bool clean_session) final { mqtt_client_.setCleanSession(clean_session); }
void set_credentials(const char *username, const char *password) final {
mqtt_client_.setCredentials(username, password);
}
void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final {
mqtt_client_.setWill(topic, qos, retain, payload);
}
void set_server(network::IPAddress ip, uint16_t port) final {
mqtt_client_.setServer(IPAddress(static_cast<uint32_t>(ip)), port);
}
void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); }
#if ASYNC_TCP_SSL_ENABLED
void set_secure(bool secure) { mqtt_client.setSecure(secure); }
void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); }
#endif
void set_on_connect(std::function<on_connect_callback_t> &&callback) final {
this->mqtt_client_.onConnect(std::move(callback));
}
void set_on_disconnect(std::function<on_disconnect_callback_t> &&callback) final {
auto async_callback = [callback](AsyncMqttClientDisconnectReason reason) {
// int based enum so casting isn't a problem
callback(static_cast<MQTTClientDisconnectReason>(reason));
};
this->mqtt_client_.onDisconnect(std::move(async_callback));
}
void set_on_subscribe(std::function<on_subscribe_callback_t> &&callback) final {
this->mqtt_client_.onSubscribe(std::move(callback));
}
void set_on_unsubscribe(std::function<on_unsubscribe_callback_t> &&callback) final {
this->mqtt_client_.onUnsubscribe(std::move(callback));
}
void set_on_message(std::function<on_message_callback_t> &&callback) final {
auto async_callback = [callback](const char *topic, const char *payload,
AsyncMqttClientMessageProperties async_properties, size_t len, size_t index,
size_t total) { callback(topic, payload, len, index, total); };
mqtt_client_.onMessage(std::move(async_callback));
}
void set_on_publish(std::function<on_publish_user_callback_t> &&callback) final {
this->mqtt_client_.onPublish(std::move(callback));
}
bool connected() const final { return mqtt_client_.connected(); }
void connect() final { mqtt_client_.connect(); }
void disconnect() final { mqtt_client_.disconnect(true); }
bool subscribe(const char *topic, uint8_t qos) final { return mqtt_client_.subscribe(topic, qos) != 0; }
bool unsubscribe(const char *topic) final { return mqtt_client_.unsubscribe(topic) != 0; }
bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final {
return mqtt_client_.publish(topic, qos, retain, payload, length, false, 0) != 0;
}
using MQTTBackend::publish;
protected:
AsyncMqttClient mqtt_client_;
};
} // namespace mqtt
} // namespace esphome
#endif // defined(USE_ARDUINO)

View file

@ -0,0 +1,149 @@
#ifdef USE_ESP_IDF
#include <string>
#include "mqtt_backend_idf.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace mqtt {
static const char *const TAG = "mqtt.idf";
bool MQTTBackendIDF::initialize_() {
mqtt_cfg_.user_context = (void *) this;
mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE;
mqtt_cfg_.host = this->host_.c_str();
mqtt_cfg_.port = this->port_;
mqtt_cfg_.keepalive = this->keep_alive_;
mqtt_cfg_.disable_clean_session = !this->clean_session_;
if (!this->username_.empty()) {
mqtt_cfg_.username = this->username_.c_str();
if (!this->password_.empty()) {
mqtt_cfg_.password = this->password_.c_str();
}
}
if (!this->lwt_topic_.empty()) {
mqtt_cfg_.lwt_topic = this->lwt_topic_.c_str();
this->mqtt_cfg_.lwt_qos = this->lwt_qos_;
this->mqtt_cfg_.lwt_retain = this->lwt_retain_;
if (!this->lwt_message_.empty()) {
mqtt_cfg_.lwt_msg = this->lwt_message_.c_str();
mqtt_cfg_.lwt_msg_len = this->lwt_message_.size();
}
}
if (!this->client_id_.empty()) {
mqtt_cfg_.client_id = this->client_id_.c_str();
}
if (ca_certificate_.has_value()) {
mqtt_cfg_.cert_pem = ca_certificate_.value().c_str();
mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_;
mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL;
} else {
mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP;
}
auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_);
if (mqtt_client) {
handler_.reset(mqtt_client);
is_initalized_ = true;
esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, this);
return true;
} else {
ESP_LOGE(TAG, "Failed to initialize IDF-MQTT");
return false;
}
}
void MQTTBackendIDF::loop() {
// process new events
// handle only 1 message per loop iteration
if (!mqtt_events_.empty()) {
auto &event = mqtt_events_.front();
mqtt_event_handler_(event);
mqtt_events_.pop();
}
}
void MQTTBackendIDF::mqtt_event_handler_(const esp_mqtt_event_t &event) {
ESP_LOGV(TAG, "Event dispatched from event loop event_id=%d", event.event_id);
switch (event.event_id) {
case MQTT_EVENT_BEFORE_CONNECT:
ESP_LOGV(TAG, "MQTT_EVENT_BEFORE_CONNECT");
break;
case MQTT_EVENT_CONNECTED:
ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED");
// TODO session present check
this->is_connected_ = true;
this->on_connect_.call(!mqtt_cfg_.disable_clean_session);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED");
// TODO is there a way to get the disconnect reason?
this->is_connected_ = false;
this->on_disconnect_.call(MQTTClientDisconnectReason::TCP_DISCONNECTED);
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGV(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event.msg_id);
// hardcode QoS to 0. QoS is not used in this context but required to mirror the AsyncMqtt interface
this->on_subscribe_.call((int) event.msg_id, 0);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGV(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event.msg_id);
this->on_unsubscribe_.call((int) event.msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGV(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event.msg_id);
this->on_publish_.call((int) event.msg_id);
break;
case MQTT_EVENT_DATA: {
static std::string topic;
if (event.topic) {
// not 0 terminated - create a string from it
topic = std::string(event.topic, event.topic_len);
}
ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str());
auto data_len = event.data_len;
if (data_len == 0)
data_len = strlen(event.data);
this->on_message_.call(event.topic ? const_cast<char *>(topic.c_str()) : nullptr, event.data, data_len,
event.current_data_offset, event.total_data_len);
} break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
if (event.error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
ESP_LOGE(TAG, "Last error code reported from esp-tls: 0x%x", event.error_handle->esp_tls_last_esp_err);
ESP_LOGE(TAG, "Last tls stack error number: 0x%x", event.error_handle->esp_tls_stack_err);
ESP_LOGE(TAG, "Last captured errno : %d (%s)", event.error_handle->esp_transport_sock_errno,
strerror(event.error_handle->esp_transport_sock_errno));
} else if (event.error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
ESP_LOGE(TAG, "Connection refused error: 0x%x", event.error_handle->connect_return_code);
} else {
ESP_LOGE(TAG, "Unknown error type: 0x%x", event.error_handle->error_type);
}
break;
default:
ESP_LOGV(TAG, "Other event id:%d", event.event_id);
break;
}
}
/// static - Dispatch event to instance method
void MQTTBackendIDF::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
MQTTBackendIDF *instance = static_cast<MQTTBackendIDF *>(handler_args);
// queue event to decouple processing
if (instance) {
auto event = *static_cast<esp_mqtt_event_t *>(event_data);
instance->mqtt_events_.push(event);
}
}
} // namespace mqtt
} // namespace esphome
#endif // USE_ESP_IDF

View file

@ -0,0 +1,143 @@
#pragma once
#ifdef USE_ESP_IDF
#include <string>
#include <queue>
#include <mqtt_client.h>
#include "esphome/components/network/ip_address.h"
#include "esphome/core/helpers.h"
#include "mqtt_backend.h"
namespace esphome {
namespace mqtt {
class MQTTBackendIDF final : public MQTTBackend {
public:
static const size_t MQTT_BUFFER_SIZE = 4096;
void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; }
void set_client_id(const char *client_id) final { this->client_id_ = client_id; }
void set_clean_session(bool clean_session) final { this->clean_session_ = clean_session; }
void set_credentials(const char *username, const char *password) final {
if (username)
this->username_ = username;
if (password)
this->password_ = password;
}
void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final {
if (topic)
this->lwt_topic_ = topic;
this->lwt_qos_ = qos;
if (payload)
this->lwt_message_ = payload;
this->lwt_retain_ = retain;
}
void set_server(network::IPAddress ip, uint16_t port) final {
this->host_ = ip.str();
this->port_ = port;
}
void set_server(const char *host, uint16_t port) final {
this->host_ = host;
this->port_ = port;
}
void set_on_connect(std::function<on_connect_callback_t> &&callback) final {
this->on_connect_.add(std::move(callback));
}
void set_on_disconnect(std::function<on_disconnect_callback_t> &&callback) final {
this->on_disconnect_.add(std::move(callback));
}
void set_on_subscribe(std::function<on_subscribe_callback_t> &&callback) final {
this->on_subscribe_.add(std::move(callback));
}
void set_on_unsubscribe(std::function<on_unsubscribe_callback_t> &&callback) final {
this->on_unsubscribe_.add(std::move(callback));
}
void set_on_message(std::function<on_message_callback_t> &&callback) final {
this->on_message_.add(std::move(callback));
}
void set_on_publish(std::function<on_publish_user_callback_t> &&callback) final {
this->on_publish_.add(std::move(callback));
}
bool connected() const final { return this->is_connected_; }
void connect() final {
if (!is_initalized_) {
if (initialize_()) {
esp_mqtt_client_start(handler_.get());
}
}
}
void disconnect() final {
if (is_initalized_)
esp_mqtt_client_disconnect(handler_.get());
}
bool subscribe(const char *topic, uint8_t qos) final {
return esp_mqtt_client_subscribe(handler_.get(), topic, qos) != -1;
}
bool unsubscribe(const char *topic) final { return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; }
bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final {
#if defined(USE_MQTT_IDF_ENQUEUE)
// use the non-blocking version
// it can delay sending a couple of seconds but won't block
return esp_mqtt_client_enqueue(handler_.get(), topic, payload, length, qos, retain, true) != -1;
#else
// might block for several seconds, either due to network timeout (10s)
// or if publishing payloads longer than internal buffer (due to message fragmentation)
return esp_mqtt_client_publish(handler_.get(), topic, payload, length, qos, retain) != -1;
#endif
}
using MQTTBackend::publish;
void loop() final;
void set_ca_certificate(const std::string &cert) { ca_certificate_ = cert; }
void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; }
protected:
bool initialize_();
void mqtt_event_handler_(const esp_mqtt_event_t &event);
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data);
struct MqttClientDeleter {
void operator()(esp_mqtt_client *client_handler) { esp_mqtt_client_destroy(client_handler); }
};
using ClientHandler_ = std::unique_ptr<esp_mqtt_client, MqttClientDeleter>;
ClientHandler_ handler_;
bool is_connected_{false};
bool is_initalized_{false};
esp_mqtt_client_config_t mqtt_cfg_{};
std::string host_;
uint16_t port_;
std::string username_;
std::string password_;
std::string lwt_topic_;
std::string lwt_message_;
uint8_t lwt_qos_;
bool lwt_retain_;
std::string client_id_;
uint16_t keep_alive_;
bool clean_session_;
optional<std::string> ca_certificate_;
bool skip_cert_cn_check_{false};
// callbacks
CallbackManager<on_connect_callback_t> on_connect_;
CallbackManager<on_disconnect_callback_t> on_disconnect_;
CallbackManager<on_subscribe_callback_t> on_subscribe_;
CallbackManager<on_unsubscribe_callback_t> on_unsubscribe_;
CallbackManager<on_message_callback_t> on_message_;
CallbackManager<on_publish_user_callback_t> on_publish_;
std::queue<esp_mqtt_event_t> mqtt_events_;
};
} // namespace mqtt
} // namespace esphome
#endif

View file

@ -27,21 +27,21 @@ MQTTClientComponent::MQTTClientComponent() {
// Connection // Connection
void MQTTClientComponent::setup() { void MQTTClientComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up MQTT..."); ESP_LOGCONFIG(TAG, "Setting up MQTT...");
this->mqtt_client_.onMessage([this](char const *topic, char *payload, AsyncMqttClientMessageProperties properties, this->mqtt_backend_.set_on_message(
size_t len, size_t index, size_t total) { [this](const char *topic, const char *payload, size_t len, size_t index, size_t total) {
if (index == 0) if (index == 0)
this->payload_buffer_.reserve(total); this->payload_buffer_.reserve(total);
// append new payload, may contain incomplete MQTT message // append new payload, may contain incomplete MQTT message
this->payload_buffer_.append(payload, len); this->payload_buffer_.append(payload, len);
// MQTT fully received // MQTT fully received
if (len + index == total) { if (len + index == total) {
this->on_message(topic, this->payload_buffer_); this->on_message(topic, this->payload_buffer_);
this->payload_buffer_.clear(); this->payload_buffer_.clear();
} }
}); });
this->mqtt_client_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { this->mqtt_backend_.set_on_disconnect([this](MQTTClientDisconnectReason reason) {
this->state_ = MQTT_CLIENT_DISCONNECTED; this->state_ = MQTT_CLIENT_DISCONNECTED;
this->disconnect_reason_ = reason; this->disconnect_reason_ = reason;
}); });
@ -49,8 +49,10 @@ void MQTTClientComponent::setup() {
if (this->is_log_message_enabled() && logger::global_logger != nullptr) { if (this->is_log_message_enabled() && logger::global_logger != nullptr) {
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
if (level <= this->log_level_ && this->is_connected()) { if (level <= this->log_level_ && this->is_connected()) {
this->publish(this->log_message_.topic, message, strlen(message), this->log_message_.qos, this->publish({.topic = this->log_message_.topic,
this->log_message_.retain); .payload = message,
.qos = this->log_message_.qos,
.retain = this->log_message_.retain});
} }
}); });
} }
@ -173,9 +175,9 @@ void MQTTClientComponent::start_connect_() {
ESP_LOGI(TAG, "Connecting to MQTT..."); ESP_LOGI(TAG, "Connecting to MQTT...");
// Force disconnect first // Force disconnect first
this->mqtt_client_.disconnect(true); this->mqtt_backend_.disconnect();
this->mqtt_client_.setClientId(this->credentials_.client_id.c_str()); this->mqtt_backend_.set_client_id(this->credentials_.client_id.c_str());
const char *username = nullptr; const char *username = nullptr;
if (!this->credentials_.username.empty()) if (!this->credentials_.username.empty())
username = this->credentials_.username.c_str(); username = this->credentials_.username.c_str();
@ -183,24 +185,24 @@ void MQTTClientComponent::start_connect_() {
if (!this->credentials_.password.empty()) if (!this->credentials_.password.empty())
password = this->credentials_.password.c_str(); password = this->credentials_.password.c_str();
this->mqtt_client_.setCredentials(username, password); this->mqtt_backend_.set_credentials(username, password);
this->mqtt_client_.setServer((uint32_t) this->ip_, this->credentials_.port); this->mqtt_backend_.set_server((uint32_t) this->ip_, this->credentials_.port);
if (!this->last_will_.topic.empty()) { if (!this->last_will_.topic.empty()) {
this->mqtt_client_.setWill(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain, this->mqtt_backend_.set_will(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain,
this->last_will_.payload.c_str(), this->last_will_.payload.length()); this->last_will_.payload.c_str());
} }
this->mqtt_client_.connect(); this->mqtt_backend_.connect();
this->state_ = MQTT_CLIENT_CONNECTING; this->state_ = MQTT_CLIENT_CONNECTING;
this->connect_begin_ = millis(); this->connect_begin_ = millis();
} }
bool MQTTClientComponent::is_connected() { bool MQTTClientComponent::is_connected() {
return this->state_ == MQTT_CLIENT_CONNECTED && this->mqtt_client_.connected(); return this->state_ == MQTT_CLIENT_CONNECTED && this->mqtt_backend_.connected();
} }
void MQTTClientComponent::check_connected() { void MQTTClientComponent::check_connected() {
if (!this->mqtt_client_.connected()) { if (!this->mqtt_backend_.connected()) {
if (millis() - this->connect_begin_ > 60000) { if (millis() - this->connect_begin_ > 60000) {
this->state_ = MQTT_CLIENT_DISCONNECTED; this->state_ = MQTT_CLIENT_DISCONNECTED;
this->start_dnslookup_(); this->start_dnslookup_();
@ -222,31 +224,34 @@ void MQTTClientComponent::check_connected() {
} }
void MQTTClientComponent::loop() { void MQTTClientComponent::loop() {
// Call the backend loop first
mqtt_backend_.loop();
if (this->disconnect_reason_.has_value()) { if (this->disconnect_reason_.has_value()) {
const LogString *reason_s; const LogString *reason_s;
switch (*this->disconnect_reason_) { switch (*this->disconnect_reason_) {
case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED: case MQTTClientDisconnectReason::TCP_DISCONNECTED:
reason_s = LOG_STR("TCP disconnected"); reason_s = LOG_STR("TCP disconnected");
break; break;
case AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
reason_s = LOG_STR("Unacceptable Protocol Version"); reason_s = LOG_STR("Unacceptable Protocol Version");
break; break;
case AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED:
reason_s = LOG_STR("Identifier Rejected"); reason_s = LOG_STR("Identifier Rejected");
break; break;
case AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE:
reason_s = LOG_STR("Server Unavailable"); reason_s = LOG_STR("Server Unavailable");
break; break;
case AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS:
reason_s = LOG_STR("Malformed Credentials"); reason_s = LOG_STR("Malformed Credentials");
break; break;
case AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED: case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED:
reason_s = LOG_STR("Not Authorized"); reason_s = LOG_STR("Not Authorized");
break; break;
case AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE: case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE:
reason_s = LOG_STR("Not Enough Space"); reason_s = LOG_STR("Not Enough Space");
break; break;
case AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT: case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT:
reason_s = LOG_STR("TLS Bad Fingerprint"); reason_s = LOG_STR("TLS Bad Fingerprint");
break; break;
default: default:
@ -275,7 +280,7 @@ void MQTTClientComponent::loop() {
this->check_connected(); this->check_connected();
break; break;
case MQTT_CLIENT_CONNECTED: case MQTT_CLIENT_CONNECTED:
if (!this->mqtt_client_.connected()) { if (!this->mqtt_backend_.connected()) {
this->state_ = MQTT_CLIENT_DISCONNECTED; this->state_ = MQTT_CLIENT_DISCONNECTED;
ESP_LOGW(TAG, "Lost MQTT Client connection!"); ESP_LOGW(TAG, "Lost MQTT Client connection!");
this->start_dnslookup_(); this->start_dnslookup_();
@ -302,10 +307,10 @@ bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) {
if (!this->is_connected()) if (!this->is_connected())
return false; return false;
uint16_t ret = this->mqtt_client_.subscribe(topic, qos); bool ret = this->mqtt_backend_.subscribe(topic, qos);
yield(); yield();
if (ret != 0) { if (ret) {
ESP_LOGV(TAG, "subscribe(topic='%s')", topic); ESP_LOGV(TAG, "subscribe(topic='%s')", topic);
} else { } else {
delay(5); delay(5);
@ -360,9 +365,9 @@ void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_js
} }
void MQTTClientComponent::unsubscribe(const std::string &topic) { void MQTTClientComponent::unsubscribe(const std::string &topic) {
uint16_t ret = this->mqtt_client_.unsubscribe(topic.c_str()); bool ret = this->mqtt_backend_.unsubscribe(topic.c_str());
yield(); yield();
if (ret != 0) { if (ret) {
ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str()); ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str());
} else { } else {
delay(5); delay(5);
@ -387,34 +392,35 @@ bool MQTTClientComponent::publish(const std::string &topic, const std::string &p
bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos, bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos,
bool retain) { bool retain) {
return publish({.topic = topic, .payload = payload, .qos = qos, .retain = retain});
}
bool MQTTClientComponent::publish(const MQTTMessage &message) {
if (!this->is_connected()) { if (!this->is_connected()) {
// critical components will re-transmit their messages // critical components will re-transmit their messages
return false; return false;
} }
bool logging_topic = topic == this->log_message_.topic; bool logging_topic = this->log_message_.topic == message.topic;
uint16_t ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length); bool ret = this->mqtt_backend_.publish(message);
delay(0); delay(0);
if (ret == 0 && !logging_topic && this->is_connected()) { if (!ret && !logging_topic && this->is_connected()) {
delay(0); delay(0);
ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length); ret = this->mqtt_backend_.publish(message);
delay(0); delay(0);
} }
if (!logging_topic) { if (!logging_topic) {
if (ret != 0) { if (ret) {
ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", topic.c_str(), payload, retain); ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", message.topic.c_str(), message.payload.c_str(),
message.retain);
} else { } else {
ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", topic.c_str(), ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", message.topic.c_str(),
payload_length); // NOLINT message.payload.length());
this->status_momentary_warning("publish", 1000); this->status_momentary_warning("publish", 1000);
} }
} }
return ret != 0; return ret != 0;
} }
bool MQTTClientComponent::publish(const MQTTMessage &message) {
return this->publish(message.topic, message.payload, message.qos, message.retain);
}
bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos,
bool retain) { bool retain) {
std::string message = json::build_json(f); std::string message = json::build_json(f);
@ -499,10 +505,10 @@ bool MQTTClientComponent::is_log_message_enabled() const { return !this->log_mes
void MQTTClientComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } void MQTTClientComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { this->children_.push_back(component); } void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { this->children_.push_back(component); }
void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; } void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; }
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_client_.setKeepAlive(keep_alive_s); } void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); }
void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); } void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; } const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
void MQTTClientComponent::set_topic_prefix(std::string topic_prefix) { this->topic_prefix_ = std::move(topic_prefix); } void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix) { this->topic_prefix_ = topic_prefix; }
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; } const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
void MQTTClientComponent::disable_birth_message() { void MQTTClientComponent::disable_birth_message() {
this->birth_message_.topic = ""; this->birth_message_.topic = "";
@ -549,7 +555,8 @@ void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscovery
void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; } void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; }
void MQTTClientComponent::disable_discovery() { void MQTTClientComponent::disable_discovery() {
this->discovery_info_ = MQTTDiscoveryInfo{.prefix = "", .retain = false}; this->discovery_info_ = MQTTDiscoveryInfo{
.prefix = "", .retain = false, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR};
} }
void MQTTClientComponent::on_shutdown() { void MQTTClientComponent::on_shutdown() {
if (!this->shutdown_message_.topic.empty()) { if (!this->shutdown_message_.topic.empty()) {
@ -557,13 +564,13 @@ void MQTTClientComponent::on_shutdown() {
this->publish(this->shutdown_message_); this->publish(this->shutdown_message_);
yield(); yield();
} }
this->mqtt_client_.disconnect(true); this->mqtt_backend_.disconnect();
} }
#if ASYNC_TCP_SSL_ENABLED #if ASYNC_TCP_SSL_ENABLED
void MQTTClientComponent::add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint) { void MQTTClientComponent::add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint) {
this->mqtt_client_.setSecure(true); this->mqtt_backend_.setSecure(true);
this->mqtt_client_.addServerFingerprint(fingerprint.data()); this->mqtt_backend_.addServerFingerprint(fingerprint.data());
} }
#endif #endif

View file

@ -9,7 +9,11 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/components/json/json_util.h" #include "esphome/components/json/json_util.h"
#include "esphome/components/network/ip_address.h" #include "esphome/components/network/ip_address.h"
#include <AsyncMqttClient.h> #if defined(USE_ESP_IDF)
#include "mqtt_backend_idf.h"
#elif defined(USE_ARDUINO)
#include "mqtt_backend_arduino.h"
#endif
#include "lwip/ip_addr.h" #include "lwip/ip_addr.h"
namespace esphome { namespace esphome {
@ -22,14 +26,6 @@ namespace mqtt {
using mqtt_callback_t = std::function<void(const std::string &, const std::string &)>; using mqtt_callback_t = std::function<void(const std::string &, const std::string &)>;
using mqtt_json_callback_t = std::function<void(const std::string &, JsonObject)>; using mqtt_json_callback_t = std::function<void(const std::string &, JsonObject)>;
/// internal struct for MQTT messages.
struct MQTTMessage {
std::string topic;
std::string payload;
uint8_t qos; ///< QoS. Only for last will testaments.
bool retain;
};
/// internal struct for MQTT subscriptions. /// internal struct for MQTT subscriptions.
struct MQTTSubscription { struct MQTTSubscription {
std::string topic; std::string topic;
@ -139,7 +135,10 @@ class MQTTClientComponent : public Component {
*/ */
void add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint); void add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint);
#endif #endif
#ifdef USE_ESP_IDF
void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); }
void set_skip_cert_cn_check(bool skip_check) { this->mqtt_backend_.set_skip_cert_cn_check(skip_check); }
#endif
const Availability &get_availability(); const Availability &get_availability();
/** Set the topic prefix that will be prepended to all topics together with "/". This will, in most cases, /** Set the topic prefix that will be prepended to all topics together with "/". This will, in most cases,
@ -150,7 +149,7 @@ class MQTTClientComponent : public Component {
* *
* @param topic_prefix The topic prefix. The last "/" is appended automatically. * @param topic_prefix The topic prefix. The last "/" is appended automatically.
*/ */
void set_topic_prefix(std::string topic_prefix); void set_topic_prefix(const std::string &topic_prefix);
/// Get the topic prefix of this device, using default if necessary /// Get the topic prefix of this device, using default if necessary
const std::string &get_topic_prefix() const; const std::string &get_topic_prefix() const;
@ -277,6 +276,7 @@ class MQTTClientComponent : public Component {
.prefix = "homeassistant", .prefix = "homeassistant",
.retain = true, .retain = true,
.clean = false, .clean = false,
.unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
}; };
std::string topic_prefix_{}; std::string topic_prefix_{};
MQTTMessage log_message_; MQTTMessage log_message_;
@ -284,7 +284,12 @@ class MQTTClientComponent : public Component {
int log_level_{ESPHOME_LOG_LEVEL}; int log_level_{ESPHOME_LOG_LEVEL};
std::vector<MQTTSubscription> subscriptions_; std::vector<MQTTSubscription> subscriptions_;
AsyncMqttClient mqtt_client_; #if defined(USE_ESP_IDF)
MQTTBackendIDF mqtt_backend_;
#elif defined(USE_ARDUINO)
MQTTBackendArduino mqtt_backend_;
#endif
MQTTClientState state_{MQTT_CLIENT_DISCONNECTED}; MQTTClientState state_{MQTT_CLIENT_DISCONNECTED};
network::IPAddress ip_; network::IPAddress ip_;
bool dns_resolved_{false}; bool dns_resolved_{false};
@ -293,7 +298,7 @@ class MQTTClientComponent : public Component {
uint32_t reboot_timeout_{300000}; uint32_t reboot_timeout_{300000};
uint32_t connect_begin_; uint32_t connect_begin_;
uint32_t last_connected_{0}; uint32_t last_connected_{0};
optional<AsyncMqttClientDisconnectReason> disconnect_reason_{}; optional<MQTTClientDisconnectReason> disconnect_reason_{};
}; };
extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View file

@ -1,3 +1,4 @@
from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
@ -5,3 +6,7 @@ CODEOWNERS = ["@jesserockz"]
nfc_ns = cg.esphome_ns.namespace("nfc") nfc_ns = cg.esphome_ns.namespace("nfc")
NfcTag = nfc_ns.class_("NfcTag") NfcTag = nfc_ns.class_("NfcTag")
NfcOnTagTrigger = nfc_ns.class_(
"NfcOnTagTrigger", automation.Trigger.template(cg.std_string, NfcTag)
)

View file

@ -0,0 +1,9 @@
#include "automation.h"
namespace esphome {
namespace nfc {
void NfcOnTagTrigger::process(const std::unique_ptr<NfcTag> &tag) { this->trigger(format_uid(tag->get_uid()), *tag); }
} // namespace nfc
} // namespace esphome

View file

@ -0,0 +1,17 @@
#pragma once
#include <string>
#include "esphome/core/automation.h"
#include "nfc.h"
namespace esphome {
namespace nfc {
class NfcOnTagTrigger : public Trigger<std::string, NfcTag> {
public:
void process(const std::unique_ptr<NfcTag> &tag);
};
} // namespace nfc
} // namespace esphome

View file

@ -63,8 +63,8 @@ NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e
cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger),
cv.Optional(CONF_ABOVE): cv.float_, cv.Optional(CONF_ABOVE): cv.templatable(cv.float_),
cv.Optional(CONF_BELOW): cv.float_, cv.Optional(CONF_BELOW): cv.templatable(cv.float_),
}, },
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
), ),

View file

@ -474,7 +474,7 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_
}); });
// Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised. // Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised.
delay(100); // NOLINT delay(300); // NOLINT
App.setup(); App.setup();
ESP_LOGI(TAG, "Waiting for OTA attempt."); ESP_LOGI(TAG, "Waiting for OTA attempt.");

View file

@ -14,9 +14,6 @@ CONF_ON_FINISHED_WRITE = "on_finished_write"
pn532_ns = cg.esphome_ns.namespace("pn532") pn532_ns = cg.esphome_ns.namespace("pn532")
PN532 = pn532_ns.class_("PN532", cg.PollingComponent) PN532 = pn532_ns.class_("PN532", cg.PollingComponent)
PN532OnTagTrigger = pn532_ns.class_(
"PN532OnTagTrigger", automation.Trigger.template(cg.std_string, nfc.NfcTag)
)
PN532OnFinishedWriteTrigger = pn532_ns.class_( PN532OnFinishedWriteTrigger = pn532_ns.class_(
"PN532OnFinishedWriteTrigger", automation.Trigger.template() "PN532OnFinishedWriteTrigger", automation.Trigger.template()
) )
@ -30,7 +27,7 @@ PN532_SCHEMA = cv.Schema(
cv.GenerateID(): cv.declare_id(PN532), cv.GenerateID(): cv.declare_id(PN532),
cv.Optional(CONF_ON_TAG): automation.validate_automation( cv.Optional(CONF_ON_TAG): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
} }
), ),
cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation(
@ -42,7 +39,7 @@ PN532_SCHEMA = cv.Schema(
), ),
cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
} }
), ),
} }

View file

@ -144,9 +144,9 @@ void PN532::loop() {
} }
if (nfcid.size() == this->current_uid_.size()) { if (nfcid.size() == this->current_uid_.size()) {
bool same_uid = false; bool same_uid = true;
for (size_t i = 0; i < nfcid.size(); i++) for (size_t i = 0; i < nfcid.size(); i++)
same_uid |= nfcid[i] == this->current_uid_[i]; same_uid &= nfcid[i] == this->current_uid_[i];
if (same_uid) if (same_uid)
return; return;
} }
@ -376,9 +376,6 @@ bool PN532BinarySensor::process(std::vector<uint8_t> &data) {
this->found_ = true; this->found_ = true;
return true; return true;
} }
void PN532OnTagTrigger::process(const std::unique_ptr<nfc::NfcTag> &tag) {
this->trigger(nfc::format_uid(tag->get_uid()), *tag);
}
} // namespace pn532 } // namespace pn532
} // namespace esphome } // namespace esphome

View file

@ -5,6 +5,7 @@
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/nfc/nfc_tag.h" #include "esphome/components/nfc/nfc_tag.h"
#include "esphome/components/nfc/nfc.h" #include "esphome/components/nfc/nfc.h"
#include "esphome/components/nfc/automation.h"
namespace esphome { namespace esphome {
namespace pn532 { namespace pn532 {
@ -16,7 +17,6 @@ static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40;
static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A;
class PN532BinarySensor; class PN532BinarySensor;
class PN532OnTagTrigger;
class PN532 : public PollingComponent { class PN532 : public PollingComponent {
public: public:
@ -30,8 +30,8 @@ class PN532 : public PollingComponent {
void loop() override; void loop() override;
void register_tag(PN532BinarySensor *tag) { this->binary_sensors_.push_back(tag); } void register_tag(PN532BinarySensor *tag) { this->binary_sensors_.push_back(tag); }
void register_ontag_trigger(PN532OnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); }
void register_ontagremoved_trigger(PN532OnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); }
void add_on_finished_write_callback(std::function<void()> callback) { void add_on_finished_write_callback(std::function<void()> callback) {
this->on_finished_write_callback_.add(std::move(callback)); this->on_finished_write_callback_.add(std::move(callback));
@ -79,8 +79,8 @@ class PN532 : public PollingComponent {
bool requested_read_{false}; bool requested_read_{false};
std::vector<PN532BinarySensor *> binary_sensors_; std::vector<PN532BinarySensor *> binary_sensors_;
std::vector<PN532OnTagTrigger *> triggers_ontag_; std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_;
std::vector<PN532OnTagTrigger *> triggers_ontagremoved_; std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
std::vector<uint8_t> current_uid_; std::vector<uint8_t> current_uid_;
nfc::NdefMessage *next_task_message_to_write_; nfc::NdefMessage *next_task_message_to_write_;
enum NfcTask { enum NfcTask {
@ -115,11 +115,6 @@ class PN532BinarySensor : public binary_sensor::BinarySensor {
bool found_{false}; bool found_{false};
}; };
class PN532OnTagTrigger : public Trigger<std::string, nfc::NfcTag> {
public:
void process(const std::unique_ptr<nfc::NfcTag> &tag);
};
class PN532OnFinishedWriteTrigger : public Trigger<> { class PN532OnFinishedWriteTrigger : public Trigger<> {
public: public:
explicit PN532OnFinishedWriteTrigger(PN532 *parent) { explicit PN532OnFinishedWriteTrigger(PN532 *parent) {

View file

@ -0,0 +1 @@
CODEOWNERS = ["@andrewpc"]

View file

@ -0,0 +1,397 @@
#include "qmp6988.h"
#include <cmath>
namespace esphome {
namespace qmp6988 {
static const uint8_t QMP6988_CHIP_ID = 0x5C;
static const uint8_t QMP6988_CHIP_ID_REG = 0xD1; /* Chip ID confirmation Register */
static const uint8_t QMP6988_RESET_REG = 0xE0; /* Device reset register */
static const uint8_t QMP6988_DEVICE_STAT_REG = 0xF3; /* Device state register */
static const uint8_t QMP6988_CTRLMEAS_REG = 0xF4; /* Measurement Condition Control Register */
/* data */
static const uint8_t QMP6988_PRESSURE_MSB_REG = 0xF7; /* Pressure MSB Register */
static const uint8_t QMP6988_TEMPERATURE_MSB_REG = 0xFA; /* Temperature MSB Reg */
/* compensation calculation */
static const uint8_t QMP6988_CALIBRATION_DATA_START = 0xA0; /* QMP6988 compensation coefficients */
static const uint8_t QMP6988_CALIBRATION_DATA_LENGTH = 25;
static const uint8_t SHIFT_RIGHT_4_POSITION = 4;
static const uint8_t SHIFT_LEFT_2_POSITION = 2;
static const uint8_t SHIFT_LEFT_4_POSITION = 4;
static const uint8_t SHIFT_LEFT_5_POSITION = 5;
static const uint8_t SHIFT_LEFT_8_POSITION = 8;
static const uint8_t SHIFT_LEFT_12_POSITION = 12;
static const uint8_t SHIFT_LEFT_16_POSITION = 16;
/* power mode */
static const uint8_t QMP6988_SLEEP_MODE = 0x00;
static const uint8_t QMP6988_FORCED_MODE = 0x01;
static const uint8_t QMP6988_NORMAL_MODE = 0x03;
static const uint8_t QMP6988_CTRLMEAS_REG_MODE_POS = 0;
static const uint8_t QMP6988_CTRLMEAS_REG_MODE_MSK = 0x03;
static const uint8_t QMP6988_CTRLMEAS_REG_MODE_LEN = 2;
static const uint8_t QMP6988_CTRLMEAS_REG_OSRST_POS = 5;
static const uint8_t QMP6988_CTRLMEAS_REG_OSRST_MSK = 0xE0;
static const uint8_t QMP6988_CTRLMEAS_REG_OSRST_LEN = 3;
static const uint8_t QMP6988_CTRLMEAS_REG_OSRSP_POS = 2;
static const uint8_t QMP6988_CTRLMEAS_REG_OSRSP_MSK = 0x1C;
static const uint8_t QMP6988_CTRLMEAS_REG_OSRSP_LEN = 3;
static const uint8_t QMP6988_CONFIG_REG = 0xF1; /*IIR filter co-efficient setting Register*/
static const uint8_t QMP6988_CONFIG_REG_FILTER_POS = 0;
static const uint8_t QMP6988_CONFIG_REG_FILTER_MSK = 0x07;
static const uint8_t QMP6988_CONFIG_REG_FILTER_LEN = 3;
static const uint32_t SUBTRACTOR = 8388608;
static const char *const TAG = "qmp6988";
static const char *oversampling_to_str(QMP6988Oversampling oversampling) {
switch (oversampling) {
case QMP6988_OVERSAMPLING_SKIPPED:
return "None";
case QMP6988_OVERSAMPLING_1X:
return "1x";
case QMP6988_OVERSAMPLING_2X:
return "2x";
case QMP6988_OVERSAMPLING_4X:
return "4x";
case QMP6988_OVERSAMPLING_8X:
return "8x";
case QMP6988_OVERSAMPLING_16X:
return "16x";
case QMP6988_OVERSAMPLING_32X:
return "32x";
case QMP6988_OVERSAMPLING_64X:
return "64x";
default:
return "UNKNOWN";
}
}
static const char *iir_filter_to_str(QMP6988IIRFilter filter) {
switch (filter) {
case QMP6988_IIR_FILTER_OFF:
return "OFF";
case QMP6988_IIR_FILTER_2X:
return "2x";
case QMP6988_IIR_FILTER_4X:
return "4x";
case QMP6988_IIR_FILTER_8X:
return "8x";
case QMP6988_IIR_FILTER_16X:
return "16x";
case QMP6988_IIR_FILTER_32X:
return "32x";
default:
return "UNKNOWN";
}
}
bool QMP6988Component::device_check_() {
uint8_t ret = 0;
ret = this->read_register(QMP6988_CHIP_ID_REG, &(qmp6988_data_.chip_id), 1);
if (ret != i2c::ERROR_OK) {
ESP_LOGE(TAG, "%s: read chip ID (0xD1) failed", __func__);
}
ESP_LOGD(TAG, "qmp6988 read chip id = 0x%x", qmp6988_data_.chip_id);
return qmp6988_data_.chip_id == QMP6988_CHIP_ID;
}
bool QMP6988Component::get_calibration_data_() {
uint8_t status = 0;
// BITFIELDS temp_COE;
uint8_t a_data_uint8_tr[QMP6988_CALIBRATION_DATA_LENGTH] = {0};
int len;
for (len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) {
status = this->read_register(QMP6988_CALIBRATION_DATA_START + len, &a_data_uint8_tr[len], 1);
if (status != i2c::ERROR_OK) {
ESP_LOGE(TAG, "qmp6988 read calibration data (0xA0) error!");
return false;
}
}
qmp6988_data_.qmp6988_cali.COE_a0 =
(QMP6988_S32_t)(((a_data_uint8_tr[18] << SHIFT_LEFT_12_POSITION) |
(a_data_uint8_tr[19] << SHIFT_LEFT_4_POSITION) | (a_data_uint8_tr[24] & 0x0f))
<< 12);
qmp6988_data_.qmp6988_cali.COE_a0 = qmp6988_data_.qmp6988_cali.COE_a0 >> 12;
qmp6988_data_.qmp6988_cali.COE_a1 =
(QMP6988_S16_t)(((a_data_uint8_tr[20]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[21]);
qmp6988_data_.qmp6988_cali.COE_a2 =
(QMP6988_S16_t)(((a_data_uint8_tr[22]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[23]);
qmp6988_data_.qmp6988_cali.COE_b00 =
(QMP6988_S32_t)(((a_data_uint8_tr[0] << SHIFT_LEFT_12_POSITION) | (a_data_uint8_tr[1] << SHIFT_LEFT_4_POSITION) |
((a_data_uint8_tr[24] & 0xf0) >> SHIFT_RIGHT_4_POSITION))
<< 12);
qmp6988_data_.qmp6988_cali.COE_b00 = qmp6988_data_.qmp6988_cali.COE_b00 >> 12;
qmp6988_data_.qmp6988_cali.COE_bt1 =
(QMP6988_S16_t)(((a_data_uint8_tr[2]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[3]);
qmp6988_data_.qmp6988_cali.COE_bt2 =
(QMP6988_S16_t)(((a_data_uint8_tr[4]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[5]);
qmp6988_data_.qmp6988_cali.COE_bp1 =
(QMP6988_S16_t)(((a_data_uint8_tr[6]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[7]);
qmp6988_data_.qmp6988_cali.COE_b11 =
(QMP6988_S16_t)(((a_data_uint8_tr[8]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[9]);
qmp6988_data_.qmp6988_cali.COE_bp2 =
(QMP6988_S16_t)(((a_data_uint8_tr[10]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[11]);
qmp6988_data_.qmp6988_cali.COE_b12 =
(QMP6988_S16_t)(((a_data_uint8_tr[12]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[13]);
qmp6988_data_.qmp6988_cali.COE_b21 =
(QMP6988_S16_t)(((a_data_uint8_tr[14]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[15]);
qmp6988_data_.qmp6988_cali.COE_bp3 =
(QMP6988_S16_t)(((a_data_uint8_tr[16]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[17]);
ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n");
ESP_LOGV(TAG, "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_a0,
qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, qmp6988_data_.qmp6988_cali.COE_b00);
ESP_LOGV(TAG, "COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bt1,
qmp6988_data_.qmp6988_cali.COE_bt2, qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11);
ESP_LOGV(TAG, "COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bp2,
qmp6988_data_.qmp6988_cali.COE_b12, qmp6988_data_.qmp6988_cali.COE_b21, qmp6988_data_.qmp6988_cali.COE_bp3);
ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n");
qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4
qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4
qmp6988_data_.ik.a1 = 3608L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a1 - 1731677965L; // 31Q23
qmp6988_data_.ik.a2 = 16889L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a2 - 87619360L; // 30Q47
qmp6988_data_.ik.bt1 = 2982L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt1 + 107370906L; // 28Q15
qmp6988_data_.ik.bt2 = 329854L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt2 + 108083093L; // 34Q38
qmp6988_data_.ik.bp1 = 19923L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp1 + 1133836764L; // 31Q20
qmp6988_data_.ik.b11 = 2406L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b11 + 118215883L; // 28Q34
qmp6988_data_.ik.bp2 = 3079L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp2 - 181579595L; // 29Q43
qmp6988_data_.ik.b12 = 6846L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53
qmp6988_data_.ik.b21 = 13836L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60
qmp6988_data_.ik.bp3 = 2915L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65
ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n");
ESP_LOGV(TAG, "a0[%d] a1[%d] a2[%d] b00[%d]\r\n", qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2,
qmp6988_data_.ik.b00);
ESP_LOGV(TAG, "bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n", qmp6988_data_.ik.bt1, qmp6988_data_.ik.bt2,
qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11);
ESP_LOGV(TAG, "bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n", qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12,
qmp6988_data_.ik.b21, qmp6988_data_.ik.bp3);
ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n");
return true;
}
QMP6988_S16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt) {
QMP6988_S16_t ret;
QMP6988_S64_t wk1, wk2;
// wk1: 60Q4 // bit size
wk1 = ((QMP6988_S64_t) ik->a1 * (QMP6988_S64_t) dt); // 31Q23+24-1=54 (54Q23)
wk2 = ((QMP6988_S64_t) ik->a2 * (QMP6988_S64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33)
wk2 = (wk2 * (QMP6988_S64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23)
wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04)
ret = (QMP6988_S16_t)((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0
return ret;
}
QMP6988_S32_t QMP6988Component::get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx) {
QMP6988_S32_t ret;
QMP6988_S64_t wk1, wk2, wk3;
// wk1 = 48Q16 // bit size
wk1 = ((QMP6988_S64_t) ik->bt1 * (QMP6988_S64_t) tx); // 28Q15+16-1=43 (43Q15)
wk2 = ((QMP6988_S64_t) ik->bp1 * (QMP6988_S64_t) dp) >> 5; // 31Q20+24-1=54 (49Q15)
wk1 += wk2; // 43,49->50Q15
wk2 = ((QMP6988_S64_t) ik->bt2 * (QMP6988_S64_t) tx) >> 1; // 34Q38+16-1=49 (48Q37)
wk2 = (wk2 * (QMP6988_S64_t) tx) >> 8; // 48Q37+16-1=63 (55Q29)
wk3 = wk2; // 55Q29
wk2 = ((QMP6988_S64_t) ik->b11 * (QMP6988_S64_t) tx) >> 4; // 28Q34+16-1=43 (39Q30)
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 55,61->62Q29
wk2 = ((QMP6988_S64_t) ik->bp2 * (QMP6988_S64_t) dp) >> 13; // 29Q43+24-1=52 (39Q30)
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 62,61->63Q29
wk1 += wk3 >> 14; // Q29 >> 14 -> Q15
wk2 = ((QMP6988_S64_t) ik->b12 * (QMP6988_S64_t) tx); // 29Q53+16-1=45 (45Q53)
wk2 = (wk2 * (QMP6988_S64_t) tx) >> 22; // 45Q53+16-1=61 (39Q31)
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q30)
wk3 = wk2; // 61Q30
wk2 = ((QMP6988_S64_t) ik->b21 * (QMP6988_S64_t) tx) >> 6; // 29Q60+16-1=45 (39Q54)
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q54+24-1=62 (39Q31)
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q20)
wk3 += wk2; // 61,61->62Q30
wk2 = ((QMP6988_S64_t) ik->bp3 * (QMP6988_S64_t) dp) >> 12; // 28Q65+24-1=51 (39Q53)
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q53+24-1=62 (39Q30)
wk2 = (wk2 * (QMP6988_S64_t) dp); // 39Q30+24-1=62 (62Q30)
wk3 += wk2; // 62,62->63Q30
wk1 += wk3 >> 15; // Q30 >> 15 = Q15
wk1 /= 32767L;
wk1 >>= 11; // Q15 >> 7 = Q4
wk1 += ik->b00; // Q4 + 20Q4
// wk1 >>= 4; // 28Q4 -> 24Q0
ret = (QMP6988_S32_t) wk1;
return ret;
}
void QMP6988Component::software_reset_() {
uint8_t ret = 0;
ret = this->write_byte(QMP6988_RESET_REG, 0xe6);
if (ret != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Software Reset (0xe6) failed");
}
delay(10);
this->write_byte(QMP6988_RESET_REG, 0x00);
}
void QMP6988Component::set_power_mode_(uint8_t power_mode) {
uint8_t data;
ESP_LOGD(TAG, "Setting Power mode to: %d", power_mode);
qmp6988_data_.power_mode = power_mode;
this->read_register(QMP6988_CTRLMEAS_REG, &data, 1);
data = data & 0xfc;
if (power_mode == QMP6988_SLEEP_MODE) {
data |= 0x00;
} else if (power_mode == QMP6988_FORCED_MODE) {
data |= 0x01;
} else if (power_mode == QMP6988_NORMAL_MODE) {
data |= 0x03;
}
this->write_byte(QMP6988_CTRLMEAS_REG, data);
ESP_LOGD(TAG, "Set Power mode 0xf4=0x%x \r\n", data);
delay(10);
}
void QMP6988Component::write_filter_(unsigned char filter) {
uint8_t data;
data = (filter & 0x03);
this->write_byte(QMP6988_CONFIG_REG, data);
delay(10);
}
void QMP6988Component::write_oversampling_pressure_(unsigned char oversampling_p) {
uint8_t data;
this->read_register(QMP6988_CTRLMEAS_REG, &data, 1);
data &= 0xe3;
data |= (oversampling_p << 2);
this->write_byte(QMP6988_CTRLMEAS_REG, data);
delay(10);
}
void QMP6988Component::write_oversampling_temperature_(unsigned char oversampling_t) {
uint8_t data;
this->read_register(QMP6988_CTRLMEAS_REG, &data, 1);
data &= 0x1f;
data |= (oversampling_t << 5);
this->write_byte(QMP6988_CTRLMEAS_REG, data);
delay(10);
}
void QMP6988Component::set_temperature_oversampling(QMP6988Oversampling oversampling_t) {
this->temperature_oversampling_ = oversampling_t;
}
void QMP6988Component::set_pressure_oversampling(QMP6988Oversampling oversampling_p) {
this->pressure_oversampling_ = oversampling_p;
}
void QMP6988Component::set_iir_filter(QMP6988IIRFilter iirfilter) { this->iir_filter_ = iirfilter; }
void QMP6988Component::calculate_altitude_(float pressure, float temp) {
float altitude;
altitude = (pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065;
this->qmp6988_data_.altitude = altitude;
}
void QMP6988Component::calculate_pressure_() {
uint8_t err = 0;
QMP6988_U32_t p_read, t_read;
QMP6988_S32_t p_raw, t_raw;
uint8_t a_data_uint8_tr[6] = {0};
QMP6988_S32_t t_int, p_int;
this->qmp6988_data_.temperature = 0;
this->qmp6988_data_.pressure = 0;
err = this->read_register(QMP6988_PRESSURE_MSB_REG, a_data_uint8_tr, 6);
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Error reading raw pressure/temp values");
return;
}
p_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[0])) << SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[1])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[2]));
p_raw = (QMP6988_S32_t)(p_read - SUBTRACTOR);
t_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[3])) << SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[4])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[5]));
t_raw = (QMP6988_S32_t)(t_read - SUBTRACTOR);
t_int = this->get_compensated_temperature_(&(qmp6988_data_.ik), t_raw);
p_int = this->get_compensated_pressure_(&(qmp6988_data_.ik), p_raw, t_int);
this->qmp6988_data_.temperature = (float) t_int / 256.0f;
this->qmp6988_data_.pressure = (float) p_int / 16.0f;
}
void QMP6988Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up QMP6988");
bool ret;
ret = this->device_check_();
if (!ret) {
ESP_LOGCONFIG(TAG, "Setup failed - device not found");
}
this->software_reset_();
this->get_calibration_data_();
this->set_power_mode_(QMP6988_NORMAL_MODE);
this->write_filter_(iir_filter_);
this->write_oversampling_pressure_(this->pressure_oversampling_);
this->write_oversampling_temperature_(this->temperature_oversampling_);
}
void QMP6988Component::dump_config() {
ESP_LOGCONFIG(TAG, "QMP6988:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with QMP6988 failed!");
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
ESP_LOGCONFIG(TAG, " Temperature Oversampling: %s", oversampling_to_str(this->temperature_oversampling_));
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
ESP_LOGCONFIG(TAG, " Pressure Oversampling: %s", oversampling_to_str(this->pressure_oversampling_));
ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_));
}
float QMP6988Component::get_setup_priority() const { return setup_priority::DATA; }
void QMP6988Component::update() {
this->calculate_pressure_();
float pressurehectopascals = this->qmp6988_data_.pressure / 100;
float temperature = this->qmp6988_data_.temperature;
ESP_LOGD(TAG, "Temperature=%.2f°C, Pressure=%.2fhPa", temperature, pressurehectopascals);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
if (this->pressure_sensor_ != nullptr)
this->pressure_sensor_->publish_state(pressurehectopascals);
}
} // namespace qmp6988
} // namespace esphome

View file

@ -0,0 +1,116 @@
#pragma once
#include "esphome/core/log.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace qmp6988 {
#define QMP6988_U16_t unsigned short
#define QMP6988_S16_t short
#define QMP6988_U32_t unsigned int
#define QMP6988_S32_t int
#define QMP6988_U64_t unsigned long long
#define QMP6988_S64_t long long
/* oversampling */
enum QMP6988Oversampling {
QMP6988_OVERSAMPLING_SKIPPED = 0x00,
QMP6988_OVERSAMPLING_1X = 0x01,
QMP6988_OVERSAMPLING_2X = 0x02,
QMP6988_OVERSAMPLING_4X = 0x03,
QMP6988_OVERSAMPLING_8X = 0x04,
QMP6988_OVERSAMPLING_16X = 0x05,
QMP6988_OVERSAMPLING_32X = 0x06,
QMP6988_OVERSAMPLING_64X = 0x07,
};
/* filter */
enum QMP6988IIRFilter {
QMP6988_IIR_FILTER_OFF = 0x00,
QMP6988_IIR_FILTER_2X = 0x01,
QMP6988_IIR_FILTER_4X = 0x02,
QMP6988_IIR_FILTER_8X = 0x03,
QMP6988_IIR_FILTER_16X = 0x04,
QMP6988_IIR_FILTER_32X = 0x05,
};
using qmp6988_cali_data_t = struct Qmp6988CaliData {
QMP6988_S32_t COE_a0;
QMP6988_S16_t COE_a1;
QMP6988_S16_t COE_a2;
QMP6988_S32_t COE_b00;
QMP6988_S16_t COE_bt1;
QMP6988_S16_t COE_bt2;
QMP6988_S16_t COE_bp1;
QMP6988_S16_t COE_b11;
QMP6988_S16_t COE_bp2;
QMP6988_S16_t COE_b12;
QMP6988_S16_t COE_b21;
QMP6988_S16_t COE_bp3;
};
using qmp6988_fk_data_t = struct Qmp6988FkData {
float a0, b00;
float a1, a2, bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
};
using qmp6988_ik_data_t = struct Qmp6988IkData {
QMP6988_S32_t a0, b00;
QMP6988_S32_t a1, a2;
QMP6988_S64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
};
using qmp6988_data_t = struct Qmp6988Data {
uint8_t chip_id;
uint8_t power_mode;
float temperature;
float pressure;
float altitude;
qmp6988_cali_data_t qmp6988_cali;
qmp6988_ik_data_t ik;
};
class QMP6988Component : public PollingComponent, public i2c::I2CDevice {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
void update() override;
void set_iir_filter(QMP6988IIRFilter iirfilter);
void set_temperature_oversampling(QMP6988Oversampling oversampling_t);
void set_pressure_oversampling(QMP6988Oversampling oversampling_p);
protected:
qmp6988_data_t qmp6988_data_;
sensor::Sensor *temperature_sensor_;
sensor::Sensor *pressure_sensor_;
QMP6988Oversampling temperature_oversampling_{QMP6988_OVERSAMPLING_16X};
QMP6988Oversampling pressure_oversampling_{QMP6988_OVERSAMPLING_16X};
QMP6988IIRFilter iir_filter_{QMP6988_IIR_FILTER_OFF};
void software_reset_();
bool get_calibration_data_();
bool device_check_();
void set_power_mode_(uint8_t power_mode);
void write_oversampling_temperature_(unsigned char oversampling_t);
void write_oversampling_pressure_(unsigned char oversampling_p);
void write_filter_(unsigned char filter);
void calculate_pressure_();
void calculate_altitude_(float pressure, float temp);
QMP6988_S32_t get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx);
QMP6988_S16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt);
};
} // namespace qmp6988
} // namespace esphome

View file

@ -0,0 +1,101 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ID,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
)
DEPENDENCIES = ["i2c"]
qmp6988_ns = cg.esphome_ns.namespace("qmp6988")
QMP6988Component = qmp6988_ns.class_(
"QMP6988Component", cg.PollingComponent, i2c.I2CDevice
)
QMP6988Oversampling = qmp6988_ns.enum("QMP6988Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": QMP6988Oversampling.QMP6988_OVERSAMPLING_SKIPPED,
"1X": QMP6988Oversampling.QMP6988_OVERSAMPLING_1X,
"2X": QMP6988Oversampling.QMP6988_OVERSAMPLING_2X,
"4X": QMP6988Oversampling.QMP6988_OVERSAMPLING_4X,
"8X": QMP6988Oversampling.QMP6988_OVERSAMPLING_8X,
"16X": QMP6988Oversampling.QMP6988_OVERSAMPLING_16X,
"32X": QMP6988Oversampling.QMP6988_OVERSAMPLING_32X,
"64X": QMP6988Oversampling.QMP6988_OVERSAMPLING_64X,
}
QMP6988IIRFilter = qmp6988_ns.enum("QMP6988IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": QMP6988IIRFilter.QMP6988_IIR_FILTER_OFF,
"2X": QMP6988IIRFilter.QMP6988_IIR_FILTER_2X,
"4X": QMP6988IIRFilter.QMP6988_IIR_FILTER_4X,
"8X": QMP6988IIRFilter.QMP6988_IIR_FILTER_8X,
"16X": QMP6988IIRFilter.QMP6988_IIR_FILTER_16X,
"32X": QMP6988IIRFilter.QMP6988_IIR_FILTER_32X,
}
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(QMP6988Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="8X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="8X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x70))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
conf = config[CONF_TEMPERATURE]
sens = await sensor.new_sensor(conf)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING]))
if CONF_PRESSURE in config:
conf = config[CONF_PRESSURE]
sens = await sensor.new_sensor(conf)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))

View file

@ -1,3 +1,10 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/rc522/rc522.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
/** /**
* Library based on https://github.com/miguelbalboa/rfid * Library based on https://github.com/miguelbalboa/rfid
* and adapted to ESPHome by @glmnet * and adapted to ESPHome by @glmnet
@ -6,14 +13,6 @@
* *
* *
*/ */
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/rc522/rc522.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace rc522_spi { namespace rc522_spi {
class RC522Spi : public rc522::RC522, class RC522Spi : public rc522::RC522,

View file

@ -40,6 +40,24 @@ namespace remote_base {
static const char *const TAG = "remote.pronto"; static const char *const TAG = "remote.pronto";
bool ProntoData::operator==(const ProntoData &rhs) const {
std::vector<uint16_t> data1 = encode_pronto(data);
std::vector<uint16_t> data2 = encode_pronto(rhs.data);
uint32_t total_diff = 0;
// Don't need to check the last one, it's the large gap at the end.
for (std::vector<uint16_t>::size_type i = 0; i < data1.size() - 1; ++i) {
int diff = data2[i] - data1[i];
diff *= diff;
if (diff > 9)
return false;
total_diff += diff;
}
return total_diff <= data1.size() * 3;
}
// DO NOT EXPORT from this file // DO NOT EXPORT from this file
static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU; static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU;
static const uint16_t LEARNED_TOKEN = 0x0000U; static const uint16_t LEARNED_TOKEN = 0x0000U;
@ -52,6 +70,7 @@ static const uint32_t REFERENCE_FREQUENCY = 4145146UL;
static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0;
static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL;
static const uint16_t PRONTO_DEFAULT_GAP = 45000; static const uint16_t PRONTO_DEFAULT_GAP = 45000;
static const uint16_t MARK_EXCESS_MICROS = 20;
static uint16_t to_frequency_k_hz(uint16_t code) { static uint16_t to_frequency_k_hz(uint16_t code) {
if (code == 0) if (code == 0)
@ -107,7 +126,7 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vector<uin
} }
} }
void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &str) { std::vector<uint16_t> encode_pronto(const std::string &str) {
size_t len = str.length() / (DIGITS_IN_PRONTO_NUMBER + 1) + 1; size_t len = str.length() / (DIGITS_IN_PRONTO_NUMBER + 1) + 1;
std::vector<uint16_t> data; std::vector<uint16_t> data;
const char *p = str.c_str(); const char *p = str.c_str();
@ -122,12 +141,90 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &st
data.push_back(x); // If input is conforming, there can be no overflow! data.push_back(x); // If input is conforming, there can be no overflow!
p = *endptr; p = *endptr;
} }
return data;
}
void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &str) {
std::vector<uint16_t> data = encode_pronto(str);
send_pronto_(dst, data); send_pronto_(dst, data);
} }
void ProntoProtocol::encode(RemoteTransmitData *dst, const ProntoData &data) { send_pronto_(dst, data.data); } void ProntoProtocol::encode(RemoteTransmitData *dst, const ProntoData &data) { send_pronto_(dst, data.data); }
optional<ProntoData> ProntoProtocol::decode(RemoteReceiveData src) { return {}; } uint16_t ProntoProtocol::effective_frequency_(uint16_t frequency) {
return frequency > 0 ? frequency : FALLBACK_FREQUENCY;
}
uint16_t ProntoProtocol::to_timebase_(uint16_t frequency) {
return MICROSECONDS_IN_SECONDS / effective_frequency_(frequency);
}
uint16_t ProntoProtocol::to_frequency_code_(uint16_t frequency) {
return REFERENCE_FREQUENCY / effective_frequency_(frequency);
}
std::string ProntoProtocol::dump_digit_(uint8_t x) {
return std::string(1, (char) (x <= 9 ? ('0' + x) : ('A' + (x - 10))));
}
std::string ProntoProtocol::dump_number_(uint16_t number, bool end /* = false */) {
std::string num;
for (uint8_t i = 0; i < DIGITS_IN_PRONTO_NUMBER; ++i) {
uint8_t shifts = BITS_IN_HEXADECIMAL * (DIGITS_IN_PRONTO_NUMBER - 1 - i);
num += dump_digit_((number >> shifts) & HEX_MASK);
}
if (!end)
num += ' ';
return num;
}
std::string ProntoProtocol::dump_duration_(uint32_t duration, uint16_t timebase, bool end /* = false */) {
return dump_number_((duration + timebase / 2) / timebase, end);
}
std::string ProntoProtocol::compensate_and_dump_sequence_(std::vector<int32_t> *data, uint16_t timebase) {
std::string out;
for (std::vector<int32_t>::size_type i = 0; i < data->size() - 1; i++) {
int32_t t_length = data->at(i);
uint32_t t_duration;
if (t_length > 0) {
// Mark
t_duration = t_length - MARK_EXCESS_MICROS;
} else {
t_duration = -t_length + MARK_EXCESS_MICROS;
}
out += dump_duration_(t_duration, timebase);
}
// append minimum gap
out += dump_duration_(PRONTO_DEFAULT_GAP, timebase, true);
return out;
}
optional<ProntoData> ProntoProtocol::decode(RemoteReceiveData src) {
ProntoData out;
uint16_t frequency = 38000U;
std::vector<int32_t> *data = src.get_raw_data();
std::string prontodata;
prontodata += dump_number_(frequency > 0 ? LEARNED_TOKEN : LEARNED_NON_MODULATED_TOKEN);
prontodata += dump_number_(to_frequency_code_(frequency));
prontodata += dump_number_((data->size() + 1) / 2);
prontodata += dump_number_(0);
uint16_t timebase = to_timebase_(frequency);
prontodata += compensate_and_dump_sequence_(data, timebase);
out.data = prontodata;
return out;
}
void ProntoProtocol::dump(const ProntoData &data) { ESP_LOGD(TAG, "Received Pronto: data=%s", data.data.c_str()); } void ProntoProtocol::dump(const ProntoData &data) { ESP_LOGD(TAG, "Received Pronto: data=%s", data.data.c_str()); }

View file

@ -6,10 +6,12 @@
namespace esphome { namespace esphome {
namespace remote_base { namespace remote_base {
std::vector<uint16_t> encode_pronto(const std::string &str);
struct ProntoData { struct ProntoData {
std::string data; std::string data;
bool operator==(const ProntoData &rhs) const { return data == rhs.data; } bool operator==(const ProntoData &rhs) const;
}; };
class ProntoProtocol : public RemoteProtocol<ProntoData> { class ProntoProtocol : public RemoteProtocol<ProntoData> {
@ -17,6 +19,14 @@ class ProntoProtocol : public RemoteProtocol<ProntoData> {
void send_pronto_(RemoteTransmitData *dst, const std::vector<uint16_t> &data); void send_pronto_(RemoteTransmitData *dst, const std::vector<uint16_t> &data);
void send_pronto_(RemoteTransmitData *dst, const std::string &str); void send_pronto_(RemoteTransmitData *dst, const std::string &str);
uint16_t effective_frequency_(uint16_t frequency);
uint16_t to_timebase_(uint16_t frequency);
uint16_t to_frequency_code_(uint16_t frequency);
std::string dump_digit_(uint8_t x);
std::string dump_number_(uint16_t number, bool end = false);
std::string dump_duration_(uint32_t duration, uint16_t timebase, bool end = false);
std::string compensate_and_dump_sequence_(std::vector<int32_t> *data, uint16_t timebase);
public: public:
void encode(RemoteTransmitData *dst, const ProntoData &data) override; void encode(RemoteTransmitData *dst, const ProntoData &data) override;
optional<ProntoData> decode(RemoteReceiveData src) override; optional<ProntoData> decode(RemoteReceiveData src) override;

View file

@ -33,14 +33,8 @@ void SCD30Component::setup() {
#endif #endif
/// Firmware version identification /// Firmware version identification
if (!this->write_command_(SCD30_CMD_GET_FIRMWARE_VERSION)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
uint16_t raw_firmware_version[3]; uint16_t raw_firmware_version[3];
if (!this->get_register(SCD30_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 3)) {
if (!this->read_data_(raw_firmware_version, 3)) {
this->error_code_ = FIRMWARE_IDENTIFICATION_FAILED; this->error_code_ = FIRMWARE_IDENTIFICATION_FAILED;
this->mark_failed(); this->mark_failed();
return; return;
@ -49,7 +43,7 @@ void SCD30Component::setup() {
uint16_t(raw_firmware_version[0] & 0xFF)); uint16_t(raw_firmware_version[0] & 0xFF));
if (this->temperature_offset_ != 0) { if (this->temperature_offset_ != 0) {
if (!this->write_command_(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) { if (!this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) {
ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset."); ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -69,7 +63,7 @@ void SCD30Component::setup() {
delay(30); delay(30);
#endif #endif
if (!this->write_command_(SCD30_CMD_MEASUREMENT_INTERVAL, update_interval_)) { if (!this->write_command(SCD30_CMD_MEASUREMENT_INTERVAL, update_interval_)) {
ESP_LOGE(TAG, "Sensor SCD30 error setting update interval."); ESP_LOGE(TAG, "Sensor SCD30 error setting update interval.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -81,7 +75,7 @@ void SCD30Component::setup() {
// The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on // The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on
if (this->altitude_compensation_ != 0xFFFF) { if (this->altitude_compensation_ != 0xFFFF) {
if (!this->write_command_(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { if (!this->write_command(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) {
ESP_LOGE(TAG, "Sensor SCD30 error setting altitude compensation."); ESP_LOGE(TAG, "Sensor SCD30 error setting altitude compensation.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -92,7 +86,7 @@ void SCD30Component::setup() {
delay(30); delay(30);
#endif #endif
if (!this->write_command_(SCD30_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { if (!this->write_command(SCD30_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) {
ESP_LOGE(TAG, "Sensor SCD30 error setting automatic self calibration."); ESP_LOGE(TAG, "Sensor SCD30 error setting automatic self calibration.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -103,7 +97,7 @@ void SCD30Component::setup() {
#endif #endif
/// Sensor initialization /// Sensor initialization
if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) { if (!this->write_command(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) {
ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -151,14 +145,14 @@ void SCD30Component::dump_config() {
} }
void SCD30Component::update() { void SCD30Component::update() {
uint16_t raw_read_status[1]; uint16_t raw_read_status;
if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
this->status_set_warning(); this->status_set_warning();
ESP_LOGW(TAG, "Data not ready yet!"); ESP_LOGW(TAG, "Data not ready yet!");
return; return;
} }
if (!this->write_command_(SCD30_CMD_READ_MEASUREMENT)) { if (!this->write_command(SCD30_CMD_READ_MEASUREMENT)) {
ESP_LOGW(TAG, "Error reading measurement!"); ESP_LOGW(TAG, "Error reading measurement!");
this->status_set_warning(); this->status_set_warning();
return; return;
@ -166,7 +160,7 @@ void SCD30Component::update() {
this->set_timeout(50, [this]() { this->set_timeout(50, [this]() {
uint16_t raw_data[6]; uint16_t raw_data[6];
if (!this->read_data_(raw_data, 6)) { if (!this->read_data(raw_data, 6)) {
this->status_set_warning(); this->status_set_warning();
return; return;
} }
@ -197,77 +191,16 @@ void SCD30Component::update() {
} }
bool SCD30Component::is_data_ready_() { bool SCD30Component::is_data_ready_() {
if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) { if (!this->write_command(SCD30_CMD_GET_DATA_READY_STATUS)) {
return false; return false;
} }
delay(4); delay(4);
uint16_t is_data_ready; uint16_t is_data_ready;
if (!this->read_data_(&is_data_ready, 1)) { if (!this->read_data(&is_data_ready, 1)) {
return false; return false;
} }
return is_data_ready == 1; return is_data_ready == 1;
} }
bool SCD30Component::write_command_(uint16_t command) {
// Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
return this->write_byte(command >> 8, command & 0xFF);
}
bool SCD30Component::write_command_(uint16_t command, uint16_t data) {
uint8_t raw[5];
raw[0] = command >> 8;
raw[1] = command & 0xFF;
raw[2] = data >> 8;
raw[3] = data & 0xFF;
raw[4] = sht_crc_(raw[2], raw[3]);
return this->write(raw, 5) == i2c::ERROR_OK;
}
uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) {
uint8_t bit;
uint8_t crc = 0xFF;
crc ^= data1;
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x131;
} else {
crc = (crc << 1);
}
}
crc ^= data2;
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x131;
} else {
crc = (crc << 1);
}
}
return crc;
}
bool SCD30Component::read_data_(uint16_t *data, uint8_t len) {
const uint8_t num_bytes = len * 3;
std::vector<uint8_t> buf(num_bytes);
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
return false;
}
for (uint8_t i = 0; i < len; i++) {
const uint8_t j = 3 * i;
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
if (crc != buf[j + 2]) {
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
return false;
}
data[i] = (buf[j] << 8) | buf[j + 1];
}
return true;
}
} // namespace scd30 } // namespace scd30
} // namespace esphome } // namespace esphome

View file

@ -2,13 +2,13 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/sensirion_common/i2c_sensirion.h"
namespace esphome { namespace esphome {
namespace scd30 { namespace scd30 {
/// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors. /// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors.
class SCD30Component : public Component, public i2c::I2CDevice { class SCD30Component : public Component, public sensirion_common::SensirionI2CDevice {
public: public:
void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
@ -27,10 +27,6 @@ class SCD30Component : public Component, public i2c::I2CDevice {
float get_setup_priority() const override { return setup_priority::DATA; } float get_setup_priority() const override { return setup_priority::DATA; }
protected: protected:
bool write_command_(uint16_t command);
bool write_command_(uint16_t command, uint16_t data);
bool read_data_(uint16_t *data, uint8_t len);
uint8_t sht_crc_(uint8_t data1, uint8_t data2);
bool is_data_ready_(); bool is_data_ready_();
enum ErrorCode { enum ErrorCode {

View file

@ -2,6 +2,7 @@ from esphome import core
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import i2c, sensor from esphome.components import i2c, sensor
from esphome.components import sensirion_common
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_HUMIDITY, CONF_HUMIDITY,
@ -18,9 +19,12 @@ from esphome.const import (
) )
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
scd30_ns = cg.esphome_ns.namespace("scd30") scd30_ns = cg.esphome_ns.namespace("scd30")
SCD30Component = scd30_ns.class_("SCD30Component", cg.Component, i2c.I2CDevice) SCD30Component = scd30_ns.class_(
"SCD30Component", cg.Component, sensirion_common.SensirionI2CDevice
)
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_ALTITUDE_COMPENSATION = "altitude_compensation"

View file

@ -25,15 +25,8 @@ void SCD4XComponent::setup() {
// the sensor needs 1000 ms to enter the idle state // the sensor needs 1000 ms to enter the idle state
this->set_timeout(1000, [this]() { this->set_timeout(1000, [this]() {
// Check if measurement is ready before reading the value uint16_t raw_read_status;
if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_STATUS)) { if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) {
ESP_LOGE(TAG, "Failed to write data ready status command");
this->mark_failed();
return;
}
uint16_t raw_read_status[1];
if (!this->read_data_(raw_read_status, 1)) {
ESP_LOGE(TAG, "Failed to read data ready status"); ESP_LOGE(TAG, "Failed to read data ready status");
this->mark_failed(); this->mark_failed();
return; return;
@ -41,9 +34,9 @@ void SCD4XComponent::setup() {
uint32_t stop_measurement_delay = 0; uint32_t stop_measurement_delay = 0;
// In order to query the device periodic measurement must be ceased // In order to query the device periodic measurement must be ceased
if (raw_read_status[0]) { if (raw_read_status) {
ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement");
if (!this->write_command_(SCD4X_CMD_STOP_MEASUREMENTS)) { if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
ESP_LOGE(TAG, "Failed to stop measurements"); ESP_LOGE(TAG, "Failed to stop measurements");
this->mark_failed(); this->mark_failed();
return; return;
@ -53,15 +46,8 @@ void SCD4XComponent::setup() {
stop_measurement_delay = 500; stop_measurement_delay = 500;
} }
this->set_timeout(stop_measurement_delay, [this]() { this->set_timeout(stop_measurement_delay, [this]() {
if (!this->write_command_(SCD4X_CMD_GET_SERIAL_NUMBER)) {
ESP_LOGE(TAG, "Failed to write get serial command");
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
uint16_t raw_serial_number[3]; uint16_t raw_serial_number[3];
if (!this->read_data_(raw_serial_number, 3)) { if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) {
ESP_LOGE(TAG, "Failed to read serial number"); ESP_LOGE(TAG, "Failed to read serial number");
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
this->mark_failed(); this->mark_failed();
@ -70,8 +56,8 @@ void SCD4XComponent::setup() {
ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8), ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8),
uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8)); uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8));
if (!this->write_command_(SCD4X_CMD_TEMPERATURE_OFFSET, if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET,
(uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) {
ESP_LOGE(TAG, "Error setting temperature offset."); ESP_LOGE(TAG, "Error setting temperature offset.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -88,7 +74,7 @@ void SCD4XComponent::setup() {
return; return;
} }
} else { } else {
if (!this->write_command_(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) {
ESP_LOGE(TAG, "Error setting altitude compensation."); ESP_LOGE(TAG, "Error setting altitude compensation.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -96,7 +82,7 @@ void SCD4XComponent::setup() {
} }
} }
if (!this->write_command_(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) {
ESP_LOGE(TAG, "Error setting automatic self calibration."); ESP_LOGE(TAG, "Error setting automatic self calibration.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -104,7 +90,7 @@ void SCD4XComponent::setup() {
} }
// Finally start sensor measurements // Finally start sensor measurements
if (!this->write_command_(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { if (!this->write_command(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) {
ESP_LOGE(TAG, "Error starting continuous measurements."); ESP_LOGE(TAG, "Error starting continuous measurements.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -164,19 +150,19 @@ void SCD4XComponent::update() {
} }
// Check if data is ready // Check if data is ready
if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_STATUS)) { if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) {
this->status_set_warning(); this->status_set_warning();
return; return;
} }
uint16_t raw_read_status[1]; uint16_t raw_read_status;
if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
this->status_set_warning(); this->status_set_warning();
ESP_LOGW(TAG, "Data not ready yet!"); ESP_LOGW(TAG, "Data not ready yet!");
return; return;
} }
if (!this->write_command_(SCD4X_CMD_READ_MEASUREMENT)) { if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
ESP_LOGW(TAG, "Error reading measurement!"); ESP_LOGW(TAG, "Error reading measurement!");
this->status_set_warning(); this->status_set_warning();
return; return;
@ -184,7 +170,7 @@ void SCD4XComponent::update() {
// Read off sensor data // Read off sensor data
uint16_t raw_data[3]; uint16_t raw_data[3];
if (!this->read_data_(raw_data, 3)) { if (!this->read_data(raw_data, 3)) {
this->status_set_warning(); this->status_set_warning();
return; return;
} }
@ -218,7 +204,7 @@ void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) {
} }
bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) { bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) {
if (this->write_command_(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) { if (this->write_command(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) {
ESP_LOGD(TAG, "setting ambient pressure compensation to %d hPa", pressure_in_hpa); ESP_LOGD(TAG, "setting ambient pressure compensation to %d hPa", pressure_in_hpa);
return true; return true;
} else { } else {
@ -227,70 +213,5 @@ bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_
} }
} }
uint8_t SCD4XComponent::sht_crc_(uint8_t data1, uint8_t data2) {
uint8_t bit;
uint8_t crc = 0xFF;
crc ^= data1;
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x131;
} else {
crc = (crc << 1);
}
}
crc ^= data2;
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x131;
} else {
crc = (crc << 1);
}
}
return crc;
}
bool SCD4XComponent::read_data_(uint16_t *data, uint8_t len) {
const uint8_t num_bytes = len * 3;
std::vector<uint8_t> buf(num_bytes);
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
return false;
}
for (uint8_t i = 0; i < len; i++) {
const uint8_t j = 3 * i;
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
if (crc != buf[j + 2]) {
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
return false;
}
data[i] = (buf[j] << 8) | buf[j + 1];
}
return true;
}
bool SCD4XComponent::write_command_(uint16_t command) {
const uint8_t num_bytes = 2;
uint8_t buffer[num_bytes];
buffer[0] = (command >> 8);
buffer[1] = command & 0xff;
return this->write(buffer, num_bytes) == i2c::ERROR_OK;
}
bool SCD4XComponent::write_command_(uint16_t command, uint16_t data) {
uint8_t raw[5];
raw[0] = command >> 8;
raw[1] = command & 0xFF;
raw[2] = data >> 8;
raw[3] = data & 0xFF;
raw[4] = sht_crc_(raw[2], raw[3]);
return this->write(raw, 5) == i2c::ERROR_OK;
}
} // namespace scd4x } // namespace scd4x
} // namespace esphome } // namespace esphome

View file

@ -2,14 +2,14 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/sensirion_common/i2c_sensirion.h"
namespace esphome { namespace esphome {
namespace scd4x { namespace scd4x {
enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN };
class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
public: public:
float get_setup_priority() const override { return setup_priority::DATA; } float get_setup_priority() const override { return setup_priority::DATA; }
void setup() override; void setup() override;
@ -27,10 +27,6 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice {
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
protected: protected:
uint8_t sht_crc_(uint8_t data1, uint8_t data2);
bool read_data_(uint16_t *data, uint8_t len);
bool write_command_(uint16_t command);
bool write_command_(uint16_t command, uint16_t data);
bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa);
ERRORCODE error_code_; ERRORCODE error_code_;

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import i2c, sensor from esphome.components import i2c, sensor
from esphome.components import sensirion_common
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_CO2, CONF_CO2,
@ -21,9 +21,12 @@ from esphome.const import (
CODEOWNERS = ["@sjtrny"] CODEOWNERS = ["@sjtrny"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
scd4x_ns = cg.esphome_ns.namespace("scd4x") scd4x_ns = cg.esphome_ns.namespace("scd4x")
SCD4XComponent = scd4x_ns.class_("SCD4XComponent", cg.PollingComponent, i2c.I2CDevice) SCD4XComponent = scd4x_ns.class_(
"SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
)
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_ALTITUDE_COMPENSATION = "altitude_compensation"

View file

@ -7,55 +7,50 @@ namespace esphome {
namespace sdp3x { namespace sdp3x {
static const char *const TAG = "sdp3x.sensor"; static const char *const TAG = "sdp3x.sensor";
static const uint8_t SDP3X_SOFT_RESET[2] = {0x00, 0x06}; static const uint16_t SDP3X_SOFT_RESET = 0x0006;
static const uint8_t SDP3X_READ_ID1[2] = {0x36, 0x7C}; static const uint16_t SDP3X_READ_ID1 = 0x367C;
static const uint8_t SDP3X_READ_ID2[2] = {0xE1, 0x02}; static const uint16_t SDP3X_READ_ID2 = 0xE102;
static const uint8_t SDP3X_START_DP_AVG[2] = {0x36, 0x15}; static const uint16_t SDP3X_START_DP_AVG = 0x3615;
static const uint8_t SDP3X_START_MASS_FLOW_AVG[2] = {0x36, 0x03}; static const uint16_t SDP3X_START_MASS_FLOW_AVG = 0x3603;
static const uint8_t SDP3X_STOP_MEAS[2] = {0x3F, 0xF9}; static const uint16_t SDP3X_STOP_MEAS = 0x3FF9;
void SDP3XComponent::update() { this->read_pressure_(); } void SDP3XComponent::update() { this->read_pressure_(); }
void SDP3XComponent::setup() { void SDP3XComponent::setup() {
ESP_LOGD(TAG, "Setting up SDP3X..."); ESP_LOGD(TAG, "Setting up SDP3X...");
if (this->write(SDP3X_STOP_MEAS, 2) != i2c::ERROR_OK) { if (!this->write_command(SDP3X_STOP_MEAS)) {
ESP_LOGW(TAG, "Stop SDP3X failed!"); // This sometimes fails for no good reason ESP_LOGW(TAG, "Stop SDP3X failed!"); // This sometimes fails for no good reason
} }
if (this->write(SDP3X_SOFT_RESET, 2) != i2c::ERROR_OK) { if (!this->write_command(SDP3X_SOFT_RESET)) {
ESP_LOGW(TAG, "Soft Reset SDP3X failed!"); // This sometimes fails for no good reason ESP_LOGW(TAG, "Soft Reset SDP3X failed!"); // This sometimes fails for no good reason
} }
this->set_timeout(20, [this] { this->set_timeout(20, [this] {
if (this->write(SDP3X_READ_ID1, 2) != i2c::ERROR_OK) { if (!this->write_command(SDP3X_READ_ID1)) {
ESP_LOGE(TAG, "Read ID1 SDP3X failed!"); ESP_LOGE(TAG, "Read ID1 SDP3X failed!");
this->mark_failed(); this->mark_failed();
return; return;
} }
if (this->write(SDP3X_READ_ID2, 2) != i2c::ERROR_OK) { if (!this->write_command(SDP3X_READ_ID2)) {
ESP_LOGE(TAG, "Read ID2 SDP3X failed!"); ESP_LOGE(TAG, "Read ID2 SDP3X failed!");
this->mark_failed(); this->mark_failed();
return; return;
} }
uint8_t data[18]; uint16_t data[6];
if (this->read(data, 18) != i2c::ERROR_OK) { if (this->read_data(data, 6) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read ID SDP3X failed!"); ESP_LOGE(TAG, "Read ID SDP3X failed!");
this->mark_failed(); this->mark_failed();
return; return;
} }
if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]))) {
ESP_LOGE(TAG, "CRC ID SDP3X failed!");
this->mark_failed();
return;
}
// SDP8xx // SDP8xx
// ref: // ref:
// https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/8_Differential_Pressure/Datasheets/Sensirion_Differential_Pressure_Datasheet_SDP8xx_Digital.pdf // https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/8_Differential_Pressure/Datasheets/Sensirion_Differential_Pressure_Datasheet_SDP8xx_Digital.pdf
if (data[2] == 0x02) { if (data[1] >> 8 == 0x02) {
switch (data[3]) { switch (data[1] & 0xFF) {
case 0x01: // SDP800-500Pa case 0x01: // SDP800-500Pa
ESP_LOGCONFIG(TAG, "Sensor is SDP800-500Pa"); ESP_LOGCONFIG(TAG, "Sensor is SDP800-500Pa");
break; break;
@ -75,15 +70,16 @@ void SDP3XComponent::setup() {
ESP_LOGCONFIG(TAG, "Sensor is SDP810-125Pa"); ESP_LOGCONFIG(TAG, "Sensor is SDP810-125Pa");
break; break;
} }
} else if (data[2] == 0x01) { } else if (data[1] >> 8 == 0x01) {
if (data[3] == 0x01) { if ((data[1] & 0xFF) == 0x01) {
ESP_LOGCONFIG(TAG, "Sensor is SDP31-500Pa"); ESP_LOGCONFIG(TAG, "Sensor is SDP31-500Pa");
} else if (data[3] == 0x02) { } else if ((data[1] & 0xFF) == 0x02) {
ESP_LOGCONFIG(TAG, "Sensor is SDP32-125Pa"); ESP_LOGCONFIG(TAG, "Sensor is SDP32-125Pa");
} }
} }
if (this->write(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG, 2) != i2c::ERROR_OK) { if (this->write_command(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG) !=
i2c::ERROR_OK) {
ESP_LOGE(TAG, "Start Measurements SDP3X failed!"); ESP_LOGE(TAG, "Start Measurements SDP3X failed!");
this->mark_failed(); this->mark_failed();
return; return;
@ -101,22 +97,16 @@ void SDP3XComponent::dump_config() {
} }
void SDP3XComponent::read_pressure_() { void SDP3XComponent::read_pressure_() {
uint8_t data[9]; uint16_t data[3];
if (this->read(data, 9) != i2c::ERROR_OK) { if (this->read_data(data, 3) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Couldn't read SDP3X data!"); ESP_LOGW(TAG, "Couldn't read SDP3X data!");
this->status_set_warning(); this->status_set_warning();
return; return;
} }
if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]) && check_crc_(&data[6], 2, data[8]))) { int16_t pressure_raw = data[0];
ESP_LOGW(TAG, "Invalid SDP3X data!"); int16_t temperature_raw = data[1];
this->status_set_warning(); int16_t scale_factor_raw = data[2];
return;
}
int16_t pressure_raw = encode_uint16(data[0], data[1]);
int16_t temperature_raw = encode_uint16(data[3], data[4]);
int16_t scale_factor_raw = encode_uint16(data[6], data[7]);
// scale factor is in Pa - convert to hPa // scale factor is in Pa - convert to hPa
float pressure = pressure_raw / (scale_factor_raw * 100.0f); float pressure = pressure_raw / (scale_factor_raw * 100.0f);
ESP_LOGV(TAG, "Got raw pressure=%d, raw scale factor =%d, raw temperature=%d ", pressure_raw, scale_factor_raw, ESP_LOGV(TAG, "Got raw pressure=%d, raw scale factor =%d, raw temperature=%d ", pressure_raw, scale_factor_raw,
@ -129,26 +119,5 @@ void SDP3XComponent::read_pressure_() {
float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; } float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; }
// Check CRC function from SDP3X sample code provided by sensirion
// Returns true if a checksum is OK
bool SDP3XComponent::check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum) {
uint8_t crc = 0xFF;
// calculates 8-Bit checksum with given polynomial 0x31 (x^8 + x^5 + x^4 + 1)
for (int i = 0; i < size; i++) {
crc ^= (data[i]);
for (uint8_t bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x31;
} else {
crc = (crc << 1);
}
}
}
// verify checksum
return (crc == checksum);
}
} // namespace sdp3x } // namespace sdp3x
} // namespace esphome } // namespace esphome

View file

@ -2,14 +2,14 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/sensirion_common/i2c_sensirion.h"
namespace esphome { namespace esphome {
namespace sdp3x { namespace sdp3x {
enum MeasurementMode { MASS_FLOW_AVG, DP_AVG }; enum MeasurementMode { MASS_FLOW_AVG, DP_AVG };
class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { class SDP3XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice, public sensor::Sensor {
public: public:
/// Schedule temperature+pressure readings. /// Schedule temperature+pressure readings.
void update() override; void update() override;
@ -23,8 +23,6 @@ class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public se
protected: protected:
/// Internal method to read the pressure from the component after it has been scheduled. /// Internal method to read the pressure from the component after it has been scheduled.
void read_pressure_(); void read_pressure_();
bool check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum);
MeasurementMode measurement_mode_; MeasurementMode measurement_mode_;
}; };

View file

@ -1,6 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import i2c, sensor from esphome.components import i2c, sensor
from esphome.components import sensirion_common
from esphome.const import ( from esphome.const import (
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
@ -8,10 +9,13 @@ from esphome.const import (
) )
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
CODEOWNERS = ["@Azimath"] CODEOWNERS = ["@Azimath"]
sdp3x_ns = cg.esphome_ns.namespace("sdp3x") sdp3x_ns = cg.esphome_ns.namespace("sdp3x")
SDP3XComponent = sdp3x_ns.class_("SDP3XComponent", cg.PollingComponent, i2c.I2CDevice) SDP3XComponent = sdp3x_ns.class_(
"SDP3XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
)
MeasurementMode = sdp3x_ns.enum("MeasurementMode") MeasurementMode = sdp3x_ns.enum("MeasurementMode")

View file

@ -0,0 +1,10 @@
import esphome.codegen as cg
from esphome.components import i2c
CODEOWNERS = ["@martgras"]
sensirion_common_ns = cg.esphome_ns.namespace("sensirion_common")
SensirionI2CDevice = sensirion_common_ns.class_("SensirionI2CDevice", i2c.I2CDevice)

View file

@ -0,0 +1,128 @@
#include "i2c_sensirion.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace sensirion_common {
static const char *const TAG = "sensirion_i2c";
// To avoid memory allocations for small writes a stack buffer is used
static const size_t BUFFER_STACK_SIZE = 16;
bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) {
const uint8_t num_bytes = len * 3;
std::vector<uint8_t> buf(num_bytes);
last_error_ = this->read(buf.data(), num_bytes);
if (last_error_ != i2c::ERROR_OK) {
return false;
}
for (uint8_t i = 0; i < len; i++) {
const uint8_t j = 3 * i;
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
if (crc != buf[j + 2]) {
ESP_LOGE(TAG, "CRC8 Checksum invalid at pos %d! 0x%02X != 0x%02X", i, buf[j + 2], crc);
last_error_ = i2c::ERROR_CRC;
return false;
}
data[i] = encode_uint16(buf[j], buf[j + 1]);
}
return true;
}
/***
* write command with parameters and insert crc
* use stack array for less than 4 paramaters. Most sensirion i2c commands have less parameters
*/
bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data,
uint8_t data_len) {
uint8_t temp_stack[BUFFER_STACK_SIZE];
std::unique_ptr<uint8_t[]> temp_heap;
uint8_t *temp;
size_t required_buffer_len = data_len * 3 + 2;
// Is a dynamic allocation required ?
if (required_buffer_len >= BUFFER_STACK_SIZE) {
temp_heap = std::unique_ptr<uint8_t[]>(new uint8_t[required_buffer_len]);
temp = temp_heap.get();
} else {
temp = temp_stack;
}
// First byte or word is the command
uint8_t raw_idx = 0;
if (command_len == 1) {
temp[raw_idx++] = command & 0xFF;
} else {
// command is 2 bytes
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
temp[raw_idx++] = command >> 8;
temp[raw_idx++] = command & 0xFF;
#else
temp[raw_idx++] = command & 0xFF;
temp[raw_idx++] = command >> 8;
#endif
}
// add parameters folllowed by crc
// skipped if len == 0
for (size_t i = 0; i < data_len; i++) {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
temp[raw_idx++] = data[i] >> 8;
temp[raw_idx++] = data[i] & 0xFF;
#else
temp[raw_idx++] = data[i] & 0xFF;
temp[raw_idx++] = data[i] >> 8;
#endif
temp[raw_idx++] = sht_crc_(data[i]);
}
last_error_ = this->write(temp, raw_idx);
return last_error_ == i2c::ERROR_OK;
}
bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len,
uint8_t delay_ms) {
if (!this->write_command_(reg, command_len, nullptr, 0)) {
ESP_LOGE(TAG, "Failed to write i2c register=0x%X (%d) err=%d,", reg, command_len, this->last_error_);
return false;
}
delay(delay_ms);
bool result = this->read_data(data, len);
if (!result) {
ESP_LOGE(TAG, "Failed to read data from register=0x%X err=%d,", reg, this->last_error_);
}
return result;
}
// The 8-bit CRC checksum is transmitted after each data word
uint8_t SensirionI2CDevice::sht_crc_(uint16_t data) {
uint8_t bit;
uint8_t crc = 0xFF;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
crc ^= data >> 8;
#else
crc ^= data & 0xFF;
#endif
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ crc_polynomial_;
} else {
crc = (crc << 1);
}
}
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
crc ^= data & 0xFF;
#else
crc ^= data >> 8;
#endif
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ crc_polynomial_;
} else {
crc = (crc << 1);
}
}
return crc;
}
} // namespace sensirion_common
} // namespace esphome

View file

@ -0,0 +1,155 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace sensirion_common {
/**
* Implementation of a i2c functions for Sensirion sensors
* Sensirion data requires crc checking.
* Each 16 bit word is/must be followed 8 bit CRC code
* (Applies to read and write - note the i2c command code doesn't need a CRC)
* Format:
* | 16 Bit Command Code | 16 bit Data word 1 | CRC of DW 1 | 16 bit Data word 1 | CRC of DW 2 | ..
*/
class SensirionI2CDevice : public i2c::I2CDevice {
public:
enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 };
/** Read data words from i2c device.
* handles crc check used by Sensirion sensors
* @param data pointer to raw result
* @param len number of words to read
* @return true if reading succeded
*/
bool read_data(uint16_t *data, uint8_t len);
/** Read 1 data word from i2c device.
* @param data reference to raw result
* @return true if reading succeded
*/
bool read_data(uint16_t &data) { return this->read_data(&data, 1); }
/** get data words from i2c register.
* handles crc check used by Sensirion sensors
* @param i2c register
* @param data pointer to raw result
* @param len number of words to read
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
* @return true if reading succeded
*/
bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay = 0) {
return get_register_(command, ADDR_16_BIT, data, len, delay);
}
/** Read 1 data word from 16 bit i2c register.
* @param i2c register
* @param data reference to raw result
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
* @return true if reading succeded
*/
bool get_register(uint16_t i2c_register, uint16_t &data, uint8_t delay = 0) {
return this->get_register_(i2c_register, ADDR_16_BIT, &data, 1, delay);
}
/** get data words from i2c register.
* handles crc check used by Sensirion sensors
* @param i2c register
* @param data pointer to raw result
* @param len number of words to read
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
* @return true if reading succeded
*/
bool get_8bit_register(uint8_t i2c_register, uint16_t *data, uint8_t len, uint8_t delay = 0) {
return get_register_(i2c_register, ADDR_8_BIT, data, len, delay);
}
/** Read 1 data word from 8 bit i2c register.
* @param i2c register
* @param data reference to raw result
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
* @return true if reading succeded
*/
bool get_8bit_register(uint8_t i2c_register, uint16_t &data, uint8_t delay = 0) {
return this->get_register_(i2c_register, ADDR_8_BIT, &data, 1, delay);
}
/** Write a command to the i2c device.
* @param command i2c command to send
* @return true if reading succeded
*/
template<class T> bool write_command(T i2c_register) { return write_command(i2c_register, nullptr, 0); }
/** Write a command and one data word to the i2c device .
* @param command i2c command to send
* @param data argument for the i2c command
* @return true if reading succeded
*/
template<class T> bool write_command(T i2c_register, uint16_t data) { return write_command(i2c_register, &data, 1); }
/** Write a command with arguments as words
* @param i2c_register i2c command to send - an be uint8_t or uint16_t
* @param data vector<uint16> arguments for the i2c command
* @return true if reading succeded
*/
template<class T> bool write_command(T i2c_register, const std::vector<uint16_t> &data) {
return write_command_(i2c_register, sizeof(T), data.data(), data.size());
}
/** Write a command with arguments as words
* @param i2c_register i2c command to send - an be uint8_t or uint16_t
* @param data arguments for the i2c command
* @param len number of arguments (words)
* @return true if reading succeded
*/
template<class T> bool write_command(T i2c_register, const uint16_t *data, uint8_t len) {
// limit to 8 or 16 bit only
static_assert(sizeof(i2c_register) == 1 || sizeof(i2c_register) == 2,
"only 8 or 16 bit command types are supported.");
return write_command_(i2c_register, CommandLen(sizeof(T)), data, len);
}
protected:
uint8_t crc_polynomial_{0x31u}; // default for sensirion
/** Write a command with arguments as words
* @param command i2c command to send can be uint8_t or uint16_t
* @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes
* @param data arguments for the i2c command
* @param data_len number of arguments (words)
* @return true if reading succeded
*/
bool write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, uint8_t data_len);
/** get data words from i2c register.
* handles crc check used by Sensirion sensors
* @param i2c register
* @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes
* @param data pointer to raw result
* @param len number of words to read
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
* @return true if reading succeded
*/
bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay);
/** 8-bit CRC checksum that is transmitted after each data word for read and write operation
* @param command i2c command to send
* @param data data word for which the crc8 checksum is calculated
* @param len number of arguments (words)
* @return 8 Bit CRC
*/
uint8_t sht_crc_(uint16_t data);
/** 8-bit CRC checksum that is transmitted after each data word for read and write operation
* @param command i2c command to send
* @param data1 high byte of data word
* @param data2 low byte of data word
* @return 8 Bit CRC
*/
uint8_t sht_crc_(uint8_t data1, uint8_t data2) { return sht_crc_(encode_uint16(data1, data2)); }
/** last error code from i2c operation
*/
i2c::ErrorCode last_error_;
};
} // namespace sensirion_common
} // namespace esphome

View file

@ -212,8 +212,8 @@ SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger),
cv.Optional(CONF_ABOVE): cv.float_, cv.Optional(CONF_ABOVE): cv.templatable(cv.float_),
cv.Optional(CONF_BELOW): cv.float_, cv.Optional(CONF_BELOW): cv.templatable(cv.float_),
}, },
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
), ),

View file

@ -1,10 +1,13 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import i2c, sensor from esphome.components import i2c, sensor, sensirion_common
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_BASELINE, CONF_BASELINE,
CONF_ECO2, CONF_ECO2,
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
CONF_TVOC, CONF_TVOC,
ICON_RADIATOR, ICON_RADIATOR,
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
@ -17,17 +20,19 @@ from esphome.const import (
) )
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
sgp30_ns = cg.esphome_ns.namespace("sgp30") sgp30_ns = cg.esphome_ns.namespace("sgp30")
SGP30Component = sgp30_ns.class_("SGP30Component", cg.PollingComponent, i2c.I2CDevice) SGP30Component = sgp30_ns.class_(
"SGP30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice
)
CONF_ECO2_BASELINE = "eco2_baseline" CONF_ECO2_BASELINE = "eco2_baseline"
CONF_TVOC_BASELINE = "tvoc_baseline" CONF_TVOC_BASELINE = "tvoc_baseline"
CONF_STORE_BASELINE = "store_baseline"
CONF_UPTIME = "uptime" CONF_UPTIME = "uptime"
CONF_COMPENSATION = "compensation" CONF_COMPENSATION = "compensation"
CONF_HUMIDITY_SOURCE = "humidity_source" CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_TEMPERATURE_SOURCE = "temperature_source"
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.Schema( cv.Schema(

View file

@ -36,14 +36,8 @@ void SGP30Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up SGP30..."); ESP_LOGCONFIG(TAG, "Setting up SGP30...");
// Serial Number identification // Serial Number identification
if (!this->write_command_(SGP30_CMD_GET_SERIAL_ID)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
uint16_t raw_serial_number[3]; uint16_t raw_serial_number[3];
if (!this->get_register(SGP30_CMD_GET_SERIAL_ID, raw_serial_number, 3)) {
if (!this->read_data_(raw_serial_number, 3)) {
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -52,16 +46,12 @@ void SGP30Component::setup() {
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
// Featureset identification for future use // Featureset identification for future use
if (!this->write_command_(SGP30_CMD_GET_FEATURESET)) { uint16_t raw_featureset;
if (!this->get_register(SGP30_CMD_GET_FEATURESET, raw_featureset)) {
this->mark_failed(); this->mark_failed();
return; return;
} }
uint16_t raw_featureset[1]; this->featureset_ = raw_featureset;
if (!this->read_data_(raw_featureset, 1)) {
this->mark_failed();
return;
}
this->featureset_ = raw_featureset[0];
if (uint16_t(this->featureset_ >> 12) != 0x0) { if (uint16_t(this->featureset_ >> 12) != 0x0) {
if (uint16_t(this->featureset_ >> 12) == 0x1) { if (uint16_t(this->featureset_ >> 12) == 0x1) {
// ID matching a different sensor: SGPC3 // ID matching a different sensor: SGPC3
@ -76,7 +66,7 @@ void SGP30Component::setup() {
ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
// Sensor initialization // Sensor initialization
if (!this->write_command_(SGP30_CMD_IAQ_INIT)) { if (!this->write_command(SGP30_CMD_IAQ_INIT)) {
ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed."); ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed.");
this->error_code_ = MEASUREMENT_INIT_FAILED; this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed(); this->mark_failed();
@ -119,14 +109,14 @@ bool SGP30Component::is_sensor_baseline_reliable_() {
void SGP30Component::read_iaq_baseline_() { void SGP30Component::read_iaq_baseline_() {
if (this->is_sensor_baseline_reliable_()) { if (this->is_sensor_baseline_reliable_()) {
if (!this->write_command_(SGP30_CMD_GET_IAQ_BASELINE)) { if (!this->write_command(SGP30_CMD_GET_IAQ_BASELINE)) {
ESP_LOGD(TAG, "Error getting baseline"); ESP_LOGD(TAG, "Error getting baseline");
this->status_set_warning(); this->status_set_warning();
return; return;
} }
this->set_timeout(50, [this]() { this->set_timeout(50, [this]() {
uint16_t raw_data[2]; uint16_t raw_data[2];
if (!this->read_data_(raw_data, 2)) { if (!this->read_data(raw_data, 2)) {
this->status_set_warning(); this->status_set_warning();
return; return;
} }
@ -274,14 +264,14 @@ void SGP30Component::dump_config() {
} }
void SGP30Component::update() { void SGP30Component::update() {
if (!this->write_command_(SGP30_CMD_MEASURE_IAQ)) { if (!this->write_command(SGP30_CMD_MEASURE_IAQ)) {
this->status_set_warning(); this->status_set_warning();
return; return;
} }
this->seconds_since_last_store_ += this->update_interval_ / 1000; this->seconds_since_last_store_ += this->update_interval_ / 1000;
this->set_timeout(50, [this]() { this->set_timeout(50, [this]() {
uint16_t raw_data[2]; uint16_t raw_data[2];
if (!this->read_data_(raw_data, 2)) { if (!this->read_data(raw_data, 2)) {
this->status_set_warning(); this->status_set_warning();
return; return;
} }
@ -305,56 +295,5 @@ void SGP30Component::update() {
}); });
} }
bool SGP30Component::write_command_(uint16_t command) {
// Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
return this->write_byte(command >> 8, command & 0xFF);
}
uint8_t SGP30Component::sht_crc_(uint8_t data1, uint8_t data2) {
uint8_t bit;
uint8_t crc = 0xFF;
crc ^= data1;
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x131;
} else {
crc = (crc << 1);
}
}
crc ^= data2;
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x131;
} else {
crc = (crc << 1);
}
}
return crc;
}
bool SGP30Component::read_data_(uint16_t *data, uint8_t len) {
const uint8_t num_bytes = len * 3;
std::vector<uint8_t> buf(num_bytes);
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
return false;
}
for (uint8_t i = 0; i < len; i++) {
const uint8_t j = 3 * i;
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
if (crc != buf[j + 2]) {
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
return false;
}
data[i] = (buf[j] << 8) | buf[j + 1];
}
return true;
}
} // namespace sgp30 } // namespace sgp30
} // namespace esphome } // namespace esphome

View file

@ -2,7 +2,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/sensirion_common/i2c_sensirion.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include <cmath> #include <cmath>
@ -15,7 +15,7 @@ struct SGP30Baselines {
} PACKED; } PACKED;
/// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors. /// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors.
class SGP30Component : public PollingComponent, public i2c::I2CDevice { class SGP30Component : public PollingComponent, public sensirion_common::SensirionI2CDevice {
public: public:
void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; }
void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
@ -33,13 +33,10 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice {
float get_setup_priority() const override { return setup_priority::DATA; } float get_setup_priority() const override { return setup_priority::DATA; }
protected: protected:
bool write_command_(uint16_t command);
bool read_data_(uint16_t *data, uint8_t len);
void send_env_data_(); void send_env_data_();
void read_iaq_baseline_(); void read_iaq_baseline_();
bool is_sensor_baseline_reliable_(); bool is_sensor_baseline_reliable_();
void write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline); void write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline);
uint8_t sht_crc_(uint8_t data1, uint8_t data2);
uint64_t serial_number_; uint64_t serial_number_;
uint16_t featureset_; uint16_t featureset_;
uint32_t required_warm_up_time_; uint32_t required_warm_up_time_;

View file

@ -1,25 +1,30 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import i2c, sensor from esphome.components import i2c, sensor, sensirion_common
from esphome.const import ( from esphome.const import (
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
ICON_RADIATOR, ICON_RADIATOR,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
) )
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
CODEOWNERS = ["@SenexCrenshaw"] CODEOWNERS = ["@SenexCrenshaw"]
sgp40_ns = cg.esphome_ns.namespace("sgp40") sgp40_ns = cg.esphome_ns.namespace("sgp40")
SGP40Component = sgp40_ns.class_( SGP40Component = sgp40_ns.class_(
"SGP40Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice "SGP40Component",
sensor.Sensor,
cg.PollingComponent,
sensirion_common.SensirionI2CDevice,
) )
CONF_COMPENSATION = "compensation" CONF_COMPENSATION = "compensation"
CONF_HUMIDITY_SOURCE = "humidity_source" CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_TEMPERATURE_SOURCE = "temperature_source"
CONF_STORE_BASELINE = "store_baseline"
CONF_VOC_BASELINE = "voc_baseline" CONF_VOC_BASELINE = "voc_baseline"
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (

View file

@ -12,14 +12,14 @@ void SGP40Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up SGP40..."); ESP_LOGCONFIG(TAG, "Setting up SGP40...");
// Serial Number identification // Serial Number identification
if (!this->write_command_(SGP40_CMD_GET_SERIAL_ID)) { if (!this->write_command(SGP40_CMD_GET_SERIAL_ID)) {
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(); this->mark_failed();
return; return;
} }
uint16_t raw_serial_number[3]; uint16_t raw_serial_number[3];
if (!this->read_data_(raw_serial_number, 3)) { if (!this->read_data(raw_serial_number, 3)) {
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -28,19 +28,19 @@ void SGP40Component::setup() {
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
// Featureset identification for future use // Featureset identification for future use
if (!this->write_command_(SGP40_CMD_GET_FEATURESET)) { if (!this->write_command(SGP40_CMD_GET_FEATURESET)) {
ESP_LOGD(TAG, "raw_featureset write_command_ failed"); ESP_LOGD(TAG, "raw_featureset write_command_ failed");
this->mark_failed(); this->mark_failed();
return; return;
} }
uint16_t raw_featureset[1]; uint16_t raw_featureset;
if (!this->read_data_(raw_featureset, 1)) { if (!this->read_data(raw_featureset)) {
ESP_LOGD(TAG, "raw_featureset read_data_ failed"); ESP_LOGD(TAG, "raw_featureset read_data_ failed");
this->mark_failed(); this->mark_failed();
return; return;
} }
this->featureset_ = raw_featureset[0]; this->featureset_ = raw_featureset;
if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) { if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) {
ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF), ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
SGP40_FEATURESET); SGP40_FEATURESET);
@ -95,21 +95,21 @@ void SGP40Component::setup() {
void SGP40Component::self_test_() { void SGP40Component::self_test_() {
ESP_LOGD(TAG, "Self-test started"); ESP_LOGD(TAG, "Self-test started");
if (!this->write_command_(SGP40_CMD_SELF_TEST)) { if (!this->write_command(SGP40_CMD_SELF_TEST)) {
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
ESP_LOGD(TAG, "Self-test communication failed"); ESP_LOGD(TAG, "Self-test communication failed");
this->mark_failed(); this->mark_failed();
} }
this->set_timeout(250, [this]() { this->set_timeout(250, [this]() {
uint16_t reply[1]; uint16_t reply;
if (!this->read_data_(reply, 1)) { if (!this->read_data(reply)) {
ESP_LOGD(TAG, "Self-test read_data_ failed"); ESP_LOGD(TAG, "Self-test read_data_ failed");
this->mark_failed(); this->mark_failed();
return; return;
} }
if (reply[0] == 0xD400) { if (reply == 0xD400) {
this->self_test_complete_ = true; this->self_test_complete_ = true;
ESP_LOGD(TAG, "Self-test completed"); ESP_LOGD(TAG, "Self-test completed");
return; return;
@ -192,51 +192,28 @@ uint16_t SGP40Component::measure_raw_() {
temperature = 25; temperature = 25;
} }
uint8_t command[8]; uint16_t data[2];
command[0] = 0x26;
command[1] = 0x0F;
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
command[2] = rhticks >> 8;
command[3] = rhticks & 0xFF;
command[4] = generate_crc_(command + 2, 2);
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
command[5] = tempticks >> 8; // first paramater is the relative humidity ticks
command[6] = tempticks & 0xFF; data[0] = rhticks;
command[7] = generate_crc_(command + 5, 2); // second paramater is the temperature ticks
data[1] = tempticks;
if (this->write(command, 8) != i2c::ERROR_OK) { if (!this->write_command(SGP40_CMD_MEASURE_RAW, data, 2)) {
this->status_set_warning(); this->status_set_warning();
ESP_LOGD(TAG, "write error"); ESP_LOGD(TAG, "write error (%d)", this->last_error_);
return UINT16_MAX; return false;
} }
delay(30); delay(30);
uint16_t raw_data[1];
if (!this->read_data_(raw_data, 1)) { uint16_t raw_data;
if (!this->read_data(raw_data)) {
this->status_set_warning(); this->status_set_warning();
ESP_LOGD(TAG, "read_data_ error"); ESP_LOGD(TAG, "read_data_ error");
return UINT16_MAX; return UINT16_MAX;
} }
return raw_data[0]; return raw_data;
}
uint8_t SGP40Component::generate_crc_(const uint8_t *data, uint8_t datalen) {
// calculates 8-Bit checksum with given polynomial
uint8_t crc = SGP40_CRC8_INIT;
for (uint8_t i = 0; i < datalen; i++) {
crc ^= data[i];
for (uint8_t b = 0; b < 8; b++) {
if (crc & 0x80) {
crc = (crc << 1) ^ SGP40_CRC8_POLYNOMIAL;
} else {
crc <<= 1;
}
}
}
return crc;
} }
void SGP40Component::update_voc_index() { void SGP40Component::update_voc_index() {
@ -293,56 +270,5 @@ void SGP40Component::dump_config() {
} }
} }
bool SGP40Component::write_command_(uint16_t command) {
// Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
return this->write_byte(command >> 8, command & 0xFF);
}
uint8_t SGP40Component::sht_crc_(uint8_t data1, uint8_t data2) {
uint8_t bit;
uint8_t crc = 0xFF;
crc ^= data1;
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x131;
} else {
crc = (crc << 1);
}
}
crc ^= data2;
for (bit = 8; bit > 0; --bit) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x131;
} else {
crc = (crc << 1);
}
}
return crc;
}
bool SGP40Component::read_data_(uint16_t *data, uint8_t len) {
const uint8_t num_bytes = len * 3;
std::vector<uint8_t> buf(num_bytes);
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
return false;
}
for (uint8_t i = 0; i < len; i++) {
const uint8_t j = 3 * i;
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
if (crc != buf[j + 2]) {
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
return false;
}
data[i] = (buf[j] << 8) | buf[j + 1];
}
return true;
}
} // namespace sgp40 } // namespace sgp40
} // namespace esphome } // namespace esphome

View file

@ -2,7 +2,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/sensirion_common/i2c_sensirion.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include "sensirion_voc_algorithm.h" #include "sensirion_voc_algorithm.h"
@ -28,6 +28,7 @@ static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word
static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682; static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682;
static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f; static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f;
static const uint16_t SGP40_CMD_SELF_TEST = 0x280e; static const uint16_t SGP40_CMD_SELF_TEST = 0x280e;
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
// Shortest time interval of 3H for storing baseline values. // Shortest time interval of 3H for storing baseline values.
// Prevents wear of the flash because of too many write operations // Prevents wear of the flash because of too many write operations
@ -39,7 +40,7 @@ const uint32_t MAXIMUM_STORAGE_DIFF = 50;
class SGP40Component; class SGP40Component;
/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors. /// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors.
class SGP40Component : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { class SGP40Component : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
public: public:
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
@ -55,11 +56,8 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2
/// Input sensor for humidity and temperature compensation. /// Input sensor for humidity and temperature compensation.
sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr};
bool write_command_(uint16_t command);
bool read_data_(uint16_t *data, uint8_t len);
int16_t sensirion_init_sensors_(); int16_t sensirion_init_sensors_();
int16_t sgp40_probe_(); int16_t sgp40_probe_();
uint8_t sht_crc_(uint8_t data1, uint8_t data2);
uint64_t serial_number_; uint64_t serial_number_;
uint16_t featureset_; uint16_t featureset_;
int32_t measure_voc_index_(); int32_t measure_voc_index_();

Some files were not shown because too many files have changed in this diff Show more