diff --git a/esphome/components/pvvx_mithermometer/display/__init__.py b/esphome/components/pvvx_mithermometer/display/__init__.py new file mode 100644 index 0000000000..d935638933 --- /dev/null +++ b/esphome/components/pvvx_mithermometer/display/__init__.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ble_client, display, time +from esphome.const import ( + CONF_AUTO_CLEAR_ENABLED, + CONF_DISCONNECT_DELAY, + CONF_ID, + CONF_LAMBDA, + CONF_TIME_ID, + CONF_VALIDITY_PERIOD, +) + +DEPENDENCIES = ["ble_client"] + +pvvx_ns = cg.esphome_ns.namespace("pvvx_mithermometer") +PVVXDisplay = pvvx_ns.class_( + "PVVXDisplay", cg.PollingComponent, ble_client.BLEClientNode +) +PVVXDisplayRef = PVVXDisplay.operator("ref") + +CONFIG_SCHEMA = ( + display.BASIC_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PVVXDisplay), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean, + cv.Optional(CONF_DISCONNECT_DELAY, default="5s"): cv.positive_time_period, + cv.Optional(CONF_VALIDITY_PERIOD, default="5min"): cv.All( + cv.positive_time_period_seconds, + cv.Range(max=cv.TimePeriod(seconds=65535)), + ), + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await display.register_display(var, config) + await ble_client.register_ble_node(var, config) + cg.add(var.set_disconnect_delay(config[CONF_DISCONNECT_DELAY].total_milliseconds)) + cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED])) + cg.add(var.set_validity_period(config[CONF_VALIDITY_PERIOD].total_seconds)) + + if CONF_TIME_ID in config: + time_ = await cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_time(time_)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(PVVXDisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp new file mode 100644 index 0000000000..21638ef7e4 --- /dev/null +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -0,0 +1,154 @@ +#include "pvvx_display.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 +namespace esphome { +namespace pvvx_mithermometer { + +static const char *const TAG = "display.pvvx_mithermometer"; + +void PVVXDisplay::dump_config() { + ESP_LOGCONFIG(TAG, "PVVX MiThermometer display:"); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent_->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID : %s", this->char_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Auto clear : %s", YESNO(this->auto_clear_enabled_)); + ESP_LOGCONFIG(TAG, " Set time on connection: %s", YESNO(this->time_ != nullptr)); + ESP_LOGCONFIG(TAG, " Disconnect delay : %dms", this->disconnect_delay_ms_); + LOG_UPDATE_INTERVAL(this); +} + +void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: + ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str()); + this->delayed_disconnect_(); + break; + case ESP_GATTC_DISCONNECT_EVT: + ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str().c_str()); + this->connection_established_ = false; + this->cancel_timeout("disconnect"); + this->char_handle_ = 0; + break; + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent_->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str().c_str()); + break; + } + this->connection_established_ = true; + this->char_handle_ = chr->handle; +#ifdef USE_TIME + this->sync_time_(); +#endif + this->display(); + break; + } + default: + break; + } +} + +void PVVXDisplay::update() { + if (this->auto_clear_enabled_) + this->clear(); + if (this->writer_.has_value()) + (*this->writer_)(*this); + this->display(); +} + +void PVVXDisplay::display() { + if (!this->parent_->enabled) { + ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str().c_str()); + this->parent_->set_enabled(true); + return; + } + if (!this->connection_established_) { + ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", + this->parent_->address_str().c_str()); + return; + } + if (!this->char_handle_) { + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", + this->parent_->address_str().c_str()); + return; + } + ESP_LOGD(TAG, "[%s] Send to display: bignum %d, smallnum: %d, cfg: 0x%02x, validity period: %u.", + this->parent_->address_str().c_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_); + uint8_t blk[8] = {}; + blk[0] = 0x22; + blk[1] = this->bignum_ & 0xff; + blk[2] = (this->bignum_ >> 8) & 0xff; + blk[3] = this->smallnum_ & 0xff; + blk[4] = (this->smallnum_ >> 8) & 0xff; + blk[5] = this->validity_period_ & 0xff; + blk[6] = (this->validity_period_ >> 8) & 0xff; + blk[7] = this->cfg_; + this->send_to_setup_char_(blk, sizeof(blk)); +} + +void PVVXDisplay::setcfgbit_(uint8_t bit, bool value) { + uint8_t mask = 1 << bit; + if (value) { + this->cfg_ |= mask; + } else { + this->cfg_ &= (0xFF ^ mask); + } +} + +void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) { + if (!this->connection_established_) { + ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str().c_str()); + return; + } + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, size, blk, + ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str().c_str(), size); + this->delayed_disconnect_(); + } +} + +void PVVXDisplay::delayed_disconnect_() { + if (this->disconnect_delay_ms_ == 0) + return; + this->cancel_timeout("disconnect"); + this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); }); +} + +#ifdef USE_TIME +void PVVXDisplay::sync_time_() { + if (this->time_ == nullptr) + return; + if (!this->connection_established_) { + ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + return; + } + if (!this->char_handle_) { + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + return; + } + auto time = this->time_->now(); + if (!time.is_valid()) { + ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str().c_str()); + return; + } + time.recalc_timestamp_utc(true); // calculate timestamp of local time + uint8_t blk[5] = {}; + ESP_LOGD(TAG, "[%s] Sync time with timestamp %lu.", this->parent_->address_str().c_str(), time.timestamp); + blk[0] = 0x23; + blk[1] = time.timestamp & 0xff; + blk[2] = (time.timestamp >> 8) & 0xff; + blk[3] = (time.timestamp >> 16) & 0xff; + blk[4] = (time.timestamp >> 24) & 0xff; + this->send_to_setup_char_(blk, sizeof(blk)); +} +#endif + +} // namespace pvvx_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h new file mode 100644 index 0000000000..4de1ab7ba6 --- /dev/null +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -0,0 +1,133 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/ble_client/ble_client.h" + +#ifdef USE_ESP32 +#include +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace pvvx_mithermometer { + +class PVVXDisplay; + +/// Possible units for the big number +enum UNIT { + UNIT_NONE = 0, ///< do not show a unit + UNIT_DEG_GHE, ///< show "°Г" + UNIT_MINUS, ///< show " -" + UNIT_DEG_F, ///< show "°F" + UNIT_LOWDASH, ///< show " _" + UNIT_DEG_C, ///< show "°C" + UNIT_LINES, ///< show " =" + UNIT_DEG_E, ///< show "°E" +}; + +using pvvx_writer_t = std::function; + +class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { + public: + void set_writer(pvvx_writer_t &&writer) { this->writer_ = writer; } + void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; } + void set_disconnect_delay(uint32_t ms) { this->disconnect_delay_ms_ = ms; } + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + void update() override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + + /// Set validity period of the display information in seconds (1..65535) + void set_validity_period(uint16_t validity_period) { this->validity_period_ = validity_period; } + /// Clear the screen + void clear() { + this->bignum_ = 0; + this->smallnum_ = 0; + this->cfg_ = 0; + } + /** + * Print the big number + * + * Valid values are from -99.5 to 1999.5. Smaller values are displayed as Lo, higher as Hi. + * It will printed as it fits in the screen. + */ + void print_bignum(float bignum) { this->bignum_ = bignum * 10; } + /** + * Print the small number + * + * Valid values are from -9 to 99. Smaller values are displayed as Lo, higher as Hi. + */ + void print_smallnum(float smallnum) { this->smallnum_ = smallnum; } + /** + * Print a happy face + * + * Can be combined with print_sad() print_bracket(). + * Possible ouputs are: + * + * @verbatim + * bracket sad happy + * 0 0 0 " " + * 0 0 1 " ^_^ " + * 0 1 0 " -∧- " + * 0 1 1 " Δ△Δ " + * 1 0 0 "( )" + * 1 0 1 "(^_^)" + * 1 1 0 "(-∧-)" + * 1 1 1 "(Δ△Δ)" + * @endverbatim + */ + void print_happy(bool happy = true) { this->setcfgbit_(0, happy); } + /// Print a sad face + void print_sad(bool sad = true) { this->setcfgbit_(1, sad); } + /// Print round brackets around the face + void print_bracket(bool bracket = true) { this->setcfgbit_(2, bracket); } + /// Print percent sign at small number + void print_percent(bool percent = true) { this->setcfgbit_(3, percent); } + /// Print battery sign + void print_battery(bool battery = true) { this->setcfgbit_(4, battery); } + /// Print unit of big number + void print_unit(UNIT unit) { this->cfg_ = (this->cfg_ & 0x1F) | ((unit & 0x7) << 5); } + + void display(); + +#ifdef USE_TIME + void set_time(time::RealTimeClock *time) { this->time_ = time; }; +#endif + + protected: + bool auto_clear_enabled_{true}; + uint32_t disconnect_delay_ms_ = 5000; + uint16_t validity_period_ = 300; + uint16_t bignum_ = 0; + uint16_t smallnum_ = 0; + uint8_t cfg_ = 0; + + void setcfgbit_(uint8_t bit, bool value); + void send_to_setup_char_(uint8_t *blk, size_t size); + void delayed_disconnect_(); +#ifdef USE_TIME + void sync_time_(); + time::RealTimeClock *time_ = nullptr; +#endif + uint16_t char_handle_ = 0; + bool connection_established_ = false; + + esp32_ble_tracker::ESPBTUUID service_uuid_ = + esp32_ble_tracker::ESPBTUUID::from_raw("00001f10-0000-1000-8000-00805f9b34fb"); + esp32_ble_tracker::ESPBTUUID char_uuid_ = + esp32_ble_tracker::ESPBTUUID::from_raw("00001f1f-0000-1000-8000-00805f9b34fb"); + + optional writer_{}; +}; + +} // namespace pvvx_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/const.py b/esphome/const.py index 6f5eaabfcb..b3e038fe20 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -173,6 +173,7 @@ CONF_DIR_PIN = "dir_pin" CONF_DIRECTION = "direction" CONF_DIRECTION_OUTPUT = "direction_output" CONF_DISABLED_BY_DEFAULT = "disabled_by_default" +CONF_DISCONNECT_DELAY = "disconnect_delay" CONF_DISCOVERY = "discovery" CONF_DISCOVERY_OBJECT_ID_GENERATOR = "discovery_object_id_generator" CONF_DISCOVERY_PREFIX = "discovery_prefix" @@ -736,6 +737,7 @@ CONF_USE_ABBREVIATIONS = "use_abbreviations" CONF_USE_ADDRESS = "use_address" CONF_USERNAME = "username" CONF_UUID = "uuid" +CONF_VALIDITY_PERIOD = "validity_period" CONF_VALUE = "value" CONF_VALUE_FONT = "value_font" CONF_VARIABLES = "variables" diff --git a/tests/test1.yaml b/tests/test1.yaml index 5897639ead..387aab6bbe 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2436,6 +2436,21 @@ display: it.fill(Color::WHITE); id(glob_bool_processed) = true; } + - platform: pvvx_mithermometer + ble_client_id: ble_foo + time_id: sntp_time + disconnect_delay: 3s + update_interval: 10min + validity_period: 20min + lambda: |- + it.print_bignum(188.8); + it.print_unit(pvvx_mithermometer::UNIT_DEG_E); + it.print_smallnum(88); + it.print_percent(true); + it.print_happy(true); + it.print_sad(true); + it.print_bracket(true); + it.print_battery(true); tm1651: id: tm1651_battery