diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9549d5cedc..1e666f20af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/ambv/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black args: @@ -26,7 +26,7 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/CODEOWNERS b/CODEOWNERS index c111fa7816..7595fc52e2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -82,6 +82,7 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter esphome/components/honeywellabp/* @RubyBailey esphome/components/hrxl_maxsonar_wr/* @netmikey +esphome/components/hydreon_rgxx/* @functionpointer esphome/components/i2c/* @esphome/core esphome/components/improv_serial/* @esphome/core esphome/components/ina260/* @MrEditor97 @@ -151,6 +152,7 @@ esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core esphome/components/pulse_meter/* @cstaahl @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz +esphome/components/qmp6988/* @andrewpc esphome/components/qr_code/* @wjtje esphome/components/radon_eye_ble/* @jeffeb3 esphome/components/radon_eye_rd200/* @jeffeb3 @@ -168,6 +170,7 @@ esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath esphome/components/selec_meter/* @sourabhjaiswal esphome/components/select/* @esphome/core +esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny @@ -175,6 +178,7 @@ esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/socket/* @esphome/core +esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/spi/* @esphome/core esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 @@ -222,4 +226,5 @@ esphome/components/whirlpool/* @glmnet esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs +esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xpt2046/* @numo68 diff --git a/docker/Dockerfile b/docker/Dockerfile index 65e831f89b..610b689298 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,13 +6,13 @@ ARG BASEIMGTYPE=docker # 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/aarch64:5.2.3 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/amd64:5.3.0 AS base-hassio-amd64 +FROM ghcr.io/hassio-addons/debian-base/aarch64:5.3.0 AS base-hassio-arm64 +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 -FROM debian:bullseye-20220125-slim AS base-docker-amd64 -FROM debian:bullseye-20220125-slim AS base-docker-arm64 -FROM debian:bullseye-20220125-slim AS base-docker-armv7 +FROM debian:bullseye-20220328-slim AS base-docker-amd64 +FROM debian:bullseye-20220328-slim AS base-docker-arm64 +FROM debian:bullseye-20220328-slim AS base-docker-armv7 # Use TARGETARCH/TARGETVARIANT defined by docker # 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 && apt-get install -y --no-install-recommends \ 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-pil=8.1.2+dfsg-0.3+deb11u1 \ python3-cryptography=3.3.2-1 \ diff --git a/esphome/automation.py b/esphome/automation.py index fab998527f..4007dc4c51 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -262,21 +262,16 @@ async def repeat_action_to_code(config, action_id, template_arg, args): return var -def validate_wait_until(value): - schema = cv.Schema( - { - cv.Required(CONF_CONDITION): validate_potentially_and_condition, - cv.Optional(CONF_TIMEOUT): cv.templatable( - cv.positive_time_period_milliseconds - ), - } - ) - if isinstance(value, dict) and CONF_CONDITION in value: - return schema(value) - return validate_wait_until({CONF_CONDITION: value}) +_validate_wait_until = cv.maybe_simple_value( + { + cv.Required(CONF_CONDITION): validate_potentially_and_condition, + cv.Optional(CONF_TIMEOUT): cv.templatable(cv.positive_time_period_milliseconds), + }, + key=CONF_CONDITION, +) -@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): conditions = await build_condition(config[CONF_CONDITION], template_arg, args) var = cg.new_Pvariable(action_id, template_arg, conditions) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b998ef5929..81f2465b74 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -23,7 +23,7 @@ static const char *const TAG = "api.connection"; static const int ESP32_CAMERA_STOP_STREAM = 5000; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) - : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { + : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { this->proto_write_buffer_.reserve(64); #if defined(USE_API_PLAINTEXT) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 3214da5b3d..fdc46922ad 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -7,7 +7,6 @@ #include "esphome/components/socket/socket.h" #include "api_pb2.h" #include "api_pb2_service.h" -#include "util.h" #include "list_entities.h" #include "subscribe_state.h" #include "user_services.h" diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index fb0dfa3d05..9f55fda617 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -40,8 +40,7 @@ bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->s #endif bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } -ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client) - : ComponentIterator(server), client_(client) {} +ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_list_entities_services_response(resp); diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index bfceb39ebf..51c343eb03 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -1,8 +1,8 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/component_iterator.h" #include "esphome/core/defines.h" -#include "util.h" namespace esphome { namespace api { @@ -11,7 +11,7 @@ class APIConnection; class ListEntitiesIterator : public ComponentIterator { public: - ListEntitiesIterator(APIServer *server, APIConnection *client); + ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; #endif @@ -60,5 +60,3 @@ class ListEntitiesIterator : public ComponentIterator { } // namespace api } // namespace esphome - -#include "api_server.h" diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 0ba277d90a..ca7a4c0887 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -1,5 +1,4 @@ #include "proto.h" -#include "util.h" #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 38fd98b489..32f525990d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -195,6 +195,20 @@ class ProtoWriteBuffer { this->write((value >> 16) & 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 void encode_enum(uint32_t field_id, T value, bool force = false) { this->encode_uint32(field_id, static_cast(value), force); } @@ -229,6 +243,15 @@ class ProtoWriteBuffer { } 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 void encode_message(uint32_t field_id, const C &value, bool force = false) { this->encode_field_raw(field_id, 2); size_t begin = this->buffer_->size(); diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 10416ecc5c..ba277502c8 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -50,8 +50,7 @@ bool InitialStateIterator::on_select(select::Select *select) { #ifdef USE_LOCK bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); } #endif -InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) - : ComponentIterator(server), client_(client) {} +InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api } // namespace esphome diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index caea013f84..515e1a2d07 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -1,9 +1,9 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/component_iterator.h" #include "esphome/core/controller.h" #include "esphome/core/defines.h" -#include "util.h" namespace esphome { namespace api { @@ -12,7 +12,7 @@ class APIConnection; class InitialStateIterator : public ComponentIterator { public: - InitialStateIterator(APIServer *server, APIConnection *client); + InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; #endif @@ -55,5 +55,3 @@ class InitialStateIterator : public ComponentIterator { } // namespace api } // namespace esphome - -#include "api_server.h" diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index ee12226977..2e548a8072 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -38,7 +38,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { const auto &data = service_data.data; 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); 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]; float battery_voltage = battery_millivolt / 1000.0f; - // Temperature in 1000 * Celsius. - uint16_t temp_millicelcius = data[4] << 8 | data[5]; - float temp_celcius = temp_millicelcius / 1000.0f; + // Temperature in 1000 * Celsius (protocol v1) or 100 * Celsius (protocol v2). + float temp_celsius; + 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). 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); } if (temperature_ != nullptr) { - temperature_->publish_state(temp_celcius); + temperature_->publish_state(temp_celsius); } if (humidity_ != nullptr) { humidity_->publish_state(humidity_percent); diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 5b2701d77a..7bef0d652c 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -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); break; } - this->conn_id = param->open.conn_id; - auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id); + break; + } + 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) { - 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; } case ESP_GATTC_CFG_MTU_EVT: { 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); 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) { 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_) delete svc; // NOLINT(cppcoreguidelines-owning-memory) 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. // Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ float BLEClient::parse_char_value(uint8_t *value, uint16_t length) { diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index e0a1bf61b9..b122bfd11e 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace esphome { 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, 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; void on_scan_end() override {} void connect() override; diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 5f614eb0a4..20f2642144 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -10,6 +10,7 @@ IS_PLATFORM_COMPONENT = True CONF_CAN_ID = "can_id" CONF_CAN_ID_MASK = "can_id_mask" CONF_USE_EXTENDED_ID = "use_extended_id" +CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request" CONF_CANBUS_ID = "canbus_id" CONF_BIT_RATE = "bit_rate" 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.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), 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), }, 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)) + 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] if isinstance(data, bytes): data = [int(x) for x in data] diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index 14dc1544cf..5d9084706b 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -22,20 +22,22 @@ void Canbus::dump_config() { } } -void Canbus::send_data(uint32_t can_id, bool use_extended_id, const std::vector &data) { +void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, + const std::vector &data) { struct CanFrame can_message; uint8_t size = static_cast(data.size()); 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 { - 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) size = CAN_MAX_DATA_LENGTH; can_message.can_data_length_code = size; can_message.can_id = can_id; can_message.use_extended_id = use_extended_id; + can_message.remote_transmission_request = remote_transmission_request; for (int i = 0; i < size; i++) { can_message.data[i] = data[i]; diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 0491e8d3c1..20c490c083 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -62,7 +62,12 @@ class Canbus : public Component { float get_setup_priority() const override { return setup_priority::HARDWARE; } void loop() override; - void send_data(uint32_t can_id, bool use_extended_id, const std::vector &data); + void send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, + const std::vector &data); + void send_data(uint32_t can_id, bool use_extended_id, const std::vector &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_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; } @@ -96,21 +101,26 @@ template class CanbusSendAction : public Action, public P 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 { auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto use_extended_id = this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; 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 { 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: optional can_id_{}; optional use_extended_id_{}; + bool remote_transmission_request_{false}; bool static_{false}; std::function(Ts...)> data_func_{}; std::vector data_static_{}; diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 3e5626919c..139400a08a 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -76,7 +76,7 @@ enum ClimateSwingMode : uint8_t { CLIMATE_SWING_HORIZONTAL = 3, }; -/// Enum for all modes a climate swing can be in +/// Enum for all preset modes enum ClimatePreset : uint8_t { /// No preset is active 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. 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); } // namespace climate diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index 885846e5e5..5bd0f42855 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -142,7 +142,6 @@ void IRAM_ATTR ESPOneWire::select(uint64_t address) { void IRAM_ATTR ESPOneWire::reset_search() { this->last_discrepancy_ = 0; this->last_device_flag_ = false; - this->last_family_discrepancy_ = 0; this->rom_number_ = 0; } uint64_t IRAM_ATTR ESPOneWire::search() { @@ -195,9 +194,6 @@ uint64_t IRAM_ATTR ESPOneWire::search() { if (!branch) { last_zero = id_bit_number; - if (last_zero < 9) { - this->last_discrepancy_ = last_zero; - } } } diff --git a/esphome/components/dallas/esp_one_wire.h b/esphome/components/dallas/esp_one_wire.h index ef6f079f02..7544a6fe98 100644 --- a/esphome/components/dallas/esp_one_wire.h +++ b/esphome/components/dallas/esp_one_wire.h @@ -60,7 +60,6 @@ class ESPOneWire { ISRInternalGPIOPin pin_; uint8_t last_discrepancy_{0}; - uint8_t last_family_discrepancy_{0}; bool last_device_flag_{false}; uint64_t rom_number_{0}; }; diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 2a74d0c1bb..058358fa67 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,13 +1,18 @@ import esphome.codegen as cg +from esphome.components import time import esphome.config_validation as cv from esphome import pins, automation from esphome.const import ( + CONF_HOUR, CONF_ID, + CONF_MINUTE, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_DURATION, + CONF_SECOND, CONF_SLEEP_DURATION, + CONF_TIME_ID, CONF_WAKEUP_PIN, ) @@ -15,6 +20,7 @@ from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, + VARIANT_ESP32S2, ) WAKEUP_PINS = { @@ -39,6 +45,30 @@ WAKEUP_PINS = { 39, ], 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_GPIO_WAKEUP_REASON = "gpio_wakeup_reason" CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason" +CONF_UNTIL = "until" WAKEUP_CAUSES_SCHEMA = cv.Schema( { @@ -177,13 +208,19 @@ async def to_code(config): cg.add_define("USE_DEEP_SLEEP") -DEEP_SLEEP_ENTER_SCHEMA = automation.maybe_simple_id( - { - cv.GenerateID(): cv.use_id(DeepSleepComponent), - cv.Optional(CONF_SLEEP_DURATION): cv.templatable( - cv.positive_time_period_milliseconds - ), - } +DEEP_SLEEP_ENTER_SCHEMA = cv.All( + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(DeepSleepComponent), + 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: template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32) 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 diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 82751b538b..1bb70e0d7e 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -1,6 +1,7 @@ #include "deep_sleep_component.h" -#include "esphome/core/log.h" +#include #include "esphome/core/application.h" +#include "esphome/core/log.h" #ifdef USE_ESP8266 #include @@ -101,6 +102,8 @@ void DeepSleepComponent::begin_sleep(bool manual) { #endif 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(); diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 057d992427..5e90d4b89d 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -9,6 +9,10 @@ #include #endif +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + namespace esphome { namespace deep_sleep { @@ -116,15 +120,71 @@ template class EnterDeepSleepAction : public Action { EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} 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 { if (this->sleep_duration_.has_value()) { 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); } protected: DeepSleepComponent *deep_sleep_; +#ifdef USE_TIME + optional hour_; + optional minute_; + optional second_; + + time::RealTimeClock *time_; +#endif }; template class PreventDeepSleepAction : public Action { diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 67c6a4ebd3..f468d13492 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -12,6 +12,7 @@ using namespace esphome::cover; CoverTraits EndstopCover::get_traits() { auto traits = CoverTraits(); traits.set_supports_position(true); + traits.set_supports_toggle(true); traits.set_is_assumed_state(false); return traits; } @@ -20,6 +21,20 @@ void EndstopCover::control(const CoverCall &call) { this->start_direction_(COVER_OPERATION_IDLE); 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()) { auto pos = *call.get_position(); if (pos == this->position) { @@ -125,9 +140,11 @@ void EndstopCover::start_direction_(CoverOperation dir) { trig = this->stop_trigger_; break; case COVER_OPERATION_OPENING: + this->last_operation_ = dir; trig = this->open_trigger_; break; case COVER_OPERATION_CLOSING: + this->last_operation_ = dir; trig = this->close_trigger_; break; default: diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h index f8d2746234..6ae15de8c1 100644 --- a/esphome/components/endstop/endstop_cover.h +++ b/esphome/components/endstop/endstop_cover.h @@ -51,6 +51,7 @@ class EndstopCover : public cover::Cover, public Component { uint32_t start_dir_time_{0}; uint32_t last_publish_time_{0}; float target_position_{0}; + cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; }; } // namespace endstop diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 7614e33979..9722104e25 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -262,6 +262,9 @@ void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ default: 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 ¶m) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 9ff2a5a861..62fff30a20 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -155,6 +155,7 @@ class ESPBTClient : public ESPBTDeviceListener { public: 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; + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; virtual void connect() = 0; void set_state(ClientState st) { this->state_ = st; } ClientState state() const { return state_; } diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 6af5be45d4..9317b2ec94 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,12 +1,29 @@ import functools +from pathlib import Path +import hashlib +import re + +import requests from esphome import core from esphome.components import display import esphome.config_validation as cv 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 + +DOMAIN = "font" DEPENDENCIES = ["display"] MULTI_CONF = True @@ -71,6 +88,128 @@ def validate_truetype_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 = ( ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ) @@ -79,7 +218,7 @@ CONF_RAW_GLYPH_ID = "raw_glyph_id" FONT_SCHEMA = cv.Schema( { 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_SIZE, default=20): cv.int_range(min=1), 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): 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: - font = ImageFont.truetype(path, config[CONF_SIZE]) + font = ImageFont.truetype(str(path), config[CONF_SIZE]) except Exception as e: raise core.EsphomeError(f"Could not load truetype file {path}: {e}") diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index ed7240ab6c..ed753c4d3f 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -7,9 +7,11 @@ namespace growatt_solar { static const char *const TAG = "growatt_solar"; 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 &data) { 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 &data) { 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].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_[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_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_, 11, 12, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_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->phases_[0].voltage_sensor_, 14, 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_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_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].current_sensor_, 19, 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_[1].voltage_sensor_, 18, 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_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_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, 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_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->total_energy_production_, 28, 29, 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_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() { diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index 5356ac907a..0067998133 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -10,12 +10,19 @@ namespace growatt_solar { static const float TWO_DEC_UNIT = 0.01; static const float ONE_DEC_UNIT = 0.1; +enum GrowattProtocolVersion { + RTU = 0, + RTU2, +}; + class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { public: void update() override; void on_modbus_data(const std::vector &data) 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_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 *total_energy_production_{nullptr}; sensor::Sensor *inverter_module_temp_{nullptr}; + GrowattProtocolVersion protocol_version_; }; } // namespace growatt_solar diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py index 99936c33ee..4961595505 100644 --- a/esphome/components/growatt_solar/sensor.py +++ b/esphome/components/growatt_solar/sensor.py @@ -39,7 +39,7 @@ UNIT_MILLIAMPERE = "mA" CONF_INVERTER_STATUS = "inverter_status" CONF_PV_ACTIVE_POWER = "pv_active_power" CONF_INVERTER_MODULE_TEMP = "inverter_module_temp" - +CONF_PROTOCOL_VERSION = "protocol_version" AUTO_LOAD = ["modbus"] CODEOWNERS = ["@leeuwte"] @@ -95,10 +95,20 @@ PV_SCHEMA = cv.Schema( {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 = ( cv.Schema( { 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_B): 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 modbus.register_modbus_device(var, config) + cg.add(var.set_protocol_version(config[CONF_PROTOCOL_VERSION])) + if CONF_INVERTER_STATUS in config: sens = await sensor.new_sensor(config[CONF_INVERTER_STATUS]) cg.add(var.set_inverter_status_sensor(sens)) diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 744ef5e527..a253a778de 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -25,6 +25,7 @@ PROTOCOLS = { "daikin_arc417": Protocol.PROTOCOL_DAIKIN_ARC417, "daikin_arc480": Protocol.PROTOCOL_DAIKIN_ARC480, "daikin": Protocol.PROTOCOL_DAIKIN, + "electroluxyal": Protocol.PROTOCOL_ELECTROLUXYAL, "fuego": Protocol.PROTOCOL_FUEGO, "fujitsu_awyz": Protocol.PROTOCOL_FUJITSU_AWYZ, "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_min_temperature(config[CONF_MIN_TEMPERATURE])) - # PIO isn't updating releases, so referencing the release tag directly. See: - # https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd - cg.add_library("", "", "https://github.com/ToniA/arduino-heatpumpir.git#1.0.18") + cg.add_library("tonia/HeatpumpIR", "1.0.20") diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index ad3731b955..cd24411763 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -20,6 +20,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT {PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT {PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT + {PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT {PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT {PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT {PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index 18d9b5040f..decf1eae07 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -20,6 +20,7 @@ enum Protocol { PROTOCOL_DAIKIN_ARC417, PROTOCOL_DAIKIN_ARC480, PROTOCOL_DAIKIN, + PROTOCOL_ELECTROLUXYAL, PROTOCOL_FUEGO, PROTOCOL_FUJITSU_AWYZ, PROTOCOL_GREE, diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h index 42d900a262..038828e9de 100644 --- a/esphome/components/hm3301/abstract_aqi_calculator.h +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -7,7 +7,7 @@ namespace hm3301 { class AbstractAQICalculator { 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 diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index 08d1dc2921..6c830f9bad 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -7,7 +7,7 @@ namespace hm3301 { class AQICalculator : public AbstractAQICalculator { 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 pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); diff --git a/esphome/components/hm3301/caqi_calculator.h b/esphome/components/hm3301/caqi_calculator.h index 1ec61f2416..3f338776d8 100644 --- a/esphome/components/hm3301/caqi_calculator.h +++ b/esphome/components/hm3301/caqi_calculator.h @@ -8,7 +8,7 @@ namespace hm3301 { class CAQICalculator : public AbstractAQICalculator { 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 pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index a2bef2a01d..379c4dbc5a 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -62,7 +62,7 @@ void HM3301Component::update() { 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) { 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); diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index c151abc250..776aa7fd7b 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -1,4 +1,20 @@ import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL CODEOWNERS = ["@OttoWinter"] 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])) diff --git a/esphome/components/homeassistant/binary_sensor/__init__.py b/esphome/components/homeassistant/binary_sensor/__init__.py index a4f854c16e..a943368dd7 100644 --- a/esphome/components/homeassistant/binary_sensor/__init__.py +++ b/esphome/components/homeassistant/binary_sensor/__init__.py @@ -1,30 +1,24 @@ import esphome.codegen as cg -import esphome.config_validation as cv 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"] + HomeassistantBinarySensor = homeassistant_ns.class_( "HomeassistantBinarySensor", binary_sensor.BinarySensor, cg.Component ) -CONFIG_SCHEMA = ( - binary_sensor.binary_sensor_schema(HomeassistantBinarySensor) - .extend( - { - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } - ) - .extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).extend( + HOME_ASSISTANT_IMPORT_SCHEMA ) async def to_code(config): var = await binary_sensor.new_binary_sensor(config) await cg.register_component(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])) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index 28fee9f7f6..6437476827 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -1,12 +1,11 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import ( - CONF_ATTRIBUTE, - CONF_ENTITY_ID, - CONF_ID, + +from .. import ( + HOME_ASSISTANT_IMPORT_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, ) -from .. import homeassistant_ns DEPENDENCIES = ["api"] @@ -14,19 +13,12 @@ HomeassistantSensor = homeassistant_ns.class_( "HomeassistantSensor", sensor.Sensor, cg.Component ) -CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1,).extend( - { - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } +CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).extend( + HOME_ASSISTANT_IMPORT_SCHEMA ) 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 sensor.register_sensor(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])) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/text_sensor/__init__.py b/esphome/components/homeassistant/text_sensor/__init__.py index be59bab676..b59f9d23df 100644 --- a/esphome/components/homeassistant/text_sensor/__init__.py +++ b/esphome/components/homeassistant/text_sensor/__init__.py @@ -1,9 +1,11 @@ import esphome.codegen as cg -import esphome.config_validation as cv 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"] @@ -11,19 +13,12 @@ HomeassistantTextSensor = homeassistant_ns.class_( "HomeassistantTextSensor", text_sensor.TextSensor, cg.Component ) -CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend( - { - cv.GenerateID(): cv.declare_id(HomeassistantTextSensor), - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } +CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend( + HOME_ASSISTANT_IMPORT_SCHEMA ) async def to_code(config): var = await text_sensor.new_text_sensor(config) await cg.register_component(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])) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/hydreon_rgxx/__init__.py b/esphome/components/hydreon_rgxx/__init__.py new file mode 100644 index 0000000000..5fe050edf2 --- /dev/null +++ b/esphome/components/hydreon_rgxx/__init__.py @@ -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 +) diff --git a/esphome/components/hydreon_rgxx/binary_sensor.py b/esphome/components/hydreon_rgxx/binary_sensor.py new file mode 100644 index 0000000000..0d489ebcb7 --- /dev/null +++ b/esphome/components/hydreon_rgxx/binary_sensor.py @@ -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)) diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp new file mode 100644 index 0000000000..3ed65831ae --- /dev/null +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -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(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 diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h new file mode 100644 index 0000000000..ebe4a35b19 --- /dev/null +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -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 diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py new file mode 100644 index 0000000000..409500305a --- /dev/null +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -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)) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 50a0b3ae50..ffc0dadf81 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -46,21 +46,21 @@ class I2CDevice { 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_register(uint8_t a_register, uint8_t *data, size_t len) { - ErrorCode err = this->write(&a_register, 1); + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true) { + ErrorCode err = this->write(&a_register, 1, stop); if (err != ERROR_OK) return err; return this->read(data, len); } - ErrorCode write(const uint8_t *data, uint8_t len) { return bus_->write(address_, data, len); } - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t 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, bool stop = true) { WriteBuffer buffers[2]; buffers[0].data = &a_register; buffers[0].len = 1; buffers[1].data = data; buffers[1].len = len; - return bus_->writev(address_, buffers, 2); + return bus_->writev(address_, buffers, 2, stop); } // Compat APIs @@ -93,7 +93,9 @@ class I2CDevice { 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 read_byte(uint8_t a_register) { 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 write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { - return write_register(a_register, data, len) == ERROR_OK; + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop = true) { + return write_register(a_register, data, len, stop) == ERROR_OK; } bool write_bytes(uint8_t a_register, const std::vector &data) { @@ -118,7 +120,9 @@ class I2CDevice { 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); } diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 71f6b1d15b..2633a7adf6 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -15,6 +15,7 @@ enum ErrorCode { ERROR_NOT_INITIALIZED = 4, ERROR_TOO_LARGE = 5, ERROR_UNKNOWN = 6, + ERROR_CRC = 7, }; struct ReadBuffer { @@ -36,12 +37,18 @@ class I2CBus { } 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) { + return write(address, buffer, len, true); + } + virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { WriteBuffer buf; buf.data = buffer; 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: void i2c_scan_() { diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 693b869bf7..cfdf818112 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -104,7 +104,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) 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 // should log them if (!initialized_) { @@ -139,7 +139,7 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn return ERROR_UNKNOWN; } } - uint8_t status = wire_->endTransmission(true); + uint8_t status = wire_->endTransmission(stop); if (status == 0) { return ERROR_OK; } else if (status == 1) { diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index f4151e4f37..7298c3a1c9 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -20,7 +20,7 @@ class ArduinoI2CBus : public I2CBus, public Component { void setup() override; void dump_config() 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; } void set_scan(bool scan) { scan_ = scan; } diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 606583fd7c..160b1b96d8 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -142,7 +142,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { 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 // should log them if (!initialized_) { diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index d4b0626467..c80ea8c99d 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -20,7 +20,7 @@ class IDFI2CBus : public I2CBus, public Component { void setup() override; void dump_config() 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; } void set_scan(bool scan) { scan_ = scan; } diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 9acba76597..10179c9954 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -16,16 +16,24 @@ static const char *const TAG = "json"; static std::vector global_json_build_buffer; // NOLINT std::string build_json(const json_build_t &f) { - // Here we are allocating as much heap memory as available minus 2kb to be safe + // Here we are allocating up to 5kb of memory, + // with the heap size minus 2kb to be safe if less than 5kb // as we can not have a true dynamic sized document. // The excess memory is freed below with `shrinkToFit()` #ifdef USE_ESP8266 - const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) + const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); #endif - DynamicJsonDocument json_document(free_heap); + const size_t request_size = std::min(free_heap - 2048, (size_t) 5120); + + DynamicJsonDocument json_document(request_size); + if (json_document.memoryPool().buffer() == nullptr) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", + request_size, free_heap); + return "{}"; + } JsonObject root = json_document.to(); f(root); json_document.shrinkToFit(); @@ -36,27 +44,45 @@ std::string build_json(const json_build_t &f) { } void parse_json(const std::string &data, const json_parse_t &f) { - // Here we are allocating as much heap memory as available minus 2kb to be safe + // Here we are allocating 1.5 times the data size, + // with the heap size minus 2kb to be safe if less than that // as we can not have a true dynamic sized document. // The excess memory is freed below with `shrinkToFit()` #ifdef USE_ESP8266 - const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) + const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); #endif + bool pass = false; + size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); + do { + DynamicJsonDocument json_document(request_size); + if (json_document.memoryPool().buffer() == nullptr) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, + free_heap); + return; + } + DeserializationError err = deserializeJson(json_document, data); + json_document.shrinkToFit(); - DynamicJsonDocument json_document(free_heap); - DeserializationError err = deserializeJson(json_document, data); - json_document.shrinkToFit(); + JsonObject root = json_document.as(); - JsonObject root = json_document.as(); - - if (err) { - ESP_LOGW(TAG, "Parsing JSON failed."); - return; - } - - f(root); + if (err == DeserializationError::Ok) { + pass = true; + f(root); + } else if (err == DeserializationError::NoMemory) { + if (request_size * 2 >= free_heap) { + ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); + return; + } + ESP_LOGV(TAG, "Increasing memory allocation."); + request_size *= 2; + continue; + } else { + ESP_LOGE(TAG, "JSON parse error: %s", err.c_str()); + return; + } + } while (!pass); } } // namespace json diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index 0ed2036c55..92fd0b5563 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -1,7 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv 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") LCDDisplay = lcd_base_ns.class_("LCDDisplay", cg.PollingComponent) @@ -16,9 +18,35 @@ def validate_lcd_dimensions(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( { 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")) @@ -27,3 +55,6 @@ async def setup_lcd_display(var, config): await cg.register_component(var, config) await display.register_display(var, config) 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])) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index ddd7d6a6b3..180d5e93ac 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -65,6 +65,13 @@ void LCDDisplay::setup() { 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); uint8_t display_control = LCD_DISPLAY_DISPLAY_ON; 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); } #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 esphome diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index ee150059c6..c8ba39f0d4 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -7,6 +7,8 @@ #include "esphome/components/time/real_time_clock.h" #endif +#include + namespace esphome { namespace lcd_base { @@ -19,6 +21,8 @@ class LCDDisplay : public PollingComponent { this->rows_ = rows; } + void set_user_defined_char(uint8_t pos, const std::vector &data) { this->user_defined_chars_[pos] = data; } + void setup() override; float get_setup_priority() const 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))); #endif + /// Load custom char to given location + void loadchar(uint8_t location, uint8_t charmap[]); + protected: virtual bool is_four_bit_mode() = 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 rows_; uint8_t *buffer_{nullptr}; + std::map > user_defined_chars_; }; } // namespace lcd_base diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 20a0b0f792..d11b00405d 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -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): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python cfmt = r""" @@ -234,7 +225,7 @@ def validate_printf(value): CONF_LOGGER_LOG = "logger.log" LOGGER_LOG_ACTION_SCHEMA = cv.All( - maybe_simple_message( + cv.maybe_simple_value( { cv.Required(CONF_FORMAT): cv.string, 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 ), cv.Optional(CONF_TAG, default="main"): cv.string, - } - ), - validate_printf, + }, + validate_printf, + key=CONF_FORMAT, + ) ) diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index 44044349a3..283df4ccdc 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -20,7 +20,7 @@ void MCP3204::dump_config() { } 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; this->enable(); this->transfer_byte(adc_primary_config); diff --git a/esphome/components/mcp3204/sensor/__init__.py b/esphome/components/mcp3204/sensor/__init__.py index 1d8701a91e..404880d405 100644 --- a/esphome/components/mcp3204/sensor/__init__.py +++ b/esphome/components/mcp3204/sensor/__init__.py @@ -17,7 +17,7 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(MCP3204Sensor), 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")) diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index f919cb0678..41beb67e1c 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -71,9 +71,9 @@ SENSOR_VALUE_TYPE = { "S_DWORD": SensorValueType.S_DWORD, "S_DWORD_R": SensorValueType.S_DWORD_R, "U_QWORD": SensorValueType.U_QWORD, - "U_QWORDU_R": SensorValueType.U_QWORD_R, + "U_QWORD_R": SensorValueType.U_QWORD_R, "S_QWORD": SensorValueType.S_QWORD, - "U_QWORD_R": SensorValueType.S_QWORD_R, + "S_QWORD_R": SensorValueType.S_QWORD_R, "FP32": SensorValueType.FP32, "FP32_R": SensorValueType.FP32_R, } @@ -87,9 +87,9 @@ TYPE_REGISTER_MAP = { "S_DWORD": 2, "S_DWORD_R": 2, "U_QWORD": 4, - "U_QWORDU_R": 4, - "S_QWORD": 4, "U_QWORD_R": 4, + "S_QWORD": 4, + "S_QWORD_R": 4, "FP32": 2, "FP32_R": 2, } diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 64046b9578..91e0dcc45f 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -455,6 +455,28 @@ ModbusCommandItem ModbusCommandItem::create_custom_command( return cmd; } +ModbusCommandItem ModbusCommandItem::create_custom_command( + ModbusController *modbusdevice, const std::vector &values, + std::function &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 &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() { if (this->function_code != ModbusFunctionCode::CUSTOM) { modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(), diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 09395f29b3..6aecf7f8a4 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -2,12 +2,12 @@ #include "esphome/core/component.h" -#include "esphome/core/automation.h" #include "esphome/components/modbus/modbus.h" +#include "esphome/core/automation.h" #include -#include #include +#include #include namespace esphome { @@ -374,8 +374,8 @@ class ModbusCommandItem { const std::vector &values); /** Create custom modbus 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 - * exception of the crc codess + * @param values byte 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 */ @@ -383,6 +383,18 @@ class ModbusCommandItem { ModbusController *modbusdevice, const std::vector &values, std::function &data)> &&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 &values, + std::function &data)> + &&handler = nullptr); }; /** Modbus controller class. diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index a0e990d272..001cfb5787 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -26,6 +26,7 @@ void ModbusNumber::parse_and_publish(const std::vector &data) { } void ModbusNumber::control(float value) { + ModbusCommandItem write_cmd; std::vector data; float write_value = value; // Is there are lambda configured? @@ -45,33 +46,39 @@ void ModbusNumber::control(float value) { write_value = multiply_by_ * write_value; } - // lambda didn't set payload - if (data.empty()) { - data = float_to_payload(write_value, this->sensor_value_type); - } - - 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 - 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]); + if (!data.empty()) { + ESP_LOGV(TAG, "Modbus Number write raw: %s", format_hex_pretty(data).c_str()); + write_cmd = ModbusCommandItem::create_custom_command( + this->parent_, data, + [this, write_cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data); + }); } else { - write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2, - this->register_count, data); + data = float_to_payload(write_value, this->sensor_value_type); + + 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 &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 &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); + this->publish_state(value); } void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); } diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index 6f194ef2a3..f8ef61ddc4 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import select from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC -from esphome.jsonschema import jschema_composite from .. import ( SENSOR_VALUE_TYPE, @@ -30,7 +29,6 @@ ModbusSelect = modbus_controller_ns.class_( ) -@jschema_composite def ensure_option_map(): def validator(value): cv.check_not_templatable(value) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 901b77474d..b2548d6081 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, CONF_BROKER, + CONF_CERTIFICATE_AUTHORITY, CONF_CLIENT_ID, CONF_COMMAND_TOPIC, CONF_COMMAND_RETAIN, @@ -42,9 +43,14 @@ from esphome.const import ( CONF_WILL_MESSAGE, ) from esphome.core import coroutine_with_priority, CORE +from esphome.components.esp32 import add_idf_sdkconfig_option 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): @@ -163,6 +169,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_USERNAME, default=""): cv.string, cv.Optional(CONF_PASSWORD, default=""): 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.boolean, cv.one_of("CLEAN", upper=True) ), @@ -217,7 +232,6 @@ CONFIG_SCHEMA = cv.All( } ), validate_config, - cv.only_with_arduino, ) @@ -238,9 +252,11 @@ def exp_mqtt_message(config): async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) 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_global(mqtt_ns.using) @@ -321,6 +337,19 @@ async def to_code(config): 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, []): trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC]) cg.add(trig.set_qos(conf[CONF_QOS])) diff --git a/esphome/components/mqtt/mqtt_backend.h b/esphome/components/mqtt/mqtt_backend.h new file mode 100644 index 0000000000..d23cda578d --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#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 &&callback) = 0; + virtual void set_on_disconnect(std::function &&callback) = 0; + virtual void set_on_subscribe(std::function &&callback) = 0; + virtual void set_on_unsubscribe(std::function &&callback) = 0; + virtual void set_on_message(std::function &&callback) = 0; + virtual void set_on_publish(std::function &&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 diff --git a/esphome/components/mqtt/mqtt_backend_arduino.h b/esphome/components/mqtt/mqtt_backend_arduino.h new file mode 100644 index 0000000000..6399ec88e0 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_arduino.h @@ -0,0 +1,74 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "mqtt_backend.h" +#include + +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(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 &&callback) final { + this->mqtt_client_.onConnect(std::move(callback)); + } + void set_on_disconnect(std::function &&callback) final { + auto async_callback = [callback](AsyncMqttClientDisconnectReason reason) { + // int based enum so casting isn't a problem + callback(static_cast(reason)); + }; + this->mqtt_client_.onDisconnect(std::move(async_callback)); + } + void set_on_subscribe(std::function &&callback) final { + this->mqtt_client_.onSubscribe(std::move(callback)); + } + void set_on_unsubscribe(std::function &&callback) final { + this->mqtt_client_.onUnsubscribe(std::move(callback)); + } + void set_on_message(std::function &&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 &&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) diff --git a/esphome/components/mqtt/mqtt_backend_idf.cpp b/esphome/components/mqtt/mqtt_backend_idf.cpp new file mode 100644 index 0000000000..0726f72567 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_idf.cpp @@ -0,0 +1,149 @@ +#ifdef USE_ESP_IDF + +#include +#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(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(handler_args); + // queue event to decouple processing + if (instance) { + auto event = *static_cast(event_data); + instance->mqtt_events_.push(event); + } +} + +} // namespace mqtt +} // namespace esphome +#endif // USE_ESP_IDF diff --git a/esphome/components/mqtt/mqtt_backend_idf.h b/esphome/components/mqtt/mqtt_backend_idf.h new file mode 100644 index 0000000000..77b5592d72 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_idf.h @@ -0,0 +1,143 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include +#include +#include +#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 &&callback) final { + this->on_connect_.add(std::move(callback)); + } + void set_on_disconnect(std::function &&callback) final { + this->on_disconnect_.add(std::move(callback)); + } + void set_on_subscribe(std::function &&callback) final { + this->on_subscribe_.add(std::move(callback)); + } + void set_on_unsubscribe(std::function &&callback) final { + this->on_unsubscribe_.add(std::move(callback)); + } + void set_on_message(std::function &&callback) final { + this->on_message_.add(std::move(callback)); + } + void set_on_publish(std::function &&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; + 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 ca_certificate_; + bool skip_cert_cn_check_{false}; + + // callbacks + CallbackManager on_connect_; + CallbackManager on_disconnect_; + CallbackManager on_subscribe_; + CallbackManager on_unsubscribe_; + CallbackManager on_message_; + CallbackManager on_publish_; + std::queue mqtt_events_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 1fea0c80cc..3c6ce7cdfc 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -27,21 +27,21 @@ MQTTClientComponent::MQTTClientComponent() { // Connection void MQTTClientComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up MQTT..."); - this->mqtt_client_.onMessage([this](char const *topic, char *payload, AsyncMqttClientMessageProperties properties, - size_t len, size_t index, size_t total) { - if (index == 0) - this->payload_buffer_.reserve(total); + this->mqtt_backend_.set_on_message( + [this](const char *topic, const char *payload, size_t len, size_t index, size_t total) { + if (index == 0) + this->payload_buffer_.reserve(total); - // append new payload, may contain incomplete MQTT message - this->payload_buffer_.append(payload, len); + // append new payload, may contain incomplete MQTT message + this->payload_buffer_.append(payload, len); - // MQTT fully received - if (len + index == total) { - this->on_message(topic, this->payload_buffer_); - this->payload_buffer_.clear(); - } - }); - this->mqtt_client_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { + // MQTT fully received + if (len + index == total) { + this->on_message(topic, this->payload_buffer_); + this->payload_buffer_.clear(); + } + }); + this->mqtt_backend_.set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->state_ = MQTT_CLIENT_DISCONNECTED; this->disconnect_reason_ = reason; }); @@ -49,8 +49,10 @@ void MQTTClientComponent::setup() { 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) { if (level <= this->log_level_ && this->is_connected()) { - this->publish(this->log_message_.topic, message, strlen(message), this->log_message_.qos, - this->log_message_.retain); + this->publish({.topic = this->log_message_.topic, + .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..."); // 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; if (!this->credentials_.username.empty()) username = this->credentials_.username.c_str(); @@ -183,24 +185,24 @@ void MQTTClientComponent::start_connect_() { if (!this->credentials_.password.empty()) 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()) { - this->mqtt_client_.setWill(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->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->mqtt_client_.connect(); + this->mqtt_backend_.connect(); this->state_ = MQTT_CLIENT_CONNECTING; this->connect_begin_ = millis(); } 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() { - if (!this->mqtt_client_.connected()) { + if (!this->mqtt_backend_.connected()) { if (millis() - this->connect_begin_ > 60000) { this->state_ = MQTT_CLIENT_DISCONNECTED; this->start_dnslookup_(); @@ -222,31 +224,34 @@ void MQTTClientComponent::check_connected() { } void MQTTClientComponent::loop() { + // Call the backend loop first + mqtt_backend_.loop(); + if (this->disconnect_reason_.has_value()) { const LogString *reason_s; switch (*this->disconnect_reason_) { - case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED: + case MQTTClientDisconnectReason::TCP_DISCONNECTED: reason_s = LOG_STR("TCP disconnected"); break; - case AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: + case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: reason_s = LOG_STR("Unacceptable Protocol Version"); break; - case AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: + case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: reason_s = LOG_STR("Identifier Rejected"); break; - case AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: + case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: reason_s = LOG_STR("Server Unavailable"); break; - case AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: + case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: reason_s = LOG_STR("Malformed Credentials"); break; - case AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED: + case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED: reason_s = LOG_STR("Not Authorized"); break; - case AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE: + case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE: reason_s = LOG_STR("Not Enough Space"); break; - case AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT: + case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT: reason_s = LOG_STR("TLS Bad Fingerprint"); break; default: @@ -275,7 +280,7 @@ void MQTTClientComponent::loop() { this->check_connected(); break; case MQTT_CLIENT_CONNECTED: - if (!this->mqtt_client_.connected()) { + if (!this->mqtt_backend_.connected()) { this->state_ = MQTT_CLIENT_DISCONNECTED; ESP_LOGW(TAG, "Lost MQTT Client connection!"); this->start_dnslookup_(); @@ -302,10 +307,10 @@ bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) { if (!this->is_connected()) return false; - uint16_t ret = this->mqtt_client_.subscribe(topic, qos); + bool ret = this->mqtt_backend_.subscribe(topic, qos); yield(); - if (ret != 0) { + if (ret) { ESP_LOGV(TAG, "subscribe(topic='%s')", topic); } else { delay(5); @@ -360,9 +365,9 @@ void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_js } 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(); - if (ret != 0) { + if (ret) { ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str()); } else { 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 retain) { + return publish({.topic = topic, .payload = payload, .qos = qos, .retain = retain}); +} + +bool MQTTClientComponent::publish(const MQTTMessage &message) { if (!this->is_connected()) { // critical components will re-transmit their messages return false; } - bool logging_topic = topic == this->log_message_.topic; - uint16_t ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length); + bool logging_topic = this->log_message_.topic == message.topic; + bool ret = this->mqtt_backend_.publish(message); delay(0); - if (ret == 0 && !logging_topic && this->is_connected()) { + if (!ret && !logging_topic && this->is_connected()) { delay(0); - ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length); + ret = this->mqtt_backend_.publish(message); delay(0); } if (!logging_topic) { - if (ret != 0) { - ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", topic.c_str(), payload, retain); + if (ret) { + ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", message.topic.c_str(), message.payload.c_str(), + message.retain); } else { - ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", topic.c_str(), - payload_length); // NOLINT + ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", message.topic.c_str(), + message.payload.length()); this->status_momentary_warning("publish", 1000); } } 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 retain) { 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::register_mqtt_component(MQTTComponent *component) { this->children_.push_back(component); } 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); } 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_; } void MQTTClientComponent::disable_birth_message() { 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_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() { if (!this->shutdown_message_.topic.empty()) { @@ -557,13 +564,13 @@ void MQTTClientComponent::on_shutdown() { this->publish(this->shutdown_message_); yield(); } - this->mqtt_client_.disconnect(true); + this->mqtt_backend_.disconnect(); } #if ASYNC_TCP_SSL_ENABLED void MQTTClientComponent::add_ssl_fingerprint(const std::array &fingerprint) { - this->mqtt_client_.setSecure(true); - this->mqtt_client_.addServerFingerprint(fingerprint.data()); + this->mqtt_backend_.setSecure(true); + this->mqtt_backend_.addServerFingerprint(fingerprint.data()); } #endif diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 58a4fbe166..4880bbaa5b 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -9,7 +9,11 @@ #include "esphome/core/log.h" #include "esphome/components/json/json_util.h" #include "esphome/components/network/ip_address.h" -#include +#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" namespace esphome { @@ -22,14 +26,6 @@ namespace mqtt { using mqtt_callback_t = std::function; using mqtt_json_callback_t = std::function; -/// 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. struct MQTTSubscription { std::string topic; @@ -139,7 +135,10 @@ class MQTTClientComponent : public Component { */ void add_ssl_fingerprint(const std::array &fingerprint); #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(); /** 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. */ - 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 const std::string &get_topic_prefix() const; @@ -277,6 +276,7 @@ class MQTTClientComponent : public Component { .prefix = "homeassistant", .retain = true, .clean = false, + .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, }; std::string topic_prefix_{}; MQTTMessage log_message_; @@ -284,7 +284,12 @@ class MQTTClientComponent : public Component { int log_level_{ESPHOME_LOG_LEVEL}; std::vector 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}; network::IPAddress ip_; bool dns_resolved_{false}; @@ -293,7 +298,7 @@ class MQTTClientComponent : public Component { uint32_t reboot_timeout_{300000}; uint32_t connect_begin_; uint32_t last_connected_{0}; - optional disconnect_reason_{}; + optional disconnect_reason_{}; }; extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/nfc/__init__.py b/esphome/components/nfc/__init__.py index b795a5d5ca..c3bbc50bf9 100644 --- a/esphome/components/nfc/__init__.py +++ b/esphome/components/nfc/__init__.py @@ -1,3 +1,4 @@ +from esphome import automation import esphome.codegen as cg CODEOWNERS = ["@jesserockz"] @@ -5,3 +6,7 @@ CODEOWNERS = ["@jesserockz"] nfc_ns = cg.esphome_ns.namespace("nfc") NfcTag = nfc_ns.class_("NfcTag") + +NfcOnTagTrigger = nfc_ns.class_( + "NfcOnTagTrigger", automation.Trigger.template(cg.std_string, NfcTag) +) diff --git a/esphome/components/nfc/automation.cpp b/esphome/components/nfc/automation.cpp new file mode 100644 index 0000000000..ff00340df0 --- /dev/null +++ b/esphome/components/nfc/automation.cpp @@ -0,0 +1,9 @@ +#include "automation.h" + +namespace esphome { +namespace nfc { + +void NfcOnTagTrigger::process(const std::unique_ptr &tag) { this->trigger(format_uid(tag->get_uid()), *tag); } + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/automation.h b/esphome/components/nfc/automation.h new file mode 100644 index 0000000000..565b71bdd9 --- /dev/null +++ b/esphome/components/nfc/automation.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include "esphome/core/automation.h" + +#include "nfc.h" + +namespace esphome { +namespace nfc { + +class NfcOnTagTrigger : public Trigger { + public: + void process(const std::unique_ptr &tag); +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 71e288a4cc..89788f1e98 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -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.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), - cv.Optional(CONF_ABOVE): cv.float_, - cv.Optional(CONF_BELOW): cv.float_, + cv.Optional(CONF_ABOVE): cv.templatable(cv.float_), + cv.Optional(CONF_BELOW): cv.templatable(cv.float_), }, cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), ), diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 37da3bdc44..fa2605d589 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -473,6 +473,8 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_ App.reboot(); }); + // Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised. + delay(300); // NOLINT App.setup(); ESP_LOGI(TAG, "Waiting for OTA attempt."); diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index b902e8e3d0..2f120bc983 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -14,9 +14,6 @@ CONF_ON_FINISHED_WRITE = "on_finished_write" pn532_ns = cg.esphome_ns.namespace("pn532") 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", automation.Trigger.template() ) @@ -30,7 +27,7 @@ PN532_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(PN532), 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( @@ -42,7 +39,7 @@ PN532_SCHEMA = cv.Schema( ), 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), } ), } diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 0c46ff8a57..7ebf328cff 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -144,9 +144,9 @@ void PN532::loop() { } if (nfcid.size() == this->current_uid_.size()) { - bool same_uid = false; + bool same_uid = true; 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) return; } @@ -376,9 +376,6 @@ bool PN532BinarySensor::process(std::vector &data) { this->found_ = true; return true; } -void PN532OnTagTrigger::process(const std::unique_ptr &tag) { - this->trigger(nfc::format_uid(tag->get_uid()), *tag); -} } // namespace pn532 } // namespace esphome diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 692a5011e6..4f688dacc2 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -5,6 +5,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/nfc/nfc_tag.h" #include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/automation.h" namespace esphome { namespace pn532 { @@ -16,7 +17,6 @@ static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; class PN532BinarySensor; -class PN532OnTagTrigger; class PN532 : public PollingComponent { public: @@ -30,8 +30,8 @@ class PN532 : public PollingComponent { void loop() override; 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_ontagremoved_trigger(PN532OnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } void add_on_finished_write_callback(std::function callback) { this->on_finished_write_callback_.add(std::move(callback)); @@ -79,8 +79,8 @@ class PN532 : public PollingComponent { bool requested_read_{false}; std::vector binary_sensors_; - std::vector triggers_ontag_; - std::vector triggers_ontagremoved_; + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; std::vector current_uid_; nfc::NdefMessage *next_task_message_to_write_; enum NfcTask { @@ -115,11 +115,6 @@ class PN532BinarySensor : public binary_sensor::BinarySensor { bool found_{false}; }; -class PN532OnTagTrigger : public Trigger { - public: - void process(const std::unique_ptr &tag); -}; - class PN532OnFinishedWriteTrigger : public Trigger<> { public: explicit PN532OnFinishedWriteTrigger(PN532 *parent) { diff --git a/esphome/components/qmp6988/__init__.py b/esphome/components/qmp6988/__init__.py new file mode 100644 index 0000000000..09bcf51589 --- /dev/null +++ b/esphome/components/qmp6988/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@andrewpc"] diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp new file mode 100644 index 0000000000..5bad1e4a47 --- /dev/null +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -0,0 +1,397 @@ +#include "qmp6988.h" +#include + +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 diff --git a/esphome/components/qmp6988/qmp6988.h b/esphome/components/qmp6988/qmp6988.h new file mode 100644 index 0000000000..ef944ba4ff --- /dev/null +++ b/esphome/components/qmp6988/qmp6988.h @@ -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 diff --git a/esphome/components/qmp6988/sensor.py b/esphome/components/qmp6988/sensor.py new file mode 100644 index 0000000000..fdcfd4e66b --- /dev/null +++ b/esphome/components/qmp6988/sensor.py @@ -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])) diff --git a/esphome/components/rc522_spi/rc522_spi.h b/esphome/components/rc522_spi/rc522_spi.h index 58edbbed4f..0ccbcd7588 100644 --- a/esphome/components/rc522_spi/rc522_spi.h +++ b/esphome/components/rc522_spi/rc522_spi.h @@ -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 * 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 { class RC522Spi : public rc522::RC522, diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 4f6ace720c..a2b1a16e07 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -40,6 +40,24 @@ namespace remote_base { static const char *const TAG = "remote.pronto"; +bool ProntoData::operator==(const ProntoData &rhs) const { + std::vector data1 = encode_pronto(data); + std::vector 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::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 static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU; 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 uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; 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) { if (code == 0) @@ -107,7 +126,7 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vector encode_pronto(const std::string &str) { size_t len = str.length() / (DIGITS_IN_PRONTO_NUMBER + 1) + 1; std::vector data; 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! p = *endptr; } + + return data; +} + +void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &str) { + std::vector data = encode_pronto(str); send_pronto_(dst, data); } void ProntoProtocol::encode(RemoteTransmitData *dst, const ProntoData &data) { send_pronto_(dst, data.data); } -optional 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 *data, uint16_t timebase) { + std::string out; + + for (std::vector::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 ProntoProtocol::decode(RemoteReceiveData src) { + ProntoData out; + + uint16_t frequency = 38000U; + std::vector *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()); } diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h index e96511383f..291bb8a99b 100644 --- a/esphome/components/remote_base/pronto_protocol.h +++ b/esphome/components/remote_base/pronto_protocol.h @@ -6,10 +6,12 @@ namespace esphome { namespace remote_base { +std::vector encode_pronto(const std::string &str); + struct ProntoData { std::string data; - bool operator==(const ProntoData &rhs) const { return data == rhs.data; } + bool operator==(const ProntoData &rhs) const; }; class ProntoProtocol : public RemoteProtocol { @@ -17,6 +19,14 @@ class ProntoProtocol : public RemoteProtocol { void send_pronto_(RemoteTransmitData *dst, const std::vector &data); 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 *data, uint16_t timebase); + public: void encode(RemoteTransmitData *dst, const ProntoData &data) override; optional decode(RemoteReceiveData src) override; diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 8603072bd5..103b7a255d 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -33,14 +33,8 @@ void SCD30Component::setup() { #endif /// 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]; - - if (!this->read_data_(raw_firmware_version, 3)) { + if (!this->get_register(SCD30_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 3)) { this->error_code_ = FIRMWARE_IDENTIFICATION_FAILED; this->mark_failed(); return; @@ -49,7 +43,7 @@ void SCD30Component::setup() { uint16_t(raw_firmware_version[0] & 0xFF)); 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."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -69,7 +63,7 @@ void SCD30Component::setup() { delay(30); #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."); this->error_code_ = MEASUREMENT_INIT_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 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."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -92,7 +86,7 @@ void SCD30Component::setup() { delay(30); #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."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -103,7 +97,7 @@ void SCD30Component::setup() { #endif /// 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."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -151,14 +145,14 @@ void SCD30Component::dump_config() { } void SCD30Component::update() { - uint16_t raw_read_status[1]; - if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + uint16_t raw_read_status; + if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { this->status_set_warning(); ESP_LOGW(TAG, "Data not ready yet!"); return; } - if (!this->write_command_(SCD30_CMD_READ_MEASUREMENT)) { + if (!this->write_command(SCD30_CMD_READ_MEASUREMENT)) { ESP_LOGW(TAG, "Error reading measurement!"); this->status_set_warning(); return; @@ -166,7 +160,7 @@ void SCD30Component::update() { this->set_timeout(50, [this]() { uint16_t raw_data[6]; - if (!this->read_data_(raw_data, 6)) { + if (!this->read_data(raw_data, 6)) { this->status_set_warning(); return; } @@ -197,77 +191,16 @@ void SCD30Component::update() { } 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; } delay(4); uint16_t is_data_ready; - if (!this->read_data_(&is_data_ready, 1)) { + if (!this->read_data(&is_data_ready, 1)) { return false; } 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 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 esphome diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index 64193d0cb6..c434bf0dea 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -2,13 +2,13 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace scd30 { /// 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: void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } 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; } 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_(); enum ErrorCode { diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index cd25649f2a..3cfd861a63 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -2,6 +2,7 @@ from esphome import core import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor +from esphome.components import sensirion_common from esphome.const import ( CONF_ID, CONF_HUMIDITY, @@ -18,9 +19,12 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] 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_ALTITUDE_COMPENSATION = "altitude_compensation" diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 4bd512394f..559c95df32 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -25,15 +25,8 @@ void SCD4XComponent::setup() { // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { - // Check if measurement is ready before reading the value - if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_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)) { + uint16_t raw_read_status; + if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) { ESP_LOGE(TAG, "Failed to read data ready status"); this->mark_failed(); return; @@ -41,9 +34,9 @@ void SCD4XComponent::setup() { uint32_t stop_measurement_delay = 0; // 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"); - if (!this->write_command_(SCD4X_CMD_STOP_MEASUREMENTS)) { + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { ESP_LOGE(TAG, "Failed to stop measurements"); this->mark_failed(); return; @@ -53,15 +46,8 @@ void SCD4XComponent::setup() { stop_measurement_delay = 500; } 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]; - 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"); this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_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), uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8)); - if (!this->write_command_(SCD4X_CMD_TEMPERATURE_OFFSET, - (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { + if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET, + (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { ESP_LOGE(TAG, "Error setting temperature offset."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -88,7 +74,7 @@ void SCD4XComponent::setup() { return; } } 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."); this->error_code_ = MEASUREMENT_INIT_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."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -104,7 +90,7 @@ void SCD4XComponent::setup() { } // 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."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -164,19 +150,19 @@ void SCD4XComponent::update() { } // 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(); return; } - uint16_t raw_read_status[1]; - if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + uint16_t raw_read_status; + if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { this->status_set_warning(); ESP_LOGW(TAG, "Data not ready yet!"); return; } - if (!this->write_command_(SCD4X_CMD_READ_MEASUREMENT)) { + if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { ESP_LOGW(TAG, "Error reading measurement!"); this->status_set_warning(); return; @@ -184,7 +170,7 @@ void SCD4XComponent::update() { // Read off sensor data uint16_t raw_data[3]; - if (!this->read_data_(raw_data, 3)) { + if (!this->read_data(raw_data, 3)) { this->status_set_warning(); 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) { - 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); return true; } 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 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 esphome diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 4fe2bf14cc..3e534bcf98 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -2,14 +2,14 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace scd4x { 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: float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; @@ -27,10 +27,6 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } 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); ERRORCODE error_code_; diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 3e814ffe78..6ab0e1ba99 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor - +from esphome.components import sensirion_common from esphome.const import ( CONF_ID, CONF_CO2, @@ -21,9 +21,12 @@ from esphome.const import ( CODEOWNERS = ["@sjtrny"] DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] 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_ALTITUDE_COMPENSATION = "altitude_compensation" diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp index eb1543f2c2..251d59607a 100644 --- a/esphome/components/sdp3x/sdp3x.cpp +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -7,55 +7,50 @@ namespace esphome { namespace sdp3x { static const char *const TAG = "sdp3x.sensor"; -static const uint8_t SDP3X_SOFT_RESET[2] = {0x00, 0x06}; -static const uint8_t SDP3X_READ_ID1[2] = {0x36, 0x7C}; -static const uint8_t SDP3X_READ_ID2[2] = {0xE1, 0x02}; -static const uint8_t SDP3X_START_DP_AVG[2] = {0x36, 0x15}; -static const uint8_t SDP3X_START_MASS_FLOW_AVG[2] = {0x36, 0x03}; -static const uint8_t SDP3X_STOP_MEAS[2] = {0x3F, 0xF9}; +static const uint16_t SDP3X_SOFT_RESET = 0x0006; +static const uint16_t SDP3X_READ_ID1 = 0x367C; +static const uint16_t SDP3X_READ_ID2 = 0xE102; +static const uint16_t SDP3X_START_DP_AVG = 0x3615; +static const uint16_t SDP3X_START_MASS_FLOW_AVG = 0x3603; +static const uint16_t SDP3X_STOP_MEAS = 0x3FF9; void SDP3XComponent::update() { this->read_pressure_(); } void SDP3XComponent::setup() { 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 } - 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 } 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!"); this->mark_failed(); 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!"); this->mark_failed(); return; } - uint8_t data[18]; - if (this->read(data, 18) != i2c::ERROR_OK) { + uint16_t data[6]; + if (this->read_data(data, 6) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Read ID SDP3X failed!"); this->mark_failed(); 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 // ref: // 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) { - switch (data[3]) { + if (data[1] >> 8 == 0x02) { + switch (data[1] & 0xFF) { case 0x01: // SDP800-500Pa ESP_LOGCONFIG(TAG, "Sensor is SDP800-500Pa"); break; @@ -75,15 +70,16 @@ void SDP3XComponent::setup() { ESP_LOGCONFIG(TAG, "Sensor is SDP810-125Pa"); break; } - } else if (data[2] == 0x01) { - if (data[3] == 0x01) { + } else if (data[1] >> 8 == 0x01) { + if ((data[1] & 0xFF) == 0x01) { 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"); } } - 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!"); this->mark_failed(); return; @@ -101,22 +97,16 @@ void SDP3XComponent::dump_config() { } void SDP3XComponent::read_pressure_() { - uint8_t data[9]; - if (this->read(data, 9) != i2c::ERROR_OK) { + uint16_t data[3]; + if (this->read_data(data, 3) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Couldn't read SDP3X data!"); this->status_set_warning(); return; } - if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]) && check_crc_(&data[6], 2, data[8]))) { - ESP_LOGW(TAG, "Invalid SDP3X data!"); - this->status_set_warning(); - 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]); + int16_t pressure_raw = data[0]; + int16_t temperature_raw = data[1]; + int16_t scale_factor_raw = data[2]; // scale factor is in Pa - convert to hPa 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, @@ -129,26 +119,5 @@ void SDP3XComponent::read_pressure_() { 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 esphome diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h index 0e74d0883d..e3d3533c74 100644 --- a/esphome/components/sdp3x/sdp3x.h +++ b/esphome/components/sdp3x/sdp3x.h @@ -2,14 +2,14 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sdp3x { 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: /// Schedule temperature+pressure readings. void update() override; @@ -23,8 +23,6 @@ class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public se protected: /// Internal method to read the pressure from the component after it has been scheduled. void read_pressure_(); - - bool check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum); MeasurementMode measurement_mode_; }; diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py index 66ee475b11..60608818a2 100644 --- a/esphome/components/sdp3x/sensor.py +++ b/esphome/components/sdp3x/sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor +from esphome.components import sensirion_common from esphome.const import ( DEVICE_CLASS_PRESSURE, STATE_CLASS_MEASUREMENT, @@ -8,10 +9,13 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] CODEOWNERS = ["@Azimath"] 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") diff --git a/esphome/components/sensirion_common/__init__.py b/esphome/components/sensirion_common/__init__.py new file mode 100644 index 0000000000..b27f37099d --- /dev/null +++ b/esphome/components/sensirion_common/__init__.py @@ -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) diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp new file mode 100644 index 0000000000..a2232a7d1b --- /dev/null +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -0,0 +1,128 @@ +#include "i2c_sensirion.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +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 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 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(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 diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h new file mode 100644 index 0000000000..88e1d59984 --- /dev/null +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -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 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 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 arguments for the i2c command + * @return true if reading succeded + */ + template bool write_command(T i2c_register, const std::vector &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 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 diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 65ae7b2168..0c38ceeb37 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -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.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), - cv.Optional(CONF_ABOVE): cv.float_, - cv.Optional(CONF_BELOW): cv.float_, + cv.Optional(CONF_ABOVE): cv.templatable(cv.float_), + cv.Optional(CONF_BELOW): cv.templatable(cv.float_), }, cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), ), diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 14a078b501..0029e2c515 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -1,10 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common + from esphome.const import ( CONF_ID, CONF_BASELINE, CONF_ECO2, + CONF_STORE_BASELINE, + CONF_TEMPERATURE_SOURCE, CONF_TVOC, ICON_RADIATOR, DEVICE_CLASS_CARBON_DIOXIDE, @@ -17,17 +20,19 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] 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_TVOC_BASELINE = "tvoc_baseline" -CONF_STORE_BASELINE = "store_baseline" CONF_UPTIME = "uptime" CONF_COMPENSATION = "compensation" CONF_HUMIDITY_SOURCE = "humidity_source" -CONF_TEMPERATURE_SOURCE = "temperature_source" + CONFIG_SCHEMA = ( cv.Schema( diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index b55097fcd0..a6572620c4 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -36,14 +36,8 @@ void SGP30Component::setup() { ESP_LOGCONFIG(TAG, "Setting up SGP30..."); // 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]; - - if (!this->read_data_(raw_serial_number, 3)) { + if (!this->get_register(SGP30_CMD_GET_SERIAL_ID, raw_serial_number, 3)) { this->mark_failed(); return; } @@ -52,16 +46,12 @@ void SGP30Component::setup() { ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); // 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(); return; } - uint16_t raw_featureset[1]; - if (!this->read_data_(raw_featureset, 1)) { - this->mark_failed(); - return; - } - this->featureset_ = raw_featureset[0]; + this->featureset_ = raw_featureset; if (uint16_t(this->featureset_ >> 12) != 0x0) { if (uint16_t(this->featureset_ >> 12) == 0x1) { // 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)); // 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."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -119,14 +109,14 @@ bool SGP30Component::is_sensor_baseline_reliable_() { void SGP30Component::read_iaq_baseline_() { 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"); this->status_set_warning(); return; } this->set_timeout(50, [this]() { uint16_t raw_data[2]; - if (!this->read_data_(raw_data, 2)) { + if (!this->read_data(raw_data, 2)) { this->status_set_warning(); return; } @@ -274,14 +264,14 @@ void SGP30Component::dump_config() { } void SGP30Component::update() { - if (!this->write_command_(SGP30_CMD_MEASURE_IAQ)) { + if (!this->write_command(SGP30_CMD_MEASURE_IAQ)) { this->status_set_warning(); return; } this->seconds_since_last_store_ += this->update_interval_ / 1000; this->set_timeout(50, [this]() { uint16_t raw_data[2]; - if (!this->read_data_(raw_data, 2)) { + if (!this->read_data(raw_data, 2)) { this->status_set_warning(); 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 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 esphome diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 91a1c1e9c7..d61eee00db 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -2,7 +2,7 @@ #include "esphome/core/component.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 @@ -15,7 +15,7 @@ struct SGP30Baselines { } PACKED; /// 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: void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } 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; } protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); void send_env_data_(); void read_iaq_baseline_(); bool is_sensor_baseline_reliable_(); 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_; uint16_t featureset_; uint32_t required_warm_up_time_; diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index 6f27b54fb0..ee267d6062 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -1,25 +1,30 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common + from esphome.const import ( + CONF_STORE_BASELINE, + CONF_TEMPERATURE_SOURCE, ICON_RADIATOR, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] CODEOWNERS = ["@SenexCrenshaw"] sgp40_ns = cg.esphome_ns.namespace("sgp40") SGP40Component = sgp40_ns.class_( - "SGP40Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice + "SGP40Component", + sensor.Sensor, + cg.PollingComponent, + sensirion_common.SensirionI2CDevice, ) CONF_COMPENSATION = "compensation" CONF_HUMIDITY_SOURCE = "humidity_source" -CONF_TEMPERATURE_SOURCE = "temperature_source" -CONF_STORE_BASELINE = "store_baseline" CONF_VOC_BASELINE = "voc_baseline" CONFIG_SCHEMA = ( diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp index 829c00a218..9d78572b50 100644 --- a/esphome/components/sgp40/sgp40.cpp +++ b/esphome/components/sgp40/sgp40.cpp @@ -12,14 +12,14 @@ void SGP40Component::setup() { ESP_LOGCONFIG(TAG, "Setting up SGP40..."); // 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->mark_failed(); return; } 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(); return; } @@ -28,19 +28,19 @@ void SGP40Component::setup() { ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); // 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"); this->mark_failed(); return; } - uint16_t raw_featureset[1]; - if (!this->read_data_(raw_featureset, 1)) { + uint16_t raw_featureset; + if (!this->read_data(raw_featureset)) { ESP_LOGD(TAG, "raw_featureset read_data_ failed"); this->mark_failed(); return; } - this->featureset_ = raw_featureset[0]; + this->featureset_ = raw_featureset; if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) { ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF), SGP40_FEATURESET); @@ -95,21 +95,21 @@ void SGP40Component::setup() { void SGP40Component::self_test_() { 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; ESP_LOGD(TAG, "Self-test communication failed"); this->mark_failed(); } this->set_timeout(250, [this]() { - uint16_t reply[1]; - if (!this->read_data_(reply, 1)) { + uint16_t reply; + if (!this->read_data(reply)) { ESP_LOGD(TAG, "Self-test read_data_ failed"); this->mark_failed(); return; } - if (reply[0] == 0xD400) { + if (reply == 0xD400) { this->self_test_complete_ = true; ESP_LOGD(TAG, "Self-test completed"); return; @@ -192,51 +192,28 @@ uint16_t SGP40Component::measure_raw_() { temperature = 25; } - uint8_t command[8]; - - command[0] = 0x26; - command[1] = 0x0F; - + uint16_t data[2]; 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); - command[5] = tempticks >> 8; - command[6] = tempticks & 0xFF; - command[7] = generate_crc_(command + 5, 2); + // first paramater is the relative humidity ticks + data[0] = rhticks; + // 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(); - ESP_LOGD(TAG, "write error"); - return UINT16_MAX; + ESP_LOGD(TAG, "write error (%d)", this->last_error_); + return false; } 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(); ESP_LOGD(TAG, "read_data_ error"); return UINT16_MAX; } - return raw_data[0]; -} - -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; + return raw_data; } 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 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 esphome diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h index c854b21060..c5b7d2dfa0 100644 --- a/esphome/components/sgp40/sgp40.h +++ b/esphome/components/sgp40/sgp40.h @@ -2,7 +2,7 @@ #include "esphome/core/component.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/preferences.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_FEATURESET = 0x202f; 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. // Prevents wear of the flash because of too many write operations @@ -39,7 +40,7 @@ const uint32_t MAXIMUM_STORAGE_DIFF = 50; class SGP40Component; /// 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: void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } 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. sensor::Sensor *humidity_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 sgp40_probe_(); - uint8_t sht_crc_(uint8_t data1, uint8_t data2); uint64_t serial_number_; uint16_t featureset_; int32_t measure_voc_index_(); diff --git a/esphome/components/sht3xd/sensor.py b/esphome/components/sht3xd/sensor.py index b9e7bce733..8e1ef426ad 100644 --- a/esphome/components/sht3xd/sensor.py +++ b/esphome/components/sht3xd/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( CONF_HUMIDITY, CONF_ID, @@ -13,10 +13,11 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] sht3xd_ns = cg.esphome_ns.namespace("sht3xd") SHT3XDComponent = sht3xd_ns.class_( - "SHT3XDComponent", cg.PollingComponent, i2c.I2CDevice + "SHT3XDComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice ) CONFIG_SCHEMA = ( diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index e7981b64cf..4e1c9742bc 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -17,13 +17,8 @@ static const uint16_t SHT3XD_COMMAND_FETCH_DATA = 0xE000; void SHT3XDComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SHT3xD..."); - if (!this->write_command_(SHT3XD_COMMAND_READ_SERIAL_NUMBER)) { - this->mark_failed(); - return; - } - uint16_t raw_serial_number[2]; - if (!this->read_data_(raw_serial_number, 2)) { + if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2)) { this->mark_failed(); return; } @@ -45,16 +40,16 @@ float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; void SHT3XDComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); - this->write_command_(SHT3XD_COMMAND_SOFT_RESET); + this->write_command(SHT3XD_COMMAND_SOFT_RESET); } - if (!this->write_command_(SHT3XD_COMMAND_POLLING_H)) { + if (!this->write_command(SHT3XD_COMMAND_POLLING_H)) { this->status_set_warning(); return; } this->set_timeout(50, [this]() { uint16_t raw_data[2]; - if (!this->read_data_(raw_data, 2)) { + if (!this->read_data(raw_data, 2)) { this->status_set_warning(); return; } @@ -71,56 +66,5 @@ void SHT3XDComponent::update() { }); } -bool SHT3XDComponent::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 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 SHT3XDComponent::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector 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 sht3xd } // namespace esphome diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 709f8aebe7..3164aa0687 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -2,13 +2,13 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sht3xd { /// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors. -class SHT3XDComponent : public PollingComponent, public i2c::I2CDevice { +class SHT3XDComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -19,9 +19,6 @@ class SHT3XDComponent : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); - sensor::Sensor *temperature_sensor_; sensor::Sensor *humidity_sensor_; }; diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py index a66ca1a526..9fb8fc969e 100644 --- a/esphome/components/sht4x/sensor.py +++ b/esphome/components/sht4x/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( CONF_ID, CONF_TEMPERATURE, @@ -16,10 +16,13 @@ from esphome.const import ( CODEOWNERS = ["@sjtrny"] DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] sht4x_ns = cg.esphome_ns.namespace("sht4x") -SHT4XComponent = sht4x_ns.class_("SHT4XComponent", cg.PollingComponent, i2c.I2CDevice) +SHT4XComponent = sht4x_ns.class_( + "SHT4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) CONF_PRECISION = "precision" SHT4XPRECISION = sht4x_ns.enum("SHT4XPRECISION") diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 248f32c4de..bdc3e62d2f 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -50,31 +50,28 @@ void SHT4XComponent::setup() { void SHT4XComponent::dump_config() { LOG_I2C_DEVICE(this); } void SHT4XComponent::update() { - uint8_t cmd[] = {MEASURECOMMANDS[this->precision_]}; - // Send command - this->write(cmd, 1); + this->write_command(MEASURECOMMANDS[this->precision_]); this->set_timeout(10, [this]() { - const uint8_t num_bytes = 6; - uint8_t buffer[num_bytes]; + uint16_t buffer[2]; // Read measurement - bool read_status = this->read_bytes_raw(buffer, num_bytes); + bool read_status = this->read_data(buffer, 2); if (read_status) { // Evaluate and publish measurements if (this->temp_sensor_ != nullptr) { - // Temp is contained in the first 16 bits - float sensor_value_temp = (buffer[0] << 8) + buffer[1]; + // Temp is contained in the first result word + float sensor_value_temp = buffer[0]; float temp = -45 + 175 * sensor_value_temp / 65535; this->temp_sensor_->publish_state(temp); } if (this->humidity_sensor_ != nullptr) { - // Relative humidity is in the last 16 bits - float sensor_value_rh = (buffer[3] << 8) + buffer[4]; + // Relative humidity is in the second result word + float sensor_value_rh = buffer[1]; float rh = -6 + 125 * sensor_value_rh / 65535; this->humidity_sensor_->publish_state(rh); diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index 8694bd9879..01553d5c15 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sht4x { @@ -13,7 +13,7 @@ enum SHT4XHEATERPOWER { SHT4X_HEATERPOWER_HIGH, SHT4X_HEATERPOWER_MED, SHT4X_HEA enum SHT4XHEATERTIME { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME_SHORT = 110 }; -class SHT4XComponent : public PollingComponent, public i2c::I2CDevice { +class SHT4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py index ba2283a9b4..c8b56cfe30 100644 --- a/esphome/components/shtcx/sensor.py +++ b/esphome/components/shtcx/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( CONF_HUMIDITY, CONF_ID, @@ -13,9 +13,12 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] shtcx_ns = cg.esphome_ns.namespace("shtcx") -SHTCXComponent = shtcx_ns.class_("SHTCXComponent", cg.PollingComponent, i2c.I2CDevice) +SHTCXComponent = shtcx_ns.class_( + "SHTCXComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) SHTCXType = shtcx_ns.enum("SHTCXType") diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index 867c26df1d..0de56a8044 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -29,21 +29,23 @@ void SHTCXComponent::setup() { this->wake_up(); this->soft_reset(); - if (!this->write_command_(SHTCX_COMMAND_READ_ID_REGISTER)) { + if (!this->write_command(SHTCX_COMMAND_READ_ID_REGISTER)) { ESP_LOGE(TAG, "Error requesting Device ID"); this->mark_failed(); return; } - uint16_t device_id_register[1]; - if (!this->read_data_(device_id_register, 1)) { + uint16_t device_id_register; + if (!this->read_data(&device_id_register, 1)) { ESP_LOGE(TAG, "Error reading Device ID"); this->mark_failed(); return; } - if (((device_id_register[0] << 2) & 0x1C) == 0x1C) { - if ((device_id_register[0] & 0x847) == 0x847) { + this->sensor_id_ = device_id_register; + + if ((device_id_register & 0x3F) == 0x07) { + if (device_id_register & 0x800) { this->type_ = SHTCX_TYPE_SHTC3; } else { this->type_ = SHTCX_TYPE_SHTC1; @@ -51,11 +53,11 @@ void SHTCXComponent::setup() { } else { this->type_ = SHTCX_TYPE_UNKNOWN; } - ESP_LOGCONFIG(TAG, " Device identified: %s", to_string(this->type_)); + ESP_LOGCONFIG(TAG, " Device identified: %s (%04x)", to_string(this->type_), device_id_register); } void SHTCXComponent::dump_config() { ESP_LOGCONFIG(TAG, "SHTCx:"); - ESP_LOGCONFIG(TAG, " Model: %s", to_string(this->type_)); + ESP_LOGCONFIG(TAG, " Model: %s (%04x)", to_string(this->type_), this->sensor_id_); LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, "Communication with SHTCx failed!"); @@ -74,22 +76,29 @@ void SHTCXComponent::update() { if (this->type_ != SHTCX_TYPE_SHTC1) { this->wake_up(); } - if (!this->write_command_(SHTCX_COMMAND_POLLING_H)) { + if (!this->write_command(SHTCX_COMMAND_POLLING_H)) { + ESP_LOGE(TAG, "sensor polling failed"); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(NAN); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(NAN); this->status_set_warning(); return; } this->set_timeout(50, [this]() { + float temperature = NAN; + float humidity = NAN; uint16_t raw_data[2]; - if (!this->read_data_(raw_data, 2)) { + if (!this->read_data(raw_data, 2)) { + ESP_LOGE(TAG, "sensor read failed"); this->status_set_warning(); - return; + } else { + temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f; + humidity = 100.0f * float(raw_data[1]) / 65536.0f; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); } - - float temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f; - float humidity = 100.0f * float(raw_data[1]) / 65536.0f; - - ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->humidity_sensor_ != nullptr) @@ -101,65 +110,14 @@ void SHTCXComponent::update() { }); } -bool SHTCXComponent::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 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 SHTCXComponent::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector 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; -} - void SHTCXComponent::soft_reset() { - this->write_command_(SHTCX_COMMAND_SOFT_RESET); + this->write_command(SHTCX_COMMAND_SOFT_RESET); delayMicroseconds(200); } -void SHTCXComponent::sleep() { this->write_command_(SHTCX_COMMAND_SLEEP); } +void SHTCXComponent::sleep() { this->write_command(SHTCX_COMMAND_SLEEP); } void SHTCXComponent::wake_up() { - this->write_command_(SHTCX_COMMAND_WAKEUP); + this->write_command(SHTCX_COMMAND_WAKEUP); delayMicroseconds(200); } diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h index ccc6533bfa..c44fb9d9c1 100644 --- a/esphome/components/shtcx/shtcx.h +++ b/esphome/components/shtcx/shtcx.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace shtcx { @@ -10,7 +10,7 @@ namespace shtcx { enum SHTCXType { SHTCX_TYPE_SHTC3 = 0, SHTCX_TYPE_SHTC1, SHTCX_TYPE_UNKNOWN }; /// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors. -class SHTCXComponent : public PollingComponent, public i2c::I2CDevice { +class SHTCXComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -24,9 +24,8 @@ class SHTCXComponent : public PollingComponent, public i2c::I2CDevice { void wake_up(); protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); SHTCXType type_; + uint16_t sensor_id_; sensor::Sensor *temperature_sensor_; sensor::Sensor *humidity_sensor_; }; diff --git a/esphome/components/sonoff_d1/__init__.py b/esphome/components/sonoff_d1/__init__.py new file mode 100644 index 0000000000..18b4d30d18 --- /dev/null +++ b/esphome/components/sonoff_d1/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@anatoly-savchenkov"] diff --git a/esphome/components/sonoff_d1/light.py b/esphome/components/sonoff_d1/light.py new file mode 100644 index 0000000000..8ffe60224e --- /dev/null +++ b/esphome/components/sonoff_d1/light.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, light +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_MIN_VALUE, + CONF_MAX_VALUE, +) + +CONF_USE_RM433_REMOTE = "use_rm433_remote" + +DEPENDENCIES = ["uart", "light"] + +sonoff_d1_ns = cg.esphome_ns.namespace("sonoff_d1") +SonoffD1Output = sonoff_d1_ns.class_( + "SonoffD1Output", cg.Component, uart.UARTDevice, light.LightOutput +) + +CONFIG_SCHEMA = ( + light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SonoffD1Output), + cv.Optional(CONF_USE_RM433_REMOTE, default=False): cv.boolean, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_range(min=0, max=100), + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_range(min=0, max=100), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "sonoff_d1", baud_rate=9600, require_tx=True, require_rx=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add(var.set_use_rm433_remote(config[CONF_USE_RM433_REMOTE])) + cg.add(var.set_min_value(config[CONF_MIN_VALUE])) + cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + await light.register_light(var, config) diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp new file mode 100644 index 0000000000..b4bcbc6760 --- /dev/null +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -0,0 +1,308 @@ +/* + sonoff_d1.cpp - Sonoff D1 Dimmer support for ESPHome + + Copyright Âİ 2021 Anatoly Savchenkov + Copyright Âİ 2020 Jeff Rescignano + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the “Software”), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom + the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ----- + + If modifying this file, in addition to the license above, please ensure to include links back to the original code: + https://jeffresc.dev/blog/2020-10-10 + https://github.com/JeffResc/Sonoff-D1-Dimmer + https://github.com/arendst/Tasmota/blob/2d4a6a29ebc7153dbe2717e3615574ac1c84ba1d/tasmota/xdrv_37_sonoff_d1.ino#L119-L131 + + ----- +*/ + +/*********************************************************************************************\ + * Sonoff D1 dimmer 433 + * Mandatory/Optional + * ^ 0 1 2 3 4 5 6 7 8 9 A B C D E F 10 + * M AA 55 - Header + * M 01 04 - Version? + * M 00 0A - Following data length (10 bytes) + * O 01 - Power state (00 = off, 01 = on, FF = ignore) + * O 64 - Dimmer percentage (01 to 64 = 1 to 100%, 0 - ignore) + * O FF FF FF FF FF FF FF FF - Not used + * M 6C - CRC over bytes 2 to F (Addition) +\*********************************************************************************************/ +#include +#include "sonoff_d1.h" + +namespace esphome { +namespace sonoff_d1 { + +static const char *const TAG = "sonoff_d1"; + +uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) { + uint8_t crc = 0; + for (int i = 2; i < len - 1; i++) { + crc += cmd[i]; + } + return crc; +} + +void SonoffD1Output::populate_checksum_(uint8_t *cmd, const size_t len) { + // Update the checksum + cmd[len - 1] = this->calc_checksum_(cmd, len); +} + +void SonoffD1Output::skip_command_() { + size_t garbage = 0; + // Read out everything from the UART FIFO + while (this->available()) { + uint8_t value = this->read(); + ESP_LOGW(TAG, "[%04d] Skip %02d: 0x%02x from the dimmer", this->write_count_, garbage, value); + garbage++; + } + + // Warn about unexpected bytes in the protocol with UART dimmer + if (garbage) + ESP_LOGW(TAG, "[%04d] Skip %d bytes from the dimmer", this->write_count_, garbage); +} + +// This assumes some data is already available +bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { + // Do consistency check + if (cmd == nullptr || len < 7) { + ESP_LOGW(TAG, "[%04d] Too short command buffer (actual len is %d bytes, minimal is 7)", this->write_count_, len); + return false; + } + + // Read a minimal packet + if (this->read_array(cmd, 6)) { + ESP_LOGV(TAG, "[%04d] Reading from dimmer:", this->write_count_); + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, 6).c_str()); + + if (cmd[0] != 0xAA || cmd[1] != 0x55) { + ESP_LOGW(TAG, "[%04d] RX: wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]); + this->skip_command_(); + return false; + } + if ((cmd[5] + 7 /*mandatory header + crc suffix length*/) > len) { + ESP_LOGW(TAG, "[%04d] RX: Payload length is unexpected (%d, max expected %d)", this->write_count_, cmd[5], + len - 7); + this->skip_command_(); + return false; + } + if (this->read_array(&cmd[6], cmd[5] + 1 /*checksum suffix*/)) { + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(&cmd[6], cmd[5] + 1).c_str()); + + // Check the checksum + uint8_t valid_checksum = this->calc_checksum_(cmd, cmd[5] + 7); + if (valid_checksum != cmd[cmd[5] + 7 - 1]) { + ESP_LOGW(TAG, "[%04d] RX: checksum mismatch (%d, expected %d)", this->write_count_, cmd[cmd[5] + 7 - 1], + valid_checksum); + this->skip_command_(); + return false; + } + len = cmd[5] + 7 /*mandatory header + suffix length*/; + + // Read remaining gardbled data (just in case, I don't see where this can appear now) + this->skip_command_(); + return true; + } + } else { + ESP_LOGW(TAG, "[%04d] RX: feedback timeout", this->write_count_); + this->skip_command_(); + } + return false; +} + +bool SonoffD1Output::read_ack_(const uint8_t *cmd, const size_t len) { + // Expected acknowledgement from rf chip + uint8_t ref_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00}; + uint8_t buffer[sizeof(ref_buffer)] = {0}; + uint32_t pos = 0, buf_len = sizeof(ref_buffer); + + // Update the reference checksum + this->populate_checksum_(ref_buffer, sizeof(ref_buffer)); + + // Read ack code, this either reads 7 bytes or exits with a timeout + this->read_command_(buffer, buf_len); + + // Compare response with expected response + while (pos < sizeof(ref_buffer) && ref_buffer[pos] == buffer[pos]) { + pos++; + } + if (pos == sizeof(ref_buffer)) { + ESP_LOGD(TAG, "[%04d] Acknowledge received", this->write_count_); + return true; + } else { + ESP_LOGW(TAG, "[%04d] Unexpected acknowledge received (possible clash of RF/HA commands), expected ack was:", + this->write_count_); + ESP_LOGW(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(ref_buffer, sizeof(ref_buffer)).c_str()); + } + return false; +} + +bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_ack) { + // Do some consistency checks + if (len < 7) { + ESP_LOGW(TAG, "[%04d] Too short command (actual len is %d bytes, minimal is 7)", this->write_count_, len); + return false; + } + if (cmd[0] != 0xAA || cmd[1] != 0x55) { + ESP_LOGW(TAG, "[%04d] Wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]); + return false; + } + if ((cmd[5] + 7 /*mandatory header + suffix length*/) != len) { + ESP_LOGW(TAG, "[%04d] Payload length field does not match packet lenght (%d, expected %d)", this->write_count_, + cmd[5], len - 7); + return false; + } + this->populate_checksum_(cmd, len); + + // Need retries here to handle the following cases: + // 1. On power up companion MCU starts to respond with a delay, so few first commands are ignored + // 2. UART command initiated by this component can clash with a command initiated by RF + uint32_t retries = 10; + do { + ESP_LOGV(TAG, "[%04d] Writing to the dimmer:", this->write_count_); + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, len).c_str()); + this->write_array(cmd, len); + this->write_count_++; + if (!needs_ack) + return true; + retries--; + } while (!this->read_ack_(cmd, len) && retries > 0); + + if (retries) { + return true; + } else { + ESP_LOGE(TAG, "[%04d] Unable to write to the dimmer", this->write_count_); + } + return false; +} + +bool SonoffD1Output::control_dimmer_(const bool binary, const uint8_t brightness) { + // Include our basic code from the Tasmota project, thank you again! + // 0 1 2 3 4 5 6 7 8 + uint8_t cmd[17] = {0xAA, 0x55, 0x01, 0x04, 0x00, 0x0A, 0x00, 0x00, 0xFF, + // 9 10 11 12 13 14 15 16 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00}; + + cmd[6] = binary; + cmd[7] = remap(brightness, 0, 100, this->min_value_, this->max_value_); + ESP_LOGI(TAG, "[%04d] Setting dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(binary), cmd[7]); + return this->write_command_(cmd, sizeof(cmd)); +} + +void SonoffD1Output::process_command_(const uint8_t *cmd, const size_t len) { + if (cmd[2] == 0x01 && cmd[3] == 0x04 && cmd[4] == 0x00 && cmd[5] == 0x0A) { + uint8_t ack_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00}; + // Ack a command from RF to ESP to prevent repeating commands + this->write_command_(ack_buffer, sizeof(ack_buffer), false); + ESP_LOGI(TAG, "[%04d] RF sets dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(cmd[6]), cmd[7]); + const uint8_t new_brightness = remap(cmd[7], this->min_value_, this->max_value_, 0, 100); + const bool new_state = cmd[6]; + + // Got light change state command. In all cases we revert the command immediately + // since we want to rely on ESP controlled transitions + if (new_state != this->last_binary_ || new_brightness != this->last_brightness_) { + this->control_dimmer_(this->last_binary_, this->last_brightness_); + } + + if (!this->use_rm433_remote_) { + // If RF remote is not used, this is a known ghost RF command + ESP_LOGI(TAG, "[%04d] Ghost command from RF detected, reverted", this->write_count_); + } else { + // If remote is used, initiate transition to the new state + this->publish_state_(new_state, new_brightness); + } + } else { + ESP_LOGW(TAG, "[%04d] Unexpected command received", this->write_count_); + } +} + +void SonoffD1Output::publish_state_(const bool is_on, const uint8_t brightness) { + if (light_state_) { + ESP_LOGV(TAG, "Publishing new state: %s, brightness=%d", ONOFF(is_on), brightness); + auto call = light_state_->make_call(); + call.set_state(is_on); + if (brightness != 0) { + // Brightness equal to 0 has a special meaning. + // D1 uses 0 as "previously set brightness". + // Usually zero brightness comes inside light ON command triggered by RF remote. + // Since we unconditionally override commands coming from RF remote in process_command_(), + // here we mimic the original behavior but with LightCall functionality + call.set_brightness((float) brightness / 100.0f); + } + call.perform(); + } +} + +// Set the device's traits +light::LightTraits SonoffD1Output::get_traits() { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + return traits; +} + +void SonoffD1Output::write_state(light::LightState *state) { + bool binary; + float brightness; + + // Fill our variables with the device's current state + state->current_values_as_binary(&binary); + state->current_values_as_brightness(&brightness); + + // Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100) + const uint8_t calculated_brightness = std::round(brightness * 100); + + if (calculated_brightness == 0) { + // if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness"); + binary = false; + } + + // If a new value, write to the dimmer + if (binary != this->last_binary_ || calculated_brightness != this->last_brightness_) { + if (this->control_dimmer_(binary, calculated_brightness)) { + this->last_brightness_ = calculated_brightness; + this->last_binary_ = binary; + } else { + // Return to original value if failed to write to the dimmer + // TODO: Test me, can be tested if high-voltage part is not connected + ESP_LOGW(TAG, "Failed to update the dimmer, publishing the previous state"); + this->publish_state_(this->last_binary_, this->last_brightness_); + } + } +} + +void SonoffD1Output::dump_config() { + ESP_LOGCONFIG(TAG, "Sonoff D1 Dimmer: '%s'", this->light_state_ ? this->light_state_->get_name().c_str() : ""); + ESP_LOGCONFIG(TAG, " Use RM433 Remote: %s", ONOFF(this->use_rm433_remote_)); + ESP_LOGCONFIG(TAG, " Minimal brightness: %d", this->min_value_); + ESP_LOGCONFIG(TAG, " Maximal brightness: %d", this->max_value_); +} + +void SonoffD1Output::loop() { + // Read commands from the dimmer + // RF chip notifies ESP about remotely changed state with the same commands as we send + if (this->available()) { + ESP_LOGV(TAG, "Have some UART data in loop()"); + uint8_t buffer[17] = {0}; + size_t len = sizeof(buffer); + if (this->read_command_(buffer, len)) { + this->process_command_(buffer, len); + } + } +} + +} // namespace sonoff_d1 +} // namespace esphome diff --git a/esphome/components/sonoff_d1/sonoff_d1.h b/esphome/components/sonoff_d1/sonoff_d1.h new file mode 100644 index 0000000000..4df0f5afb2 --- /dev/null +++ b/esphome/components/sonoff_d1/sonoff_d1.h @@ -0,0 +1,85 @@ +#pragma once + +/* + sonoff_d1.h - Sonoff D1 Dimmer support for ESPHome + + Copyright Âİ 2021 Anatoly Savchenkov + Copyright Âİ 2020 Jeff Rescignano + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the “Software”), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom + the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ----- + + If modifying this file, in addition to the license above, please ensure to include links back to the original code: + https://jeffresc.dev/blog/2020-10-10 + https://github.com/JeffResc/Sonoff-D1-Dimmer + https://github.com/arendst/Tasmota/blob/2d4a6a29ebc7153dbe2717e3615574ac1c84ba1d/tasmota/xdrv_37_sonoff_d1.ino#L119-L131 + + ----- +*/ + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/light/light_output.h" +#include "esphome/components/light/light_state.h" +#include "esphome/components/light/light_traits.h" + +namespace esphome { +namespace sonoff_d1 { + +class SonoffD1Output : public light::LightOutput, public uart::UARTDevice, public Component { + public: + // LightOutput methods + light::LightTraits get_traits() override; + void setup_state(light::LightState *state) override { this->light_state_ = state; } + void write_state(light::LightState *state) override; + + // Component methods + void setup() override{}; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + // Custom methods + void set_use_rm433_remote(const bool use_rm433_remote) { this->use_rm433_remote_ = use_rm433_remote; } + void set_min_value(const uint8_t min_value) { this->min_value_ = min_value; } + void set_max_value(const uint8_t max_value) { this->max_value_ = max_value; } + + protected: + uint8_t min_value_{0}; + uint8_t max_value_{100}; + bool use_rm433_remote_{false}; + bool last_binary_{false}; + uint8_t last_brightness_{0}; + int write_count_{0}; + int read_count_{0}; + light::LightState *light_state_{nullptr}; + + uint8_t calc_checksum_(const uint8_t *cmd, size_t len); + void populate_checksum_(uint8_t *cmd, size_t len); + void skip_command_(); + bool read_command_(uint8_t *cmd, size_t &len); + bool read_ack_(const uint8_t *cmd, size_t len); + bool write_command_(uint8_t *cmd, size_t len, bool needs_ack = true); + bool control_dimmer_(bool binary, uint8_t brightness); + void process_command_(const uint8_t *cmd, size_t len); + void publish_state_(bool is_on, uint8_t brightness); +}; + +} // namespace sonoff_d1 +} // namespace esphome diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 27264cf942..89cb25c24f 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( CONF_ID, CONF_PM_1_0, @@ -26,9 +26,12 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] sps30_ns = cg.esphome_ns.namespace("sps30") -SPS30Component = sps30_ns.class_("SPS30Component", cg.PollingComponent, i2c.I2CDevice) +SPS30Component = sps30_ns.class_( + "SPS30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) CONFIG_SCHEMA = ( cv.Schema( diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 2bd7bcb458..2885125a8a 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -22,30 +22,18 @@ static const uint8_t MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR = 5; void SPS30Component::setup() { ESP_LOGCONFIG(TAG, "Setting up sps30..."); - this->write_command_(SPS30_CMD_SOFT_RESET); + this->write_command(SPS30_CMD_SOFT_RESET); /// Deferred Sensor initialization this->set_timeout(500, [this]() { /// Firmware version identification - if (!this->write_command_(SPS30_CMD_GET_FIRMWARE_VERSION)) { - this->error_code_ = FIRMWARE_VERSION_REQUEST_FAILED; - this->mark_failed(); - return; - } - - if (!this->read_data_(&raw_firmware_version_, 1)) { + if (!this->get_register(SPS30_CMD_GET_FIRMWARE_VERSION, raw_firmware_version_, 1)) { this->error_code_ = FIRMWARE_VERSION_READ_FAILED; this->mark_failed(); return; } /// Serial number identification - if (!this->write_command_(SPS30_CMD_GET_SERIAL_NUMBER)) { - this->error_code_ = SERIAL_NUMBER_REQUEST_FAILED; - this->mark_failed(); - return; - } - uint16_t raw_serial_number[8]; - if (!this->read_data_(raw_serial_number, 8)) { + if (!this->get_register(SPS30_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 1)) { this->error_code_ = SERIAL_NUMBER_READ_FAILED; this->mark_failed(); return; @@ -109,7 +97,7 @@ void SPS30Component::update() { /// Check if warning flag active (sensor reconnected?) if (this->status_has_warning()) { ESP_LOGD(TAG, "Trying to reconnect the sensor..."); - if (this->write_command_(SPS30_CMD_SOFT_RESET)) { + if (this->write_command(SPS30_CMD_SOFT_RESET)) { ESP_LOGD(TAG, "Sensor has soft-reset successfully. Waiting for reconnection in 500ms..."); this->set_timeout(500, [this]() { this->start_continuous_measurement_(); @@ -124,13 +112,13 @@ void SPS30Component::update() { return; } /// Check if measurement is ready before reading the value - if (!this->write_command_(SPS30_CMD_GET_DATA_READY_STATUS)) { + if (!this->write_command(SPS30_CMD_GET_DATA_READY_STATUS)) { this->status_set_warning(); return; } uint16_t raw_read_status; - if (!this->read_data_(&raw_read_status, 1) || raw_read_status == 0x00) { + if (!this->read_data(&raw_read_status, 1) || raw_read_status == 0x00) { ESP_LOGD(TAG, "Sensor measurement not ready yet."); this->skipped_data_read_cycles_++; /// The following logic is required to address the cases when a sensor is quickly replaced before it's marked @@ -142,7 +130,7 @@ void SPS30Component::update() { return; } - if (!this->write_command_(SPS30_CMD_READ_MEASUREMENT)) { + if (!this->write_command(SPS30_CMD_READ_MEASUREMENT)) { ESP_LOGW(TAG, "Error reading measurement status!"); this->status_set_warning(); return; @@ -150,7 +138,7 @@ void SPS30Component::update() { this->set_timeout(50, [this]() { uint16_t raw_data[20]; - if (!this->read_data_(raw_data, 20)) { + if (!this->read_data(raw_data, 20)) { ESP_LOGW(TAG, "Error reading measurement data!"); this->status_set_warning(); return; @@ -205,69 +193,18 @@ void SPS30Component::update() { }); } -bool SPS30Component::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 SPS30Component::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 SPS30Component::start_continuous_measurement_() { uint8_t data[4]; data[0] = SPS30_CMD_START_CONTINUOUS_MEASUREMENTS & 0xFF; data[1] = 0x03; data[2] = 0x00; data[3] = sht_crc_(0x03, 0x00); - if (!this->write_bytes(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS >> 8, data, 4)) { + if (!this->write_command(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS, SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG)) { ESP_LOGE(TAG, "Error initiating measurements"); return false; } return true; } -bool SPS30Component::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector 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 sps30 } // namespace esphome diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index bae33a46e1..9a93df8597 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -2,14 +2,14 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sps30 { /// This class implements support for the Sensirion SPS30 i2c/UART Particulate Matter /// PM1.0, PM2.5, PM4, PM10 Air Quality sensors. -class SPS30Component : public PollingComponent, public i2c::I2CDevice { +class SPS30Component : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } @@ -29,9 +29,6 @@ class SPS30Component : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override { return setup_priority::DATA; } protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); - uint8_t sht_crc_(uint8_t data1, uint8_t data2); char serial_number_[17] = {0}; /// Terminating NULL character uint16_t raw_firmware_version_; bool start_continuous_measurement_(); diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py index aa7573aaf2..a99503a2b8 100644 --- a/esphome/components/sts3x/sensor.py +++ b/esphome/components/sts3x/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] sts3x_ns = cg.esphome_ns.namespace("sts3x") diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index ce166f2055..5af808b6e7 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -19,13 +19,13 @@ static const uint16_t STS3X_COMMAND_FETCH_DATA = 0xE000; void STS3XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up STS3x..."); - if (!this->write_command_(STS3X_COMMAND_READ_SERIAL_NUMBER)) { + if (!this->write_command(STS3X_COMMAND_READ_SERIAL_NUMBER)) { this->mark_failed(); return; } uint16_t raw_serial_number[2]; - if (!this->read_data_(raw_serial_number, 1)) { + if (!this->read_data(raw_serial_number, 1)) { this->mark_failed(); return; } @@ -46,16 +46,16 @@ float STS3XComponent::get_setup_priority() const { return setup_priority::DATA; void STS3XComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); - this->write_command_(STS3X_COMMAND_SOFT_RESET); + this->write_command(STS3X_COMMAND_SOFT_RESET); } - if (!this->write_command_(STS3X_COMMAND_POLLING_H)) { + if (!this->write_command(STS3X_COMMAND_POLLING_H)) { this->status_set_warning(); return; } this->set_timeout(50, [this]() { uint16_t raw_data[1]; - if (!this->read_data_(raw_data, 1)) { + if (!this->read_data(raw_data, 1)) { this->status_set_warning(); return; } @@ -67,56 +67,5 @@ void STS3XComponent::update() { }); } -bool STS3XComponent::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 sts3x_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 STS3XComponent::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector 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 = sts3x_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 sts3x } // namespace esphome diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h index 436cf938d8..261033efad 100644 --- a/esphome/components/sts3x/sts3x.h +++ b/esphome/components/sts3x/sts3x.h @@ -2,22 +2,18 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sts3x { /// This class implements support for the ST3x-DIS family of temperature i2c sensors. -class STS3XComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { +class STS3XComponent : public sensor::Sensor, public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void setup() override; void dump_config() override; float get_setup_priority() const override; void update() override; - - protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); }; } // namespace sts3x diff --git a/esphome/components/sx1509/sx1509_registers.h b/esphome/components/sx1509/sx1509_registers.h index b97b85993f..9712cacf9b 100644 --- a/esphome/components/sx1509/sx1509_registers.h +++ b/esphome/components/sx1509/sx1509_registers.h @@ -1,11 +1,15 @@ -/****************************************************************************** +/* sx1509_registers.h Register definitions for SX1509. Jim Lindblom @ SparkFun Electronics Original Creation Date: September 21, 2015 https://github.com/sparkfun/SparkFun_SX1509_Arduino_Library +*/ +#pragma once -Here you'll find the Arduino code used to interface with the SX1509 I2C +namespace esphome { +/** + Here you'll find the Arduino code used to interface with the SX1509 I2C 16 I/O expander. There are functions to take advantage of everything the SX1509 provides - input/output setting, writing pins high/low, reading the input value of pins, LED driver utilities (blink, breath, pwm), and @@ -20,10 +24,7 @@ This code is beerware; if you see me (or any other SparkFun employee) at the local, and you've found our code helpful, please buy us a round! Distributed as-is; no warranty is given. -******************************************************************************/ -#pragma once - -namespace esphome { +*/ namespace sx1509 { const uint8_t REG_INPUT_DISABLE_B = diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index f3f8685287..de0d21b968 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -12,11 +12,11 @@ i2c::ErrorCode TCA9548AChannel::readv(uint8_t address, i2c::ReadBuffer *buffers, return err; return parent_->bus_->readv(address, buffers, cnt); } -i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt) { +i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) { auto err = parent_->switch_to_channel(channel_); if (err != i2c::ERROR_OK) return err; - return parent_->bus_->writev(address, buffers, cnt); + return parent_->bus_->writev(address, buffers, cnt, stop); } void TCA9548AComponent::setup() { diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 39d07c2eb4..02553f8cd0 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -13,7 +13,7 @@ class TCA9548AChannel : public i2c::I2CBus { void set_parent(TCA9548AComponent *parent) { parent_ = parent; } i2c::ErrorCode readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) override; - i2c::ErrorCode writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt) override; + i2c::ErrorCode writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) override; protected: uint8_t channel_; diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 0469ba2c37..36c5f4161d 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -176,6 +176,31 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { res += this->second; this->timestamp = res; } + +int32_t ESPTime::timezone_offset() { + int32_t offset = 0; + time_t now = ::time(nullptr); + auto local = ESPTime::from_epoch_local(now); + auto utc = ESPTime::from_epoch_utc(now); + bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; + + if (utc.minute > local.minute) { + local.minute += 60; + local.hour -= 1; + } + offset += (local.minute - utc.minute) * 60; + + if (negative) { + offset -= (utc.hour - local.hour) * 3600; + } else { + if (utc.hour > local.hour) { + local.hour += 24; + } + offset += (local.hour - utc.hour) * 3600; + } + return offset; +} + bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; } bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; } bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; } diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index c45deb0be5..b22c6f04d7 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -88,6 +88,8 @@ struct ESPTime { /// Convert this ESPTime instance back to a tm struct. struct tm to_c_tm(); + static int32_t timezone_offset(); + /// Increment this clock instance by one second. void increment_second(); /// Increment this clock instance by one day. diff --git a/esphome/components/tm1637/binary_sensor.py b/esphome/components/tm1637/binary_sensor.py new file mode 100644 index 0000000000..66b5172358 --- /dev/null +++ b/esphome/components/tm1637/binary_sensor.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ID, CONF_KEY + +CONF_TM1637_ID = "tm1637_id" + +tm1637_ns = cg.esphome_ns.namespace("tm1637") +TM1637Display = tm1637_ns.class_("TM1637Display", cg.PollingComponent) +TM1637Key = tm1637_ns.class_("TM1637Key", binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TM1637Key), + cv.GenerateID(CONF_TM1637_ID): cv.use_id(TM1637Display), + cv.Required(CONF_KEY): cv.int_range(min=0, max=15), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await binary_sensor.register_binary_sensor(var, config) + cg.add(var.set_keycode(config[CONF_KEY])) + hub = await cg.get_variable(config[CONF_TM1637_ID]) + cg.add(hub.add_tm1637_key(var)) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 44f0a841b8..be2192ea22 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -7,11 +7,17 @@ namespace esphome { namespace tm1637 { static const char *const TAG = "display.tm1637"; -const uint8_t TM1637_I2C_COMM1 = 0x40; -const uint8_t TM1637_I2C_COMM2 = 0xC0; -const uint8_t TM1637_I2C_COMM3 = 0x80; +const uint8_t TM1637_CMD_DATA = 0x40; //!< Display data command +const uint8_t TM1637_CMD_CTRL = 0x80; //!< Display control command +const uint8_t TM1637_CMD_ADDR = 0xc0; //!< Display address command const uint8_t TM1637_UNKNOWN_CHAR = 0b11111111; +// Data command bits +const uint8_t TM1637_DATA_WRITE = 0x00; //!< Write data +const uint8_t TM1637_DATA_READ_KEYS = 0x02; //!< Read keys +const uint8_t TM1637_DATA_AUTO_INC_ADDR = 0x00; //!< Auto increment address +const uint8_t TM1637_DATA_FIXED_ADDR = 0x04; //!< Fixed address + // // A // --- @@ -138,6 +144,36 @@ void TM1637Display::dump_config() { LOG_UPDATE_INTERVAL(this); } +#ifdef USE_BINARY_SENSOR +void TM1637Display::loop() { + uint8_t val = this->get_keys(); + for (auto *tm1637_key : this->tm1637_keys_) + tm1637_key->process(val); +} + +uint8_t TM1637Display::get_keys() { + this->start_(); + this->send_byte_(TM1637_CMD_DATA | TM1637_DATA_READ_KEYS); + this->start_(); + uint8_t key_code = read_byte_(); + this->stop_(); + if (key_code != 0xFF) { + // Invert key_code: + // Bit | 7 6 5 4 3 2 1 0 + // ------+------------------------- + // From | S0 S1 S2 K1 K2 1 1 1 + // To | S0 S1 S2 K1 K2 0 0 0 + key_code = ~key_code; + // Shift bits to: + // Bit | 7 6 5 4 3 2 1 0 + // ------+------------------------ + // To | 0 0 0 0 K2 S2 S1 S0 + key_code = (uint8_t)((key_code & 0x80) >> 7 | (key_code & 0x40) >> 5 | (key_code & 0x20) >> 3 | (key_code & 0x08)); + } + return key_code; +} +#endif + void TM1637Display::update() { for (uint8_t &i : this->buffer_) i = 0; @@ -165,14 +201,14 @@ void TM1637Display::stop_() { void TM1637Display::display() { ESP_LOGVV(TAG, "Display %02X%02X%02X%02X", buffer_[0], buffer_[1], buffer_[2], buffer_[3]); - // Write COMM1 + // Write DATA CMND this->start_(); - this->send_byte_(TM1637_I2C_COMM1); + this->send_byte_(TM1637_CMD_DATA); this->stop_(); - // Write COMM2 + first digit address + // Write ADDR CMD + first digit address this->start_(); - this->send_byte_(TM1637_I2C_COMM2); + this->send_byte_(TM1637_CMD_ADDR); // Write the data bytes if (this->inverted_) { @@ -187,20 +223,17 @@ void TM1637Display::display() { this->stop_(); - // Write COMM3 + brightness + // Write display CTRL CMND + brightness this->start_(); - this->send_byte_(TM1637_I2C_COMM3 + ((this->intensity_ & 0x7) | 0x08)); + this->send_byte_(TM1637_CMD_CTRL + ((this->intensity_ & 0x7) | 0x08)); this->stop_(); } bool TM1637Display::send_byte_(uint8_t b) { uint8_t data = b; - - // 8 Data Bits for (uint8_t i = 0; i < 8; i++) { // CLK low this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); this->bit_delay_(); - // Set data bit if (data & 0x01) { this->dio_pin_->pin_mode(gpio::FLAG_INPUT); @@ -209,19 +242,16 @@ bool TM1637Display::send_byte_(uint8_t b) { } this->bit_delay_(); - // CLK high this->clk_pin_->pin_mode(gpio::FLAG_INPUT); this->bit_delay_(); data = data >> 1; } - // Wait for acknowledge // CLK to zero this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); this->dio_pin_->pin_mode(gpio::FLAG_INPUT); this->bit_delay_(); - // CLK to high this->clk_pin_->pin_mode(gpio::FLAG_INPUT); this->bit_delay_(); @@ -237,8 +267,38 @@ bool TM1637Display::send_byte_(uint8_t b) { return ack; } +uint8_t TM1637Display::read_byte_() { + uint8_t retval = 0; + // Prepare DIO to read data + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); + this->bit_delay_(); + // Data is shifted out by the TM1637 on the CLK falling edge + for (uint8_t bit = 0; bit < 8; bit++) { + this->clk_pin_->pin_mode(gpio::FLAG_INPUT); + this->bit_delay_(); + // Read next bit + retval <<= 1; + if (this->dio_pin_->digital_read()) { + retval |= 0x01; + } + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->bit_delay_(); + } + // Return DIO to output mode + // Dummy ACK + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->bit_delay_(); + this->clk_pin_->pin_mode(gpio::FLAG_INPUT); + this->bit_delay_(); + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->bit_delay_(); + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); + this->bit_delay_(); + return retval; +} + uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { - ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); + // ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); uint8_t pos = start_pos; for (; *str != '\0'; str++) { uint8_t data = TM1637_UNKNOWN_CHAR; diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 9b2f014ff9..0a77acaabe 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -8,10 +8,17 @@ #include "esphome/components/time/real_time_clock.h" #endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + namespace esphome { namespace tm1637 { class TM1637Display; +#ifdef USE_BINARY_SENSOR +class TM1637Key; +#endif using tm1637_writer_t = std::function; @@ -46,10 +53,15 @@ class TM1637Display : public PollingComponent { void display(); +#ifdef USE_BINARY_SENSOR + void loop() override; + uint8_t get_keys(); + void add_tm1637_key(TM1637Key *tm1637_key) { this->tm1637_keys_.push_back(tm1637_key); } +#endif + #ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); - /// Evaluate the strftime-format and print the result at position 0. uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); #endif @@ -58,6 +70,7 @@ class TM1637Display : public PollingComponent { void bit_delay_(); void setup_pins_(); bool send_byte_(uint8_t b); + uint8_t read_byte_(); void start_(); void stop_(); @@ -68,7 +81,23 @@ class TM1637Display : public PollingComponent { bool inverted_; optional writer_{}; uint8_t buffer_[6] = {0}; +#ifdef USE_BINARY_SENSOR + std::vector tm1637_keys_{}; +#endif }; +#ifdef USE_BINARY_SENSOR +class TM1637Key : public binary_sensor::BinarySensor { + friend class TM1637Display; + + public: + void set_keycode(uint8_t key_code) { key_code_ = key_code; } + void process(uint8_t data) { this->publish_state(static_cast(data == this->key_code_)); } + + protected: + uint8_t key_code_{0}; +}; +#endif + } // namespace tm1637 } // namespace esphome diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index 125103e2b8..a4bdc8cafd 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -4,6 +4,7 @@ import esphome.codegen as cg from esphome.components import display from esphome import automation from esphome.const import CONF_ON_TOUCH +from esphome.core import coroutine_with_priority CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["display"] @@ -39,3 +40,9 @@ async def register_touchscreen(var, config): [(TouchPoint, "touch")], config[CONF_ON_TOUCH], ) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_global(touchscreen_ns.using) + cg.add_define("USE_TOUCHSCREEN") diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp new file mode 100644 index 0000000000..6f833a5c83 --- /dev/null +++ b/esphome/components/web_server/list_entities.cpp @@ -0,0 +1,97 @@ +#ifdef USE_ARDUINO + +#include "list_entities.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include "web_server.h" + +namespace esphome { +namespace web_server { + +ListEntitiesIterator::ListEntitiesIterator(WebServer *web_server) : web_server_(web_server) {} + +#ifdef USE_BINARY_SENSOR +bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { + this->web_server_->events_.send( + this->web_server_->binary_sensor_json(binary_sensor, binary_sensor->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_COVER +bool ListEntitiesIterator::on_cover(cover::Cover *cover) { + this->web_server_->events_.send(this->web_server_->cover_json(cover, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_FAN +bool ListEntitiesIterator::on_fan(fan::Fan *fan) { + this->web_server_->events_.send(this->web_server_->fan_json(fan, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_LIGHT +bool ListEntitiesIterator::on_light(light::LightState *light) { + this->web_server_->events_.send(this->web_server_->light_json(light, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_SENSOR +bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { + this->web_server_->events_.send(this->web_server_->sensor_json(sensor, sensor->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_SWITCH +bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { + this->web_server_->events_.send(this->web_server_->switch_json(a_switch, a_switch->state, DETAIL_ALL).c_str(), + "state"); + return true; +} +#endif +#ifdef USE_BUTTON +bool ListEntitiesIterator::on_button(button::Button *button) { + this->web_server_->events_.send(this->web_server_->button_json(button, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_TEXT_SENSOR +bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { + this->web_server_->events_.send( + this->web_server_->text_sensor_json(text_sensor, text_sensor->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_LOCK +bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { + this->web_server_->events_.send(this->web_server_->lock_json(a_lock, a_lock->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_CLIMATE +bool ListEntitiesIterator::on_climate(climate::Climate *climate) { + this->web_server_->events_.send(this->web_server_->climate_json(climate, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_NUMBER +bool ListEntitiesIterator::on_number(number::Number *number) { + this->web_server_->events_.send(this->web_server_->number_json(number, number->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_SELECT +bool ListEntitiesIterator::on_select(select::Select *select) { + this->web_server_->events_.send(this->web_server_->select_json(select, select->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +} // namespace web_server +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h new file mode 100644 index 0000000000..85868caff8 --- /dev/null +++ b/esphome/components/web_server/list_entities.h @@ -0,0 +1,60 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/core/component.h" +#include "esphome/core/component_iterator.h" +#include "esphome/core/defines.h" +namespace esphome { +namespace web_server { + +class WebServer; + +class ListEntitiesIterator : public ComponentIterator { + public: + ListEntitiesIterator(WebServer *web_server); +#ifdef USE_BINARY_SENSOR + bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; +#endif +#ifdef USE_COVER + bool on_cover(cover::Cover *cover) override; +#endif +#ifdef USE_FAN + bool on_fan(fan::Fan *fan) override; +#endif +#ifdef USE_LIGHT + bool on_light(light::LightState *light) override; +#endif +#ifdef USE_SENSOR + bool on_sensor(sensor::Sensor *sensor) override; +#endif +#ifdef USE_SWITCH + bool on_switch(switch_::Switch *a_switch) override; +#endif +#ifdef USE_BUTTON + bool on_button(button::Button *button) override; +#endif +#ifdef USE_TEXT_SENSOR + bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; +#endif +#ifdef USE_CLIMATE + bool on_climate(climate::Climate *climate) override; +#endif +#ifdef USE_NUMBER + bool on_number(number::Number *number) override; +#endif +#ifdef USE_SELECT + bool on_select(select::Select *select) override; +#endif +#ifdef USE_LOCK + bool on_lock(lock::Lock *a_lock) override; +#endif + + protected: + WebServer *web_server_; +}; + +} // namespace web_server +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 278aeab937..0dfd608661 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,6 +1,7 @@ #ifdef USE_ARDUINO #include "web_server.h" + #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" @@ -17,7 +18,7 @@ #endif #ifdef USE_LOGGER -#include +#include "esphome/components/logger/logger.h" #endif #ifdef USE_FAN @@ -106,87 +107,7 @@ void WebServer::setup() { }).c_str(), "ping", millis(), 30000); -#ifdef USE_SENSOR - for (auto *obj : App.get_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_SWITCH - for (auto *obj : App.get_switches()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->switch_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_BUTTON - for (auto *obj : App.get_buttons()) - client->send(this->button_json(obj, DETAIL_ALL).c_str(), "state"); -#endif - -#ifdef USE_BINARY_SENSOR - for (auto *obj : App.get_binary_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->binary_sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_FAN - for (auto *obj : App.get_fans()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->fan_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_LIGHT - for (auto *obj : App.get_lights()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->light_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_TEXT_SENSOR - for (auto *obj : App.get_text_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->text_sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_COVER - for (auto *obj : App.get_covers()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->cover_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_NUMBER - for (auto *obj : App.get_numbers()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->number_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_SELECT - for (auto *obj : App.get_selects()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->select_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_CLIMATE - for (auto *obj : App.get_climates()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->climate_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_LOCK - for (auto *obj : App.get_locks()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->lock_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif + this->entities_iterator_.begin(this->include_internal_); }); #ifdef USE_LOGGER @@ -203,6 +124,7 @@ void WebServer::setup() { this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); } +void WebServer::loop() { this->entities_iterator_.advance(); } void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->base_->get_port()); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 2717997f60..bd7acd91a0 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -2,6 +2,8 @@ #ifdef USE_ARDUINO +#include "list_entities.h" + #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/component.h" #include "esphome/core/controller.h" @@ -32,7 +34,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; */ class WebServer : public Controller, public Component, public AsyncWebHandler { public: - WebServer(web_server_base::WebServerBase *base) : base_(base) {} + WebServer(web_server_base::WebServerBase *base) : base_(base), entities_iterator_(ListEntitiesIterator(this)) {} /** Set the URL to the CSS that's sent to each client. Defaults to * https://esphome.io/_static/webserver-v1.min.css @@ -76,6 +78,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { // (In most use cases you won't need these) /// Setup the internal web server and register handlers. void setup() override; + void loop() override; void dump_config() override; @@ -85,12 +88,12 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle an index request under '/'. void handle_index_request(AsyncWebServerRequest *request); -#ifdef WEBSERVER_CSS_INCLUDE +#ifdef USE_WEBSERVER_CSS_INCLUDE /// Handle included css request under '/0.css'. void handle_css_request(AsyncWebServerRequest *request); #endif -#ifdef WEBSERVER_JS_INCLUDE +#ifdef USE_WEBSERVER_JS_INCLUDE /// Handle included js request under '/0.js'. void handle_js_request(AsyncWebServerRequest *request); #endif @@ -217,8 +220,10 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { bool isRequestHandlerTrivial() override; protected: + friend ListEntitiesIterator; web_server_base::WebServerBase *base_; AsyncEventSource events_{"/events"}; + ListEntitiesIterator entities_iterator_; const char *css_url_{nullptr}; const char *css_include_{nullptr}; const char *js_url_{nullptr}; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index bdd745b859..95d97defe2 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -1,6 +1,6 @@ #include "xiaomi_ble.h" -#include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #ifdef USE_ESP32 @@ -12,67 +12,74 @@ namespace xiaomi_ble { static const char *const TAG = "xiaomi_ble"; -bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { +bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { + // button pressed, 3 bytes, only byte 3 is used for supported devices so far + if ((value_type == 0x1001) && (value_length == 3)) { + result.button_press = data[2] == 0; + return true; + } // motion detection, 1 byte, 8-bit unsigned integer - if ((value_type == 0x03) && (value_length == 1)) { + else if ((value_type == 0x0003) && (value_length == 1)) { result.has_motion = data[0]; } // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C - else if ((value_type == 0x04) && (value_length == 2)) { - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1004) && (value_length == 2)) { + const int16_t temperature = encode_uint16(data[1], data[0]); result.temperature = temperature / 10.0f; } // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % - else if ((value_type == 0x06) && (value_length == 2)) { - const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1006) && (value_length == 2)) { + const int16_t humidity = encode_uint16(data[1], data[0]); result.humidity = humidity / 10.0f; } // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx - else if (((value_type == 0x07) || (value_type == 0x0F)) && (value_length == 3)) { - const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); + else if (((value_type == 0x1007) || (value_type == 0x000F)) && (value_length == 3)) { + const uint32_t illuminance = encode_uint24(data[2], data[1], data[0]); result.illuminance = illuminance; - result.is_light = illuminance == 100; + result.is_light = illuminance >= 100; if (value_type == 0x0F) result.has_motion = true; } // soil moisture, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x08) && (value_length == 1)) { + else if ((value_type == 0x1008) && (value_length == 1)) { result.moisture = data[0]; } // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm - else if ((value_type == 0x09) && (value_length == 2)) { - const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1009) && (value_length == 2)) { + const uint16_t conductivity = encode_uint16(data[1], data[0]); result.conductivity = conductivity; } // battery, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x0A) && (value_length == 1)) { + else if ((value_type == 0x100A) && (value_length == 1)) { result.battery_level = data[0]; } // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % - else if ((value_type == 0x0D) && (value_length == 4)) { - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); + else if ((value_type == 0x100D) && (value_length == 4)) { + const int16_t temperature = encode_uint16(data[1], data[0]); + const int16_t humidity = encode_uint16(data[3], data[2]); result.temperature = temperature / 10.0f; result.humidity = humidity / 10.0f; } // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3 - else if ((value_type == 0x10) && (value_length == 2)) { - const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1010) && (value_length == 2)) { + const uint16_t formaldehyde = encode_uint16(data[1], data[0]); result.formaldehyde = formaldehyde / 100.0f; } // on/off state, 1 byte, 8-bit unsigned integer - else if ((value_type == 0x12) && (value_length == 1)) { + else if ((value_type == 0x1012) && (value_length == 1)) { result.is_active = data[0]; } // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x13) && (value_length == 1)) { + else if ((value_type == 0x1013) && (value_length == 1)) { result.tablet = data[0]; } // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min - else if ((value_type == 0x17) && (value_length == 4)) { + else if ((value_type == 0x1017) && (value_length == 4)) { const uint32_t idle_time = encode_uint32(data[3], data[2], data[1], data[0]); result.idle_time = idle_time / 60.0f; result.has_motion = !idle_time; + } else if ((value_type == 0x1018) && (value_length == 1)) { + result.is_light = data[0]; } else { return false; } @@ -115,7 +122,7 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult break; } - const uint8_t value_type = payload[payload_offset + 0]; + const uint16_t value_type = encode_uint16(payload[payload_offset + 1], payload[payload_offset + 0]); const uint8_t *data = &payload[payload_offset + 3]; if (parse_xiaomi_value(value_type, data, value_length, result)) @@ -155,60 +162,67 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service result.is_duplicate = false; result.raw_offset = result.has_capability ? 12 : 11; - if ((raw[2] == 0x98) && (raw[3] == 0x00)) { // MiFlora + const uint16_t device_uuid = encode_uint16(raw[3], raw[2]); + + if (device_uuid == 0x0098) { // MiFlora result.type = XiaomiParseResult::TYPE_HHCCJCY01; result.name = "HHCCJCY01"; - } else if ((raw[2] == 0xaa) && (raw[3] == 0x01)) { // round body, segment LCD + } else if (device_uuid == 0x01aa) { // round body, segment LCD result.type = XiaomiParseResult::TYPE_LYWSDCGQ; result.name = "LYWSDCGQ"; - } else if ((raw[2] == 0x5d) && (raw[3] == 0x01)) { // FlowerPot, RoPot + } else if (device_uuid == 0x015d) { // FlowerPot, RoPot result.type = XiaomiParseResult::TYPE_HHCCPOT002; result.name = "HHCCPOT002"; - } else if ((raw[2] == 0xdf) && (raw[3] == 0x02)) { // Xiaomi (Honeywell) formaldehyde sensor, OLED display + } else if (device_uuid == 0x02df) { // Xiaomi (Honeywell) formaldehyde sensor, OLED display result.type = XiaomiParseResult::TYPE_JQJCY01YM; result.name = "JQJCY01YM"; - } else if ((raw[2] == 0xdd) && (raw[3] == 0x03)) { // Philips/Xiaomi BLE nightlight + } else if (device_uuid == 0x03dd) { // Philips/Xiaomi BLE nightlight result.type = XiaomiParseResult::TYPE_MUE4094RT; result.name = "MUE4094RT"; result.raw_offset -= 6; - } else if ((raw[2] == 0x47 && raw[3] == 0x03) || // ClearGrass-branded, round body, e-ink display - (raw[2] == 0x48 && raw[3] == 0x0B)) { // Qingping-branded, round body, e-ink display — with bindkeys + } else if (device_uuid == 0x0347 || // ClearGrass-branded, round body, e-ink display + device_uuid == 0x0B48) { // Qingping-branded, round body, e-ink display — with bindkeys result.type = XiaomiParseResult::TYPE_CGG1; result.name = "CGG1"; - } else if ((raw[2] == 0xbc) && (raw[3] == 0x03)) { // VegTrug Grow Care Garden + } else if (device_uuid == 0x03bc) { // VegTrug Grow Care Garden result.type = XiaomiParseResult::TYPE_GCLS002; result.name = "GCLS002"; - } else if ((raw[2] == 0x5b) && (raw[3] == 0x04)) { // rectangular body, e-ink display + } else if (device_uuid == 0x045b) { // rectangular body, e-ink display result.type = XiaomiParseResult::TYPE_LYWSD02; result.name = "LYWSD02"; - } else if ((raw[2] == 0x0a) && (raw[3] == 0x04)) { // Mosquito Repellent Smart Version + } else if (device_uuid == 0x040a) { // Mosquito Repellent Smart Version result.type = XiaomiParseResult::TYPE_WX08ZM; result.name = "WX08ZM"; - } else if ((raw[2] == 0x76) && (raw[3] == 0x05)) { // Cleargrass (Qingping) alarm clock, segment LCD + } else if (device_uuid == 0x0576) { // Cleargrass (Qingping) alarm clock, segment LCD result.type = XiaomiParseResult::TYPE_CGD1; result.name = "CGD1"; - } else if ((raw[2] == 0x6F) && (raw[3] == 0x06)) { // Cleargrass (Qingping) Temp & RH Lite + } else if (device_uuid == 0x066F) { // Cleargrass (Qingping) Temp & RH Lite result.type = XiaomiParseResult::TYPE_CGDK2; result.name = "CGDK2"; - } else if ((raw[2] == 0x5b) && (raw[3] == 0x05)) { // small square body, segment LCD, encrypted + } else if (device_uuid == 0x055b) { // small square body, segment LCD, encrypted result.type = XiaomiParseResult::TYPE_LYWSD03MMC; result.name = "LYWSD03MMC"; - } else if ((raw[2] == 0xf6) && (raw[3] == 0x07)) { // Xiaomi-Yeelight BLE nightlight + } else if (device_uuid == 0x07f6) { // Xiaomi-Yeelight BLE nightlight result.type = XiaomiParseResult::TYPE_MJYD02YLA; result.name = "MJYD02YLA"; if (raw.size() == 19) result.raw_offset -= 6; - } else if ((raw[2] == 0xd3) && (raw[3] == 0x06)) { // rectangular body, e-ink display with alarm + } else if (device_uuid == 0x06d3) { // rectangular body, e-ink display with alarm result.type = XiaomiParseResult::TYPE_MHOC303; result.name = "MHOC303"; - } else if ((raw[2] == 0x87) && (raw[3] == 0x03)) { // square body, e-ink display + } else if (device_uuid == 0x0387) { // square body, e-ink display result.type = XiaomiParseResult::TYPE_MHOC401; result.name = "MHOC401"; - } else if ((raw[2] == 0x83) && (raw[3] == 0x0A)) { // Qingping-branded, motion & ambient light sensor + } else if (device_uuid == 0x0A83) { // Qingping-branded, motion & ambient light sensor result.type = XiaomiParseResult::TYPE_CGPR1; result.name = "CGPR1"; if (raw.size() == 19) result.raw_offset -= 6; + } else if (device_uuid == 0x0A8D) { // Xiaomi Mi Motion Sensor 2 + result.type = XiaomiParseResult::TYPE_RTCGQ02LM; + result.name = "RTCGQ02LM"; + if (raw.size() == 19) + result.raw_offset -= 6; } else { ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); return {}; @@ -343,6 +357,9 @@ bool report_xiaomi_results(const optional &result, const std: if (result->is_light.has_value()) { ESP_LOGD(TAG, " Light: %s", (*result->is_light) ? "on" : "off"); } + if (result->button_press.has_value()) { + ESP_LOGD(TAG, " Button: %s", (*result->button_press) ? "pressed" : ""); + } return true; } diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index ee65d7c82f..399bef83b8 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" #ifdef USE_ESP32 @@ -25,7 +25,8 @@ struct XiaomiParseResult { TYPE_MJYD02YLA, TYPE_MHOC303, TYPE_MHOC401, - TYPE_CGPR1 + TYPE_CGPR1, + TYPE_RTCGQ02LM, } type; std::string name; optional temperature; @@ -40,6 +41,7 @@ struct XiaomiParseResult { optional is_active; optional has_motion; optional is_light; + optional button_press; bool has_data; // 0x40 bool has_capability; // 0x20 bool has_encryption; // 0x08 @@ -61,7 +63,7 @@ struct XiaomiAESVector { size_t ivsize; }; -bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); +bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); diff --git a/esphome/components/xiaomi_rtcgq02lm/__init__.py b/esphome/components/xiaomi_rtcgq02lm/__init__.py new file mode 100644 index 0000000000..0c8331db09 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_ID, CONF_BINDKEY + + +AUTO_LOAD = ["xiaomi_ble"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32_ble_tracker"] +MULTI_CONF = True + +xiaomi_rtcgq02lm_ns = cg.esphome_ns.namespace("xiaomi_rtcgq02lm") +XiaomiRTCGQ02LM = xiaomi_rtcgq02lm_ns.class_( + "XiaomiRTCGQ02LM", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiRTCGQ02LM), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) diff --git a/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py new file mode 100644 index 0000000000..8eee10685e --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_LIGHT, + CONF_MOTION, + CONF_TIMEOUT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOTION, + CONF_ID, +) +from esphome.core import TimePeriod + +from . import XiaomiRTCGQ02LM + +DEPENDENCIES = ["xiaomi_rtcgq02lm"] + +CONF_BUTTON = "button" + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(XiaomiRTCGQ02LM), + cv.Optional(CONF_MOTION): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION + ).extend( + { + cv.Optional(CONF_TIMEOUT, default="5s"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=TimePeriod(milliseconds=65535)), + ), + } + ), + cv.Optional(CONF_LIGHT): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_LIGHT + ), + cv.Optional(CONF_BUTTON): binary_sensor.binary_sensor_schema().extend( + { + cv.Optional(CONF_TIMEOUT, default="200ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=TimePeriod(milliseconds=65535)), + ), + } + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_MOTION in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_MOTION]) + cg.add(parent.set_motion(sens)) + cg.add(parent.set_motion_timeout(config[CONF_MOTION][CONF_TIMEOUT])) + + if CONF_LIGHT in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_LIGHT]) + cg.add(parent.set_light(sens)) + + if CONF_BUTTON in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_BUTTON]) + cg.add(parent.set_button(sens)) + cg.add(parent.set_button_timeout(config[CONF_BUTTON][CONF_TIMEOUT])) diff --git a/esphome/components/xiaomi_rtcgq02lm/sensor.py b/esphome/components/xiaomi_rtcgq02lm/sensor.py new file mode 100644 index 0000000000..558e3623e5 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/sensor.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_BATTERY_LEVEL, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + CONF_ID, + DEVICE_CLASS_BATTERY, +) + +from . import XiaomiRTCGQ02LM + +DEPENDENCIES = ["xiaomi_rtcgq02lm"] + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(XiaomiRTCGQ02LM), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(parent.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp new file mode 100644 index 0000000000..498e724368 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -0,0 +1,91 @@ +#include "xiaomi_rtcgq02lm.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_rtcgq02lm { + +static const char *const TAG = "xiaomi_rtcgq02lm"; + +void XiaomiRTCGQ02LM::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi RTCGQ02LM"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Motion", this->motion_); + LOG_BINARY_SENSOR(" ", "Light", this->light_); + LOG_BINARY_SENSOR(" ", "Button", this->button_); +#endif +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +#endif +} + +bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } +#ifdef USE_BINARY_SENSOR + if (res->has_motion.has_value() && this->motion_ != nullptr) { + this->motion_->publish_state(*res->has_motion); + this->set_timeout("motion_timeout", this->motion_timeout_, + [this, res]() { this->motion_->publish_state(false); }); + } + if (res->is_light.has_value() && this->light_ != nullptr) + this->light_->publish_state(*res->is_light); + if (res->button_press.has_value() && this->button_ != nullptr) { + this->button_->publish_state(*res->button_press); + this->set_timeout("button_timeout", this->button_timeout_, + [this, res]() { this->button_->publish_state(false); }); + } +#endif +#ifdef USE_SENSOR + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); +#endif + success = true; + } + + return success; +} + +void XiaomiRTCGQ02LM::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, nullptr, 16); + } +} + +} // namespace xiaomi_rtcgq02lm +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h new file mode 100644 index 0000000000..a16c5209d9 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" +#include "esphome/core/component.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_rtcgq02lm { + +class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + +#ifdef USE_BINARY_SENSOR + void set_motion(binary_sensor::BinarySensor *motion) { this->motion_ = motion; } + void set_motion_timeout(uint16_t timeout) { this->motion_timeout_ = timeout; } + + void set_light(binary_sensor::BinarySensor *light) { this->light_ = light; } + void set_button(binary_sensor::BinarySensor *button) { this->button_ = button; } + void set_button_timeout(uint16_t timeout) { this->button_timeout_ = timeout; } +#endif + +#ifdef USE_SENSOR + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } +#endif + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + +#ifdef USE_BINARY_SENSOR + uint16_t motion_timeout_; + uint16_t button_timeout_; + + binary_sensor::BinarySensor *motion_{nullptr}; + binary_sensor::BinarySensor *light_{nullptr}; + binary_sensor::BinarySensor *button_{nullptr}; +#endif +#ifdef USE_SENSOR + sensor::Sensor *battery_level_{nullptr}; +#endif +}; + +} // namespace xiaomi_rtcgq02lm +} // namespace esphome + +#endif diff --git a/esphome/config.py b/esphome/config.py index af6c5b0b64..a878f3ef79 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -161,6 +161,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): # type: (ConfigPath) -> Optional[vol.Invalid] for err in self.errors: if self.get_deepest_path(err.path) == path: + self.errors.remove(err) return err return None @@ -647,7 +648,7 @@ class FinalValidateValidationStep(ConfigValidationStep): fv.full_config.reset(token) -def validate_config(config, command_line_substitutions): +def validate_config(config, command_line_substitutions) -> Config: result = Config() loader.clear_component_meta_finders() @@ -734,9 +735,6 @@ def validate_config(config, command_line_substitutions): result.add_validation_step(LoadValidationStep(key, config[key])) result.run_validation_steps() - if result.errors: - return result - for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) @@ -991,5 +989,10 @@ def read_config(command_line_substitutions): errstr += f" {errline}" safe_print(errstr) safe_print(indent(dump_dict(res, path)[0])) + + for err in res.errors: + safe_print(color(Fore.BOLD_RED, err.msg)) + safe_print("") + return None return OrderedDict(res) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 8e1c63a54e..3dc8011a87 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -58,7 +58,7 @@ from esphome.core import ( ) from esphome.helpers import list_starts_with, add_class_to_obj from esphome.jsonschema import ( - jschema_composite, + jschema_list, jschema_extractor, jschema_registry, jschema_typed, @@ -327,7 +327,7 @@ def boolean(value): ) -@jschema_composite +@jschema_list def ensure_list(*validators): """Validate this configuration option to be a list. @@ -494,7 +494,11 @@ def templatable(other_validators): """ schema = Schema(other_validators) + @jschema_extractor("templatable") def validator(value): + # pylint: disable=comparison-with-callable + if value == jschema_extractor: + return other_validators if isinstance(value, Lambda): return returning_lambda(value) if isinstance(other_validators, dict): @@ -1546,7 +1550,7 @@ def validate_registry(name, registry): return ensure_list(validate_registry_entry(name, registry)) -@jschema_composite +@jschema_list def maybe_simple_value(*validators, **kwargs): key = kwargs.pop("key", CONF_VALUE) validator = All(*validators) diff --git a/esphome/const.py b/esphome/const.py index 579cefd507..01d2d59c3d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.3.0b2" +__version__ = "2022.4.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" @@ -644,6 +644,7 @@ CONF_STEP_MODE = "step_mode" CONF_STEP_PIN = "step_pin" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" +CONF_STORE_BASELINE = "store_baseline" CONF_SUBNET = "subnet" CONF_SUBSTITUTIONS = "substitutions" CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action" @@ -680,6 +681,7 @@ CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC = "target_temperature_low_command_topi CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC = "target_temperature_low_state_topic" CONF_TARGET_TEMPERATURE_STATE_TOPIC = "target_temperature_state_topic" CONF_TEMPERATURE = "temperature" +CONF_TEMPERATURE_SOURCE = "temperature_source" CONF_TEMPERATURE_STEP = "temperature_step" CONF_TEXT_SENSORS = "text_sensors" CONF_THEN = "then" diff --git a/esphome/components/api/util.cpp b/esphome/core/component_iterator.cpp similarity index 80% rename from esphome/components/api/util.cpp rename to esphome/core/component_iterator.cpp index fd55f89f9b..4781607a2d 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/core/component_iterator.cpp @@ -1,16 +1,18 @@ -#include "util.h" -#include "api_server.h" -#include "user_services.h" -#include "esphome/core/log.h" +#include "component_iterator.h" + #include "esphome/core/application.h" -namespace esphome { -namespace api { +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#include "esphome/components/api/user_services.h" +#endif -ComponentIterator::ComponentIterator(APIServer *server) : server_(server) {} -void ComponentIterator::begin() { +namespace esphome { + +void ComponentIterator::begin(bool include_internal) { this->state_ = IteratorState::BEGIN; this->at_ = 0; + this->include_internal_ = include_internal; } void ComponentIterator::advance() { bool advance_platform = false; @@ -32,7 +34,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *binary_sensor = App.get_binary_sensors()[this->at_]; - if (binary_sensor->is_internal()) { + if (binary_sensor->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -47,7 +49,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *cover = App.get_covers()[this->at_]; - if (cover->is_internal()) { + if (cover->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -62,7 +64,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *fan = App.get_fans()[this->at_]; - if (fan->is_internal()) { + if (fan->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -77,7 +79,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *light = App.get_lights()[this->at_]; - if (light->is_internal()) { + if (light->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -92,7 +94,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *sensor = App.get_sensors()[this->at_]; - if (sensor->is_internal()) { + if (sensor->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -107,7 +109,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *a_switch = App.get_switches()[this->at_]; - if (a_switch->is_internal()) { + if (a_switch->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -122,7 +124,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *button = App.get_buttons()[this->at_]; - if (button->is_internal()) { + if (button->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -137,7 +139,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *text_sensor = App.get_text_sensors()[this->at_]; - if (text_sensor->is_internal()) { + if (text_sensor->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -146,20 +148,22 @@ void ComponentIterator::advance() { } break; #endif +#ifdef USE_API case IteratorState ::SERVICE: - if (this->at_ >= this->server_->get_user_services().size()) { + if (this->at_ >= api::global_api_server->get_user_services().size()) { advance_platform = true; } else { - auto *service = this->server_->get_user_services()[this->at_]; + auto *service = api::global_api_server->get_user_services()[this->at_]; success = this->on_service(service); } break; +#endif #ifdef USE_ESP32_CAMERA case IteratorState::CAMERA: if (esp32_camera::global_esp32_camera == nullptr) { advance_platform = true; } else { - if (esp32_camera::global_esp32_camera->is_internal()) { + if (esp32_camera::global_esp32_camera->is_internal() && !this->include_internal_) { advance_platform = success = true; break; } else { @@ -174,7 +178,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *climate = App.get_climates()[this->at_]; - if (climate->is_internal()) { + if (climate->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -189,7 +193,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *number = App.get_numbers()[this->at_]; - if (number->is_internal()) { + if (number->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -204,7 +208,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *select = App.get_selects()[this->at_]; - if (select->is_internal()) { + if (select->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -219,7 +223,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *a_lock = App.get_locks()[this->at_]; - if (a_lock->is_internal()) { + if (a_lock->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -244,10 +248,10 @@ void ComponentIterator::advance() { } bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } -bool ComponentIterator::on_service(UserServiceDescriptor *service) { return true; } +#ifdef USE_API +bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } +#endif #ifdef USE_ESP32_CAMERA bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; } #endif - -} // namespace api } // namespace esphome diff --git a/esphome/components/api/util.h b/esphome/core/component_iterator.h similarity index 91% rename from esphome/components/api/util.h rename to esphome/core/component_iterator.h index 9204b0829e..bd95fe95e1 100644 --- a/esphome/components/api/util.h +++ b/esphome/core/component_iterator.h @@ -1,23 +1,24 @@ #pragma once -#include "esphome/core/helpers.h" #include "esphome/core/component.h" #include "esphome/core/controller.h" +#include "esphome/core/helpers.h" + #ifdef USE_ESP32_CAMERA #include "esphome/components/esp32_camera/esp32_camera.h" #endif namespace esphome { -namespace api { -class APIServer; +#ifdef USE_API +namespace api { class UserServiceDescriptor; +} // namespace api +#endif class ComponentIterator { public: - ComponentIterator(APIServer *server); - - void begin(); + void begin(bool include_internal = false); void advance(); virtual bool on_begin(); #ifdef USE_BINARY_SENSOR @@ -44,7 +45,9 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif - virtual bool on_service(UserServiceDescriptor *service); +#ifdef USE_API + virtual bool on_service(api::UserServiceDescriptor *service); +#endif #ifdef USE_ESP32_CAMERA virtual bool on_camera(esp32_camera::ESP32Camera *camera); #endif @@ -90,7 +93,9 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif +#ifdef USE_API SERVICE, +#endif #ifdef USE_ESP32_CAMERA CAMERA, #endif @@ -109,9 +114,7 @@ class ComponentIterator { MAX, } state_{IteratorState::NONE}; size_t at_{0}; - - APIServer *server_; + bool include_internal_{false}; }; -} // namespace api } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index aabb5510f4..f304f847a5 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -29,6 +29,7 @@ #define USE_LOCK #define USE_LOGGER #define USE_MDNS +#define USE_MQTT #define USE_NUMBER #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK @@ -40,6 +41,7 @@ #define USE_SWITCH #define USE_TEXT_SENSOR #define USE_TIME +#define USE_TOUCHSCREEN #define USE_UART_DEBUGGER #define USE_WIFI @@ -48,13 +50,17 @@ #define USE_CAPTIVE_PORTAL #define USE_JSON #define USE_NEXTION_TFT_UPLOAD -#define USE_MQTT #define USE_PROMETHEUS #define USE_WEBSERVER #define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WIFI_WPA2_EAP #endif +// IDF-specific feature flags +#ifdef USE_ESP_IDF +#define USE_MQTT_IDF_ENQUEUE +#endif + // ESP32-specific feature flags #ifdef USE_ESP32 #define USE_ESP32_BLE_CLIENT diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 074bea6fd1..0972d6ccd6 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -173,6 +173,10 @@ constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, ui return (static_cast(byte1) << 24) | (static_cast(byte2) << 16) | (static_cast(byte3) << 8) | (static_cast(byte4)); } +/// Encode a 24-bit value given three bytes in most to least significant byte order. +constexpr uint32_t encode_uint24(uint8_t byte1, uint8_t byte2, uint8_t byte3) { + return ((static_cast(byte1) << 16) | (static_cast(byte2) << 8) | (static_cast(byte3))); +} /// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T). template::value, int> = 0> diff --git a/esphome/jsonschema.py b/esphome/jsonschema.py index 12929dc602..94325f4abc 100644 --- a/esphome/jsonschema.py +++ b/esphome/jsonschema.py @@ -1,7 +1,7 @@ """Helpers to retrieve schema from voluptuous validators. These are a helper decorators to help get schema from some -components which uses volutuous in a way where validation +components which uses voluptuous in a way where validation is hidden in local functions These decorators should not modify at all what the functions originally do. @@ -24,7 +24,7 @@ def jschema_extractor(validator_name): if EnableJsonSchemaCollect: def decorator(func): - hidden_schemas[str(func)] = validator_name + hidden_schemas[repr(func)] = validator_name return func return decorator @@ -41,7 +41,7 @@ def jschema_extended(func): def decorate(*args, **kwargs): ret = func(*args, **kwargs) assert len(args) == 2 - extended_schemas[str(ret)] = args + extended_schemas[repr(ret)] = args return ret return decorate @@ -49,13 +49,13 @@ def jschema_extended(func): return func -def jschema_composite(func): +def jschema_list(func): if EnableJsonSchemaCollect: def decorate(*args, **kwargs): ret = func(*args, **kwargs) # args length might be 2, but 2nd is always validator - list_schemas[str(ret)] = args + list_schemas[repr(ret)] = args return ret return decorate @@ -67,7 +67,7 @@ def jschema_registry(registry): if EnableJsonSchemaCollect: def decorator(func): - registry_schemas[str(func)] = registry + registry_schemas[repr(func)] = registry return func return decorator @@ -83,7 +83,7 @@ def jschema_typed(func): def decorate(*args, **kwargs): ret = func(*args, **kwargs) - typed_schemas[str(ret)] = (args, kwargs) + typed_schemas[repr(ret)] = (args, kwargs) return ret return decorate diff --git a/platformio.ini b/platformio.ini index 8775b28156..bc2cddb9f7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -62,9 +62,7 @@ lib_deps = glmnet/Dsmr@0.5 ; dsmr rweather/Crypto@0.2.0 ; dsmr dudanov/MideaUART@1.1.8 ; midea - ; PIO isn't update releases correctly, see: - ; https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd - https://github.com/ToniA/arduino-heatpumpir.git#1.0.18 ; heatpumpir + tonia/HeatpumpIR@1.0.20 ; heatpumpir build_flags = ${common.build_flags} -DUSE_ARDUINO diff --git a/requirements.txt b/requirements.txt index 739ad79098..465d961cb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,17 @@ -voluptuous==0.12.2 +voluptuous==0.13.1 PyYAML==6.0 paho-mqtt==1.6.1 colorama==0.4.4 tornado==6.1 -tzlocal==4.1 # from time +tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile -esptool==3.2 -click==8.0.3 +esptool==3.3 +click==8.1.2 esphome-dashboard==20220309.0 aioesphomeapi==10.8.2 -zeroconf==0.38.3 +zeroconf==0.38.4 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/requirements_test.txt b/requirements_test.txt index afc4fd9d2a..083050252d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ -pylint==2.12.2 +pylint==2.13.5 flake8==4.0.1 -black==22.1.0 -pyupgrade==2.31.0 +black==22.3.0 +pyupgrade==2.32.0 pre-commit # Unit tests -pytest==7.0.1 +pytest==7.1.1 pytest-cov==3.0.0 pytest-mock==3.7.0 -pytest-asyncio==0.18.1 +pytest-asyncio==0.18.3 asyncmock==0.4.2 hypothesis==5.49.0 diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 016a0995b9..26bf8647af 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -236,7 +236,7 @@ class Int64Type(TypeInfo): encode_func = "encode_int64" def dump(self, name): - o = f'sprintf(buffer, "%ll", {name});\n' + o = f'sprintf(buffer, "%lld", {name});\n' o += f"out.append(buffer);" return o @@ -249,7 +249,7 @@ class UInt64Type(TypeInfo): encode_func = "encode_uint64" def dump(self, name): - o = f'sprintf(buffer, "%ull", {name});\n' + o = f'sprintf(buffer, "%llu", {name});\n' o += f"out.append(buffer);" return o @@ -275,7 +275,7 @@ class Fixed64Type(TypeInfo): encode_func = "encode_fixed64" def dump(self, name): - o = f'sprintf(buffer, "%ull", {name});\n' + o = f'sprintf(buffer, "%llu", {name});\n' o += f"out.append(buffer);" return o @@ -417,7 +417,7 @@ class SFixed64Type(TypeInfo): encode_func = "encode_sfixed64" def dump(self, name): - o = f'sprintf(buffer, "%ll", {name});\n' + o = f'sprintf(buffer, "%lld", {name});\n' o += f"out.append(buffer);" return o @@ -440,10 +440,10 @@ class SInt64Type(TypeInfo): cpp_type = "int64_t" default_value = "0" decode_varint = "value.as_sint64()" - encode_func = "encode_sin64" + encode_func = "encode_sint64" def dump(self, name): - o = f'sprintf(buffer, "%ll", {name});\n' + o = f'sprintf(buffer, "%lld", {name});\n' o += f"out.append(buffer);" return o @@ -622,13 +622,13 @@ def build_message_type(desc): protected_content.insert(0, prot) if decode_64bit: decode_64bit.append("default:\n return false;") - o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64bit value) {{\n" + o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64Bit value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_64bit), " ") + "\n" o += " }\n" o += "}\n" cpp += o - prot = "bool decode_64bit(uint32_t field_id, Proto64bit value) override;" + prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;" protected_content.insert(0, prot) o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py index 7673519916..5373d404a7 100644 --- a/script/build_jsonschema.py +++ b/script/build_jsonschema.py @@ -70,7 +70,7 @@ def add_definition_array_or_single_object(ref): def add_core(): from esphome.core.config import CONFIG_SCHEMA - base_props["esphome"] = get_jschema("esphome", CONFIG_SCHEMA.schema) + base_props["esphome"] = get_jschema("esphome", CONFIG_SCHEMA) def add_buses(): @@ -216,7 +216,7 @@ def add_components(): add_module_registries(domain, c.module) add_module_schemas(domain, c.module) - # need first to iterate all platforms then iteate components + # need first to iterate all platforms then iterate components # a platform component can have other components as properties, # e.g. climate components usually have a temperature sensor @@ -325,7 +325,9 @@ def get_entry(parent_key, vschema): if DUMP_COMMENTS: entry[JSC_COMMENT] = "entry: " + parent_key + "/" + str(vschema) - if isinstance(vschema, list): + if isinstance(vschema, dict): + entry = {"what": "is_this"} + elif isinstance(vschema, list): ref = get_jschema(parent_key + "[]", vschema[0]) entry = {"type": "array", "items": ref} elif isinstance(vschema, schema_type) and hasattr(vschema, "schema"): @@ -387,8 +389,10 @@ def get_entry(parent_key, vschema): v = vschema(None) if isinstance(v, ID): - if v.type.base != "script::Script" and ( - v.type.inherits_from(Trigger) or v.type == Automation + if ( + v.type.base != "script::Script" + and v.type.base != "switch_::Switch" + and (v.type.inherits_from(Trigger) or v.type == Automation) ): return None entry = {"type": "string", "id_type": v.type.base} @@ -410,6 +414,8 @@ def default_schema(): def is_default_schema(jschema): + if jschema is None: + return False if is_ref(jschema): jschema = unref(jschema) if not jschema: @@ -425,6 +431,9 @@ def get_jschema(path, vschema, create_return_ref=True): jschema = convert_schema(path, vschema) + if jschema is None: + return None + if is_ref(jschema): # this can happen when returned extended # schemas where all properties found in previous extended schema @@ -450,6 +459,9 @@ def get_schema_str(vschema): def create_ref(name, vschema, jschema): + if jschema is None: + raise ValueError("Cannot create a ref with null jschema for " + name) + if name in schema_names: raise ValueError("Not supported") @@ -523,6 +535,15 @@ def convert_schema(path, vschema, un_extend=True): extended = ejs.extended_schemas.get(str(vschema)) if extended: lhs = get_jschema(path, extended[0], False) + + # The midea actions are extending an empty schema (resulted in the templatize not templatizing anything) + # this causes a recursion in that this extended looks the same in extended schema as the extended[1] + if ejs.extended_schemas.get(str(vschema)) == ejs.extended_schemas.get( + str(extended[1]) + ): + assert path.startswith("midea_ac") + return convert_schema(path, extended[1], False) + rhs = get_jschema(path, extended[1], False) # check if we are not merging properties which are already in base component @@ -567,6 +588,8 @@ def convert_schema(path, vschema, un_extend=True): # we should take the valid schema, # commonly all is used to validate a schema, and then a function which # is not a schema es also given, get_schema will then return a default_schema() + if v == dict: + continue # this is a dict in the SCHEMA of packages val_schema = get_jschema(path, v, False) if is_default_schema(val_schema): if not output: @@ -673,6 +696,11 @@ def add_pin_registry(): for mode in ("INPUT", "OUTPUT"): schema_name = f"PIN.GPIO_FULL_{mode}_PIN_SCHEMA" + + # TODO: get pin definitions properly + if schema_name not in definitions: + definitions[schema_name] = {"type": ["object", "null"], JSC_PROPERTIES: {}} + internal = definitions[schema_name] definitions[schema_name]["additionalItems"] = False definitions[f"PIN.{mode}_INTERNAL"] = internal @@ -683,12 +711,11 @@ def add_pin_registry(): definitions[schema_name] = {"oneOf": schemas, "type": ["string", "object"]} for k, v in pin_registry.items(): - pin_jschema = get_jschema( - f"PIN.{mode}_" + k, v[1][0 if mode == "OUTPUT" else 1] - ) - if unref(pin_jschema): - pin_jschema["required"] = [k] - schemas.append(pin_jschema) + if isinstance(v[1], vol.validators.All): + pin_jschema = get_jschema(f"PIN.{mode}_" + k, v[1]) + if unref(pin_jschema): + pin_jschema["required"] = [k] + schemas.append(pin_jschema) def dump_schema(): @@ -730,9 +757,9 @@ def dump_schema(): cv.valid_name, cv.hex_int, cv.hex_int_range, - pins.output_pin, - pins.input_pin, - pins.input_pullup_pin, + pins.gpio_output_pin_schema, + pins.gpio_input_pin_schema, + pins.gpio_input_pullup_pin_schema, cv.float_with_unit, cv.subscribe_topic, cv.publish_topic, @@ -753,12 +780,12 @@ def dump_schema(): for v in [pins.gpio_input_pin_schema, pins.gpio_input_pullup_pin_schema]: schema_registry[v] = get_ref("PIN.GPIO_FULL_INPUT_PIN_SCHEMA") - for v in [pins.internal_gpio_input_pin_schema, pins.input_pin]: + for v in [pins.internal_gpio_input_pin_schema, pins.gpio_input_pin_schema]: schema_registry[v] = get_ref("PIN.INPUT_INTERNAL") for v in [pins.gpio_output_pin_schema, pins.internal_gpio_output_pin_schema]: schema_registry[v] = get_ref("PIN.GPIO_FULL_OUTPUT_PIN_SCHEMA") - for v in [pins.internal_gpio_output_pin_schema, pins.output_pin]: + for v in [pins.internal_gpio_output_pin_schema, pins.gpio_output_pin_schema]: schema_registry[v] = get_ref("PIN.OUTPUT_INTERNAL") add_module_schemas("CONFIG", cv) diff --git a/script/build_language_schema.py b/script/build_language_schema.py new file mode 100644 index 0000000000..ec0912e9e6 --- /dev/null +++ b/script/build_language_schema.py @@ -0,0 +1,813 @@ +import inspect +import json +import argparse +from operator import truediv +import os +import voluptuous as vol + +# NOTE: Cannot import other esphome components globally as a modification in jsonschema +# is needed before modules are loaded +import esphome.jsonschema as ejs + +ejs.EnableJsonSchemaCollect = True + +# schema format: +# Schemas are splitted in several files in json format, one for core stuff, one for each platform (sensor, binary_sensor, etc) and +# one for each component (dallas, sim800l, etc.) component can have schema for root component/hub and also for platform component, +# e.g. dallas has hub component which has pin and then has the sensor platform which has sensor name, index, etc. +# When files are loaded they are merged in a single object. +# The root format is + +S_CONFIG_VAR = "config_var" +S_CONFIG_VARS = "config_vars" +S_CONFIG_SCHEMA = "CONFIG_SCHEMA" +S_COMPONENT = "component" +S_COMPONENTS = "components" +S_PLATFORMS = "platforms" +S_SCHEMA = "schema" +S_SCHEMAS = "schemas" +S_EXTENDS = "extends" +S_TYPE = "type" +S_NAME = "name" + +parser = argparse.ArgumentParser() +parser.add_argument( + "--output-path", default=".", help="Output path", type=os.path.abspath +) + +args = parser.parse_args() + +DUMP_RAW = False +DUMP_UNKNOWN = False +DUMP_PATH = False +JSON_DUMP_PRETTY = True + +# store here dynamic load of esphome components +components = {} + +schema_core = {} + +# output is where all is built +output = {"core": schema_core} +# The full generated output is here here +schema_full = {"components": output} + +# A string, string map, key is the str(schema) and value is +# a tuple, first element is the schema reference and second is the schema path given, the schema reference is needed to test as different schemas have same key +known_schemas = {} + +solve_registry = [] + + +def get_component_names(): + # return [ + # "esphome", + # "esp32", + # "esp8266", + # "logger", + # "sensor", + # "remote_receiver", + # "binary_sensor", + # ] + from esphome.loader import CORE_COMPONENTS_PATH + + component_names = ["esphome", "sensor"] + + for d in os.listdir(CORE_COMPONENTS_PATH): + if not d.startswith("__") and os.path.isdir( + os.path.join(CORE_COMPONENTS_PATH, d) + ): + if d not in component_names: + component_names.append(d) + + return component_names + + +def load_components(): + from esphome.config import get_component + + for domain in get_component_names(): + components[domain] = get_component(domain) + + +load_components() + +# Import esphome after loading components (so schema is tracked) +# pylint: disable=wrong-import-position +import esphome.core as esphome_core +import esphome.config_validation as cv +from esphome import automation +from esphome import pins +from esphome.components import remote_base +from esphome.const import CONF_TYPE +from esphome.loader import get_platform +from esphome.helpers import write_file_if_changed +from esphome.util import Registry + +# pylint: enable=wrong-import-position + + +def write_file(name, obj): + full_path = os.path.join(args.output_path, name + ".json") + if JSON_DUMP_PRETTY: + json_str = json.dumps(obj, indent=2) + else: + json_str = json.dumps(obj, separators=(",", ":")) + write_file_if_changed(full_path, json_str) + print(f"Wrote {full_path}") + + +def register_module_schemas(key, module, manifest=None): + for name, schema in module_schemas(module): + register_known_schema(key, name, schema) + if ( + manifest and manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS] + ): # not sure about 2nd part of the if, might be useless config (e.g. as3935) + output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True + + +def register_known_schema(module, name, schema): + if module not in output: + output[module] = {S_SCHEMAS: {}} + config = convert_config(schema, f"{module}/{name}") + if S_TYPE not in config: + print(f"Config var without type: {module}.{name}") + + output[module][S_SCHEMAS][name] = config + repr_schema = repr(schema) + if repr_schema in known_schemas: + schema_info = known_schemas[repr_schema] + schema_info.append((schema, f"{module}.{name}")) + else: + known_schemas[repr_schema] = [(schema, f"{module}.{name}")] + + +def module_schemas(module): + # This should yield elements in order so extended schemas are resolved properly + # To do this we check on the source code where the symbol is seen first. Seems to work. + try: + module_str = inspect.getsource(module) + except TypeError: + # improv + module_str = "" + except OSError: + # some empty __init__ files + module_str = "" + schemas = {} + for m_attr_name in dir(module): + m_attr_obj = getattr(module, m_attr_name) + if isConvertibleSchema(m_attr_obj): + schemas[module_str.find(m_attr_name)] = [m_attr_name, m_attr_obj] + + for pos in sorted(schemas.keys()): + yield schemas[pos] + + +found_registries = {} + +# Pin validators keys are the functions in pin which validate the pins +pin_validators = {} + + +def add_pin_validators(): + for m_attr_name in dir(pins): + if "gpio" in m_attr_name: + s = pin_validators[repr(getattr(pins, m_attr_name))] = {} + if "schema" in m_attr_name: + s["schema"] = True # else is just number + if "internal" in m_attr_name: + s["internal"] = True + if "input" in m_attr_name: + s["modes"] = ["input"] + elif "output" in m_attr_name: + s["modes"] = ["output"] + else: + s["modes"] = [] + if "pullup" in m_attr_name: + s["modes"].append("pullup") + from esphome.components.adc import sensor as adc_sensor + + pin_validators[repr(adc_sensor.validate_adc_pin)] = { + "internal": True, + "modes": ["input"], + } + + +def add_module_registries(domain, module): + for attr_name in dir(module): + attr_obj = getattr(module, attr_name) + if isinstance(attr_obj, Registry): + if attr_obj == automation.ACTION_REGISTRY: + reg_type = "action" + reg_domain = "core" + found_registries[repr(attr_obj)] = reg_type + elif attr_obj == automation.CONDITION_REGISTRY: + reg_type = "condition" + reg_domain = "core" + found_registries[repr(attr_obj)] = reg_type + else: # attr_name == "FILTER_REGISTRY": + reg_domain = domain + reg_type = attr_name.partition("_")[0].lower() + found_registries[repr(attr_obj)] = f"{domain}.{reg_type}" + + for name in attr_obj.keys(): + if "." not in name: + reg_entry_name = name + else: + parts = name.split(".") + if len(parts) == 2: + reg_domain = parts[0] + reg_entry_name = parts[1] + else: + reg_domain = ".".join([parts[1], parts[0]]) + reg_entry_name = parts[2] + + if reg_domain not in output: + output[reg_domain] = {} + if reg_type not in output[reg_domain]: + output[reg_domain][reg_type] = {} + output[reg_domain][reg_type][reg_entry_name] = convert_config( + attr_obj[name].schema, f"{reg_domain}/{reg_type}/{reg_entry_name}" + ) + + # print(f"{domain} - {attr_name} - {name}") + + +def do_pins(): + # do pin registries + pins_providers = schema_core["pins"] = [] + for pin_registry in pins.PIN_SCHEMA_REGISTRY: + s = convert_config( + pins.PIN_SCHEMA_REGISTRY[pin_registry][1], f"pins/{pin_registry}" + ) + if pin_registry not in output: + output[pin_registry] = {} # mcp23xxx does not create a component yet + output[pin_registry]["pin"] = s + pins_providers.append(pin_registry) + + +def do_esp32(): + import esphome.components.esp32.boards as esp32_boards + + setEnum( + output["esp32"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"]["board"], + list(esp32_boards.BOARD_TO_VARIANT.keys()), + ) + + +def do_esp8266(): + import esphome.components.esp8266.boards as esp8266_boards + + setEnum( + output["esp8266"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"]["board"], + list(esp8266_boards.ESP8266_BOARD_PINS.keys()), + ) + + +def fix_remote_receiver(): + output["remote_receiver.binary_sensor"]["schemas"]["CONFIG_SCHEMA"] = { + "type": "schema", + "schema": { + "extends": ["binary_sensor.BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"], + "config_vars": output["remote_base"]["binary"], + }, + } + + +def add_referenced_recursive(referenced_schemas, config_var, path, eat_schema=False): + assert ( + S_CONFIG_VARS not in config_var and S_EXTENDS not in config_var + ) # S_TYPE in cv or "key" in cv or len(cv) == 0 + if ( + config_var.get(S_TYPE) in ["schema", "trigger", "maybe"] + and S_SCHEMA in config_var + ): + schema = config_var[S_SCHEMA] + for k, v in schema.get(S_CONFIG_VARS, {}).items(): + if eat_schema: + new_path = path + [S_CONFIG_VARS, k] + else: + new_path = path + ["schema", S_CONFIG_VARS, k] + add_referenced_recursive(referenced_schemas, v, new_path) + for k in schema.get(S_EXTENDS, []): + if k not in referenced_schemas: + referenced_schemas[k] = [path] + else: + if path not in referenced_schemas[k]: + referenced_schemas[k].append(path) + + s1 = get_str_path_schema(k) + p = k.split(".") + if len(p) == 3 and path[0] == f"{p[0]}.{p[1]}": + # special case for schema inside platforms + add_referenced_recursive( + referenced_schemas, s1, [path[0], "schemas", p[2]] + ) + else: + add_referenced_recursive( + referenced_schemas, s1, [p[0], "schemas", p[1]] + ) + elif config_var.get(S_TYPE) == "typed": + for tk, tv in config_var.get("types").items(): + add_referenced_recursive( + referenced_schemas, + { + S_TYPE: S_SCHEMA, + S_SCHEMA: tv, + }, + path + ["types", tk], + eat_schema=True, + ) + + +def get_str_path_schema(strPath): + parts = strPath.split(".") + if len(parts) > 2: + parts[0] += "." + parts[1] + parts[1] = parts[2] + s1 = output.get(parts[0], {}).get(S_SCHEMAS, {}).get(parts[1], {}) + return s1 + + +def pop_str_path_schema(strPath): + parts = strPath.split(".") + if len(parts) > 2: + parts[0] += "." + parts[1] + parts[1] = parts[2] + output.get(parts[0], {}).get(S_SCHEMAS, {}).pop(parts[1]) + + +def get_arr_path_schema(path): + s = output + for x in path: + s = s[x] + return s + + +def merge(source, destination): + """ + run me with nosetests --with-doctest file.py + + >>> a = { 'first' : { 'all_rows' : { 'pass' : 'dog', 'number' : '1' } } } + >>> b = { 'first' : { 'all_rows' : { 'fail' : 'cat', 'number' : '5' } } } + >>> merge(b, a) == { 'first' : { 'all_rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } } + True + """ + for key, value in source.items(): + if isinstance(value, dict): + # get node or create one + node = destination.setdefault(key, {}) + merge(value, node) + else: + destination[key] = value + + return destination + + +def shrink(): + """Shrink the extending schemas which has just an end type, e.g. at this point + ota / port is type schema with extended pointing to core.port, this should instead be + type number. core.port is number + + This also fixes enums, as they are another schema and they are instead put in the same cv + """ + + # referenced_schemas contains a dict, keys are all that are shown in extends: [] arrays, values are lists of paths that are pointing to that extend + # e.g. key: core.COMPONENT_SCHEMA has a lot of paths of config vars which extends this schema + + pass_again = True + + while pass_again: + pass_again = False + + referenced_schemas = {} + + for k, v in output.items(): + for kv, vv in v.items(): + if kv != "pin" and isinstance(vv, dict): + for kvv, vvv in vv.items(): + add_referenced_recursive(referenced_schemas, vvv, [k, kv, kvv]) + + for x, paths in referenced_schemas.items(): + if len(paths) == 1: + key_s = get_str_path_schema(x) + arr_s = get_arr_path_schema(paths[0]) + # key_s |= arr_s + # key_s.pop(S_EXTENDS) + pass_again = True + if S_SCHEMA in arr_s: + if S_EXTENDS in arr_s[S_SCHEMA]: + arr_s[S_SCHEMA].pop(S_EXTENDS) + else: + print("expected extends here!" + x) + arr_s = merge(key_s, arr_s) + if arr_s[S_TYPE] == "enum": + arr_s.pop(S_SCHEMA) + else: + arr_s.pop(S_EXTENDS) + arr_s |= key_s[S_SCHEMA] + print(x) + + # simple types should be spread on each component, + # for enums so far these are logger.is_log_level, cover.validate_cover_state and pulse_counter.sensor.COUNT_MODE_SCHEMA + # then for some reasons sensor filter registry falls here + # then are all simple types, integer and strings + for x, paths in referenced_schemas.items(): + key_s = get_str_path_schema(x) + if key_s and key_s[S_TYPE] in ["enum", "registry", "integer", "string"]: + if key_s[S_TYPE] == "registry": + print("Spreading registry: " + x) + for target in paths: + target_s = get_arr_path_schema(target) + assert target_s[S_SCHEMA][S_EXTENDS] == [x] + target_s.pop(S_SCHEMA) + target_s |= key_s + if key_s[S_TYPE] in ["integer", "string"]: + target_s["data_type"] = x.split(".")[1] + # remove this dangling again + pop_str_path_schema(x) + elif not key_s: + for target in paths: + target_s = get_arr_path_schema(target) + assert target_s[S_SCHEMA][S_EXTENDS] == [x] + target_s.pop(S_SCHEMA) + target_s.pop(S_TYPE) # undefined + target_s["data_type"] = x.split(".")[1] + # remove this dangling again + pop_str_path_schema(x) + + # remove dangling items (unreachable schemas) + for domain, domain_schemas in output.items(): + for schema_name in list(domain_schemas.get(S_SCHEMAS, {}).keys()): + s = f"{domain}.{schema_name}" + if ( + not s.endswith("." + S_CONFIG_SCHEMA) + and s not in referenced_schemas.keys() + ): + print(f"Removing {s}") + output[domain][S_SCHEMAS].pop(schema_name) + + +def build_schema(): + print("Building schema") + + # check esphome was not loaded globally (IDE auto imports) + if len(ejs.extended_schemas) == 0: + raise Exception( + "no data collected. Did you globally import an ESPHome component?" + ) + + # Core schema + schema_core[S_SCHEMAS] = {} + register_module_schemas("core", cv) + + platforms = {} + schema_core[S_PLATFORMS] = platforms + core_components = {} + schema_core[S_COMPONENTS] = core_components + + add_pin_validators() + + # Load a preview of each component + for domain, manifest in components.items(): + if manifest.is_platform_component: + # e.g. sensor, binary sensor, add S_COMPONENTS + # note: S_COMPONENTS is not filled until loaded, e.g. + # if lock: is not used, then we don't need to know about their + # platforms yet. + output[domain] = {S_COMPONENTS: {}, S_SCHEMAS: {}} + platforms[domain] = {} + elif manifest.config_schema is not None: + # e.g. dallas + output[domain] = {S_SCHEMAS: {S_CONFIG_SCHEMA: {}}} + + # Generate platforms (e.g. sensor, binary_sensor, climate ) + for domain in platforms: + c = components[domain] + register_module_schemas(domain, c.module) + + # Generate components + for domain, manifest in components.items(): + if domain not in platforms: + if manifest.config_schema is not None: + core_components[domain] = {} + register_module_schemas(domain, manifest.module, manifest) + + for platform in platforms: + platform_manifest = get_platform(domain=platform, platform=domain) + if platform_manifest is not None: + output[platform][S_COMPONENTS][domain] = {} + register_module_schemas( + f"{domain}.{platform}", platform_manifest.module + ) + + # Do registries + add_module_registries("core", automation) + for domain, manifest in components.items(): + add_module_registries(domain, manifest.module) + add_module_registries("remote_base", remote_base) + + # update props pointing to registries + for reg_config_var in solve_registry: + (registry, config_var) = reg_config_var + config_var[S_TYPE] = "registry" + config_var["registry"] = found_registries[repr(registry)] + + do_pins() + do_esp8266() + do_esp32() + fix_remote_receiver() + shrink() + + # aggregate components, so all component info is in same file, otherwise we have dallas.json, dallas.sensor.json, etc. + data = {} + for component, component_schemas in output.items(): + if "." in component: + key = component.partition(".")[0] + if key not in data: + data[key] = {} + data[key][component] = component_schemas + else: + if component not in data: + data[component] = {} + data[component] |= {component: component_schemas} + + # bundle core inside esphome + data["esphome"]["core"] = data.pop("core")["core"] + + for c, s in data.items(): + write_file(c, s) + + +def setEnum(obj, items): + obj[S_TYPE] = "enum" + obj["values"] = items + + +def isConvertibleSchema(schema): + if schema is None: + return False + if isinstance(schema, (cv.Schema, cv.All)): + return True + if repr(schema) in ejs.hidden_schemas: + return True + if repr(schema) in ejs.typed_schemas: + return True + if repr(schema) in ejs.list_schemas: + return True + if repr(schema) in ejs.registry_schemas: + return True + if isinstance(schema, dict): + for k in schema.keys(): + if isinstance(k, (cv.Required, cv.Optional)): + return True + return False + + +def convert_config(schema, path): + converted = {} + convert_1(schema, converted, path) + return converted + + +def convert_1(schema, config_var, path): + """config_var can be a config_var or a schema: both are dicts + config_var has a S_TYPE property, if this is S_SCHEMA, then it has a S_SCHEMA property + schema does not have a type property, schema can have optionally both S_CONFIG_VARS and S_EXTENDS + """ + repr_schema = repr(schema) + + if repr_schema in known_schemas: + schema_info = known_schemas[(repr_schema)] + for (schema_instance, name) in schema_info: + if schema_instance is schema: + assert S_CONFIG_VARS not in config_var + assert S_EXTENDS not in config_var + if not S_TYPE in config_var: + config_var[S_TYPE] = S_SCHEMA + assert config_var[S_TYPE] == S_SCHEMA + + if S_SCHEMA not in config_var: + config_var[S_SCHEMA] = {} + if S_EXTENDS not in config_var[S_SCHEMA]: + config_var[S_SCHEMA][S_EXTENDS] = [name] + else: + config_var[S_SCHEMA][S_EXTENDS].append(name) + return + + # Extended schemas are tracked when the .extend() is used in a schema + if repr_schema in ejs.extended_schemas: + extended = ejs.extended_schemas.get(repr_schema) + # The midea actions are extending an empty schema (resulted in the templatize not templatizing anything) + # this causes a recursion in that this extended looks the same in extended schema as the extended[1] + if repr_schema == repr(extended[1]): + assert path.startswith("midea_ac/") + return + + assert len(extended) == 2 + convert_1(extended[0], config_var, path + "/extL") + convert_1(extended[1], config_var, path + "/extR") + return + + if isinstance(schema, cv.All): + i = 0 + for inner in schema.validators: + i = i + 1 + convert_1(inner, config_var, path + f"/val {i}") + return + + if hasattr(schema, "validators"): + i = 0 + for inner in schema.validators: + i = i + 1 + convert_1(inner, config_var, path + f"/val {i}") + + if isinstance(schema, cv.Schema): + convert_1(schema.schema, config_var, path + "/all") + return + + if isinstance(schema, dict): + convert_keys(config_var, schema, path) + return + + if repr_schema in ejs.list_schemas: + config_var["is_list"] = True + items_schema = ejs.list_schemas[repr_schema][0] + convert_1(items_schema, config_var, path + "/list") + return + + if DUMP_RAW: + config_var["raw"] = repr_schema + + # pylint: disable=comparison-with-callable + if schema == cv.boolean: + config_var[S_TYPE] = "boolean" + elif schema == automation.validate_potentially_and_condition: + config_var[S_TYPE] = "registry" + config_var["registry"] = "condition" + elif schema == cv.int_ or schema == cv.int_range: + config_var[S_TYPE] = "integer" + elif schema == cv.string or schema == cv.string_strict or schema == cv.valid_name: + config_var[S_TYPE] = "string" + + elif isinstance(schema, vol.Schema): + # test: esphome/project + config_var[S_TYPE] = "schema" + config_var["schema"] = convert_config(schema.schema, path + "/s")["schema"] + + elif repr_schema in pin_validators: + config_var |= pin_validators[repr_schema] + config_var[S_TYPE] = "pin" + + elif repr_schema in ejs.hidden_schemas: + schema_type = ejs.hidden_schemas[repr_schema] + + data = schema(ejs.jschema_extractor) + + # enums, e.g. esp32/variant + if schema_type == "one_of": + config_var[S_TYPE] = "enum" + config_var["values"] = list(data) + elif schema_type == "enum": + config_var[S_TYPE] = "enum" + config_var["values"] = list(data.keys()) + elif schema_type == "maybe": + config_var[S_TYPE] = "maybe" + config_var["schema"] = convert_config(data, path + "/maybe")["schema"] + # esphome/on_boot + elif schema_type == "automation": + extra_schema = None + config_var[S_TYPE] = "trigger" + if automation.AUTOMATION_SCHEMA == ejs.extended_schemas[repr(data)][0]: + extra_schema = ejs.extended_schemas[repr(data)][1] + if ( + extra_schema is not None and len(extra_schema) > 1 + ): # usually only trigger_id here + config = convert_config(extra_schema, path + "/extra") + if "schema" in config: + automation_schema = config["schema"] + if not ( + len(automation_schema["config_vars"]) == 1 + and "trigger_id" in automation_schema["config_vars"] + ): + automation_schema["config_vars"]["then"] = {S_TYPE: "trigger"} + if "trigger_id" in automation_schema["config_vars"]: + automation_schema["config_vars"].pop("trigger_id") + + config_var[S_TYPE] = "trigger" + config_var["schema"] = automation_schema + # some triggers can have a list of actions directly, while others needs to have some other configuration, + # e.g. sensor.on_value_rang, and the list of actions is only accepted under "then" property. + try: + schema({"delay": "1s"}) + except cv.Invalid: + config_var["has_required_var"] = True + else: + print("figure out " + path) + elif schema_type == "effects": + config_var[S_TYPE] = "registry" + config_var["registry"] = "light.effects" + config_var["filter"] = data[0] + elif schema_type == "templatable": + config_var["templatable"] = True + convert_1(data, config_var, path + "/templat") + elif schema_type == "triggers": + # remote base + convert_1(data, config_var, path + "/trigger") + elif schema_type == "sensor": + schema = data + convert_1(data, config_var, path + "/trigger") + else: + raise Exception("Unknown extracted schema type") + + elif repr_schema in ejs.registry_schemas: + solve_registry.append((ejs.registry_schemas[repr_schema], config_var)) + + elif repr_schema in ejs.typed_schemas: + config_var[S_TYPE] = "typed" + types = config_var["types"] = {} + typed_schema = ejs.typed_schemas[repr_schema] + if len(typed_schema) > 1: + config_var["typed_key"] = typed_schema[1].get("key", CONF_TYPE) + for schema_key, schema_type in typed_schema[0][0].items(): + config = convert_config(schema_type, path + "/type_" + schema_key) + types[schema_key] = config["schema"] + + elif DUMP_UNKNOWN: + if S_TYPE not in config_var: + config_var["unknown"] = repr_schema + + if DUMP_PATH: + config_var["path"] = path + + +def get_overridden_config(key, converted): + # check if the key is in any extended schema in this converted schema, i.e. + # if we see a on_value_range in a dallas sensor, then this is overridden because + # it is already defined in sensor + assert S_CONFIG_VARS not in converted and S_EXTENDS not in converted + config = converted.get(S_SCHEMA, {}) + + return get_overridden_key_inner(key, config, {}) + + +def get_overridden_key_inner(key, config, ret): + if S_EXTENDS not in config: + return ret + for s in config[S_EXTENDS]: + p = s.partition(".") + s1 = output.get(p[0], {}).get(S_SCHEMAS, {}).get(p[2], {}).get(S_SCHEMA) + if s1: + if key in s1.get(S_CONFIG_VARS, {}): + for k, v in s1.get(S_CONFIG_VARS)[key].items(): + if k not in ret: # keep most overridden + ret[k] = v + get_overridden_key_inner(key, s1, ret) + + return ret + + +def convert_keys(converted, schema, path): + for k, v in schema.items(): + # deprecated stuff + if repr(v).startswith("