mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 06:33:51 +00:00 
			
		
		
		
	Add support for pvvx mithermometer display via ble client (#3333)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										56
									
								
								esphome/components/pvvx_mithermometer/display/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								esphome/components/pvvx_mithermometer/display/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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_)) | ||||||
							
								
								
									
										154
									
								
								esphome/components/pvvx_mithermometer/display/pvvx_display.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								esphome/components/pvvx_mithermometer/display/pvvx_display.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										133
									
								
								esphome/components/pvvx_mithermometer/display/pvvx_display.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								esphome/components/pvvx_mithermometer/display/pvvx_display.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <esp_gattc_api.h> | ||||||
|  | #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<void(PVVXDisplay &)>; | ||||||
|  |  | ||||||
|  | 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<pvvx_writer_t> writer_{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace pvvx_mithermometer | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
| @@ -173,6 +173,7 @@ CONF_DIR_PIN = "dir_pin" | |||||||
| CONF_DIRECTION = "direction" | CONF_DIRECTION = "direction" | ||||||
| CONF_DIRECTION_OUTPUT = "direction_output" | CONF_DIRECTION_OUTPUT = "direction_output" | ||||||
| CONF_DISABLED_BY_DEFAULT = "disabled_by_default" | CONF_DISABLED_BY_DEFAULT = "disabled_by_default" | ||||||
|  | CONF_DISCONNECT_DELAY = "disconnect_delay" | ||||||
| CONF_DISCOVERY = "discovery" | CONF_DISCOVERY = "discovery" | ||||||
| CONF_DISCOVERY_OBJECT_ID_GENERATOR = "discovery_object_id_generator" | CONF_DISCOVERY_OBJECT_ID_GENERATOR = "discovery_object_id_generator" | ||||||
| CONF_DISCOVERY_PREFIX = "discovery_prefix" | CONF_DISCOVERY_PREFIX = "discovery_prefix" | ||||||
| @@ -736,6 +737,7 @@ CONF_USE_ABBREVIATIONS = "use_abbreviations" | |||||||
| CONF_USE_ADDRESS = "use_address" | CONF_USE_ADDRESS = "use_address" | ||||||
| CONF_USERNAME = "username" | CONF_USERNAME = "username" | ||||||
| CONF_UUID = "uuid" | CONF_UUID = "uuid" | ||||||
|  | CONF_VALIDITY_PERIOD = "validity_period" | ||||||
| CONF_VALUE = "value" | CONF_VALUE = "value" | ||||||
| CONF_VALUE_FONT = "value_font" | CONF_VALUE_FONT = "value_font" | ||||||
| CONF_VARIABLES = "variables" | CONF_VARIABLES = "variables" | ||||||
|   | |||||||
| @@ -2436,6 +2436,21 @@ display: | |||||||
|         it.fill(Color::WHITE); |         it.fill(Color::WHITE); | ||||||
|         id(glob_bool_processed) = true; |         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: | tm1651: | ||||||
|   id: tm1651_battery |   id: tm1651_battery | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user