mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'api_errors' into integration
This commit is contained in:
		| @@ -86,8 +86,8 @@ void APIConnection::start() { | |||||||
|   APIError err = this->helper_->init(); |   APIError err = this->helper_->init(); | ||||||
|   if (err != APIError::OK) { |   if (err != APIError::OK) { | ||||||
|     on_fatal_error(); |     on_fatal_error(); | ||||||
|     ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(), |     ESP_LOGW(TAG, "%s: Helper init failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), | ||||||
|              api_error_to_str(err), errno); |              errno); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   this->client_info_ = helper_->getpeername(); |   this->client_info_ = helper_->getpeername(); | ||||||
| @@ -119,7 +119,7 @@ void APIConnection::loop() { | |||||||
|   APIError err = this->helper_->loop(); |   APIError err = this->helper_->loop(); | ||||||
|   if (err != APIError::OK) { |   if (err != APIError::OK) { | ||||||
|     on_fatal_error(); |     on_fatal_error(); | ||||||
|     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), |     ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||||
|              api_error_to_str(err), errno); |              api_error_to_str(err), errno); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| @@ -136,14 +136,8 @@ void APIConnection::loop() { | |||||||
|         break; |         break; | ||||||
|       } else if (err != APIError::OK) { |       } else if (err != APIError::OK) { | ||||||
|         on_fatal_error(); |         on_fatal_error(); | ||||||
|         if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { |         ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), | ||||||
|           ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); |                  errno); | ||||||
|         } else if (err == APIError::CONNECTION_CLOSED) { |  | ||||||
|           ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); |  | ||||||
|         } else { |  | ||||||
|           ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), |  | ||||||
|                    api_error_to_str(err), errno); |  | ||||||
|         } |  | ||||||
|         return; |         return; | ||||||
|       } else { |       } else { | ||||||
|         this->last_traffic_ = now; |         this->last_traffic_ = now; | ||||||
| @@ -1612,7 +1606,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { | |||||||
|   APIError err = this->helper_->loop(); |   APIError err = this->helper_->loop(); | ||||||
|   if (err != APIError::OK) { |   if (err != APIError::OK) { | ||||||
|     on_fatal_error(); |     on_fatal_error(); | ||||||
|     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), |     ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||||
|              api_error_to_str(err), errno); |              api_error_to_str(err), errno); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| @@ -1633,12 +1627,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { | |||||||
|     return false; |     return false; | ||||||
|   if (err != APIError::OK) { |   if (err != APIError::OK) { | ||||||
|     on_fatal_error(); |     on_fatal_error(); | ||||||
|     if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { |     ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||||
|       ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); |              api_error_to_str(err), errno); | ||||||
|     } else { |  | ||||||
|       ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), |  | ||||||
|                api_error_to_str(err), errno); |  | ||||||
|     } |  | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   // Do not set last_traffic_ on send |   // Do not set last_traffic_ on send | ||||||
| @@ -1646,11 +1636,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { | |||||||
| } | } | ||||||
| void APIConnection::on_unauthenticated_access() { | void APIConnection::on_unauthenticated_access() { | ||||||
|   this->on_fatal_error(); |   this->on_fatal_error(); | ||||||
|   ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str()); |   ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str()); | ||||||
| } | } | ||||||
| void APIConnection::on_no_setup_connection() { | void APIConnection::on_no_setup_connection() { | ||||||
|   this->on_fatal_error(); |   this->on_fatal_error(); | ||||||
|   ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); |   ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str()); | ||||||
| } | } | ||||||
| void APIConnection::on_fatal_error() { | void APIConnection::on_fatal_error() { | ||||||
|   this->helper_->close(); |   this->helper_->close(); | ||||||
| @@ -1815,12 +1805,8 @@ void APIConnection::process_batch_() { | |||||||
|       this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); |       this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); | ||||||
|   if (err != APIError::OK && err != APIError::WOULD_BLOCK) { |   if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||||
|     on_fatal_error(); |     on_fatal_error(); | ||||||
|     if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { |     ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), | ||||||
|       ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str()); |              errno); | ||||||
|     } else { |  | ||||||
|       ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), |  | ||||||
|                api_error_to_str(err), errno); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   | |||||||
| @@ -426,7 +426,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | |||||||
|   ESP_LOGD(TAG, "Noise PSK saved"); |   ESP_LOGD(TAG, "Noise PSK saved"); | ||||||
|   if (make_active) { |   if (make_active) { | ||||||
|     this->set_timeout(100, [this, psk]() { |     this->set_timeout(100, [this, psk]() { | ||||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset connections"); |       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||||
|       this->set_noise_psk(psk); |       this->set_noise_psk(psk); | ||||||
|       for (auto &c : this->clients_) { |       for (auto &c : this->clients_) { | ||||||
|         c->send_message(DisconnectRequest()); |         c->send_message(DisconnectRequest()); | ||||||
|   | |||||||
| @@ -1,11 +1,16 @@ | |||||||
|  | import logging | ||||||
|  |  | ||||||
| from esphome import pins | from esphome import pins | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.components import binary_sensor | from esphome.components import binary_sensor | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import CONF_PIN | from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN | ||||||
|  | from esphome.core import CORE | ||||||
|  |  | ||||||
| from .. import gpio_ns | from .. import gpio_ns | ||||||
|  |  | ||||||
|  | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| GPIOBinarySensor = gpio_ns.class_( | GPIOBinarySensor = gpio_ns.class_( | ||||||
|     "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component |     "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component | ||||||
| ) | ) | ||||||
| @@ -41,6 +46,22 @@ async def to_code(config): | |||||||
|     pin = await cg.gpio_pin_expression(config[CONF_PIN]) |     pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||||
|     cg.add(var.set_pin(pin)) |     cg.add(var.set_pin(pin)) | ||||||
|  |  | ||||||
|     cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) |     # Check for ESP8266 GPIO16 interrupt limitation | ||||||
|     if config[CONF_USE_INTERRUPT]: |     # GPIO16 on ESP8266 is a special pin that doesn't support interrupts through | ||||||
|  |     # the Arduino attachInterrupt() function. This is the only known GPIO pin | ||||||
|  |     # across all supported platforms that has this limitation, so we handle it | ||||||
|  |     # here instead of in the platform-specific code. | ||||||
|  |     use_interrupt = config[CONF_USE_INTERRUPT] | ||||||
|  |     if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16: | ||||||
|  |         _LOGGER.warning( | ||||||
|  |             "GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. " | ||||||
|  |             "Falling back to polling mode (same as in ESPHome <2025.7). " | ||||||
|  |             "The sensor will work exactly as before, but other pins have better " | ||||||
|  |             "performance with interrupts.", | ||||||
|  |             config.get(CONF_NAME, config[CONF_ID]), | ||||||
|  |         ) | ||||||
|  |         use_interrupt = False | ||||||
|  |  | ||||||
|  |     cg.add(var.set_use_interrupt(use_interrupt)) | ||||||
|  |     if use_interrupt: | ||||||
|         cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) |         cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) | ||||||
|   | |||||||
| @@ -29,9 +29,9 @@ from ..defines import ( | |||||||
| ) | ) | ||||||
| from ..helpers import add_lv_use, lvgl_components_required | from ..helpers import add_lv_use, lvgl_components_required | ||||||
| from ..lv_validation import ( | from ..lv_validation import ( | ||||||
|     angle, |  | ||||||
|     get_end_value, |     get_end_value, | ||||||
|     get_start_value, |     get_start_value, | ||||||
|  |     lv_angle, | ||||||
|     lv_bool, |     lv_bool, | ||||||
|     lv_color, |     lv_color, | ||||||
|     lv_float, |     lv_float, | ||||||
| @@ -162,7 +162,7 @@ SCALE_SCHEMA = cv.Schema( | |||||||
|         cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, |         cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, | ||||||
|         cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, |         cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, | ||||||
|         cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), |         cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), | ||||||
|         cv.Optional(CONF_ROTATION): angle, |         cv.Optional(CONF_ROTATION): lv_angle, | ||||||
|         cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), |         cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
| @@ -187,7 +187,7 @@ class MeterType(WidgetType): | |||||||
|         for scale_conf in config.get(CONF_SCALES, ()): |         for scale_conf in config.get(CONF_SCALES, ()): | ||||||
|             rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 |             rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 | ||||||
|             if CONF_ROTATION in scale_conf: |             if CONF_ROTATION in scale_conf: | ||||||
|                 rotation = scale_conf[CONF_ROTATION] // 10 |                 rotation = await lv_angle.process(scale_conf[CONF_ROTATION]) | ||||||
|             with LocalVariable( |             with LocalVariable( | ||||||
|                 "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) |                 "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) | ||||||
|             ) as meter_var: |             ) as meter_var: | ||||||
| @@ -205,21 +205,20 @@ class MeterType(WidgetType): | |||||||
|                         var, |                         var, | ||||||
|                         meter_var, |                         meter_var, | ||||||
|                         ticks[CONF_COUNT], |                         ticks[CONF_COUNT], | ||||||
|                         ticks[CONF_WIDTH], |                         await size.process(ticks[CONF_WIDTH]), | ||||||
|                         ticks[CONF_LENGTH], |                         await size.process(ticks[CONF_LENGTH]), | ||||||
|                         color, |                         color, | ||||||
|                     ) |                     ) | ||||||
|                     if CONF_MAJOR in ticks: |                     if CONF_MAJOR in ticks: | ||||||
|                         major = ticks[CONF_MAJOR] |                         major = ticks[CONF_MAJOR] | ||||||
|                         color = await lv_color.process(major[CONF_COLOR]) |  | ||||||
|                         lv.meter_set_scale_major_ticks( |                         lv.meter_set_scale_major_ticks( | ||||||
|                             var, |                             var, | ||||||
|                             meter_var, |                             meter_var, | ||||||
|                             major[CONF_STRIDE], |                             major[CONF_STRIDE], | ||||||
|                             major[CONF_WIDTH], |                             await size.process(major[CONF_WIDTH]), | ||||||
|                             major[CONF_LENGTH], |                             await size.process(major[CONF_LENGTH]), | ||||||
|                             color, |                             await lv_color.process(major[CONF_COLOR]), | ||||||
|                             major[CONF_LABEL_GAP], |                             await size.process(major[CONF_LABEL_GAP]), | ||||||
|                         ) |                         ) | ||||||
|                 for indicator in scale_conf.get(CONF_INDICATORS, ()): |                 for indicator in scale_conf.get(CONF_INDICATORS, ()): | ||||||
|                     (t, v) = next(iter(indicator.items())) |                     (t, v) = next(iter(indicator.items())) | ||||||
| @@ -233,7 +232,11 @@ class MeterType(WidgetType): | |||||||
|                         lv_assign( |                         lv_assign( | ||||||
|                             ivar, |                             ivar, | ||||||
|                             lv_expr.meter_add_needle_line( |                             lv_expr.meter_add_needle_line( | ||||||
|                                 var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] |                                 var, | ||||||
|  |                                 meter_var, | ||||||
|  |                                 await size.process(v[CONF_WIDTH]), | ||||||
|  |                                 color, | ||||||
|  |                                 await size.process(v[CONF_R_MOD]), | ||||||
|                             ), |                             ), | ||||||
|                         ) |                         ) | ||||||
|                     if t == CONF_ARC: |                     if t == CONF_ARC: | ||||||
| @@ -241,7 +244,11 @@ class MeterType(WidgetType): | |||||||
|                         lv_assign( |                         lv_assign( | ||||||
|                             ivar, |                             ivar, | ||||||
|                             lv_expr.meter_add_arc( |                             lv_expr.meter_add_arc( | ||||||
|                                 var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] |                                 var, | ||||||
|  |                                 meter_var, | ||||||
|  |                                 await size.process(v[CONF_WIDTH]), | ||||||
|  |                                 color, | ||||||
|  |                                 await size.process(v[CONF_R_MOD]), | ||||||
|                             ), |                             ), | ||||||
|                         ) |                         ) | ||||||
|                     if t == CONF_TICK_STYLE: |                     if t == CONF_TICK_STYLE: | ||||||
| @@ -257,7 +264,7 @@ class MeterType(WidgetType): | |||||||
|                                 color_start, |                                 color_start, | ||||||
|                                 color_end, |                                 color_end, | ||||||
|                                 v[CONF_LOCAL], |                                 v[CONF_LOCAL], | ||||||
|                                 v[CONF_WIDTH], |                                 size.process(v[CONF_WIDTH]), | ||||||
|                             ), |                             ), | ||||||
|                         ) |                         ) | ||||||
|                     if t == CONF_IMAGE: |                     if t == CONF_IMAGE: | ||||||
|   | |||||||
							
								
								
									
										69
									
								
								tests/component_tests/gpio/test_gpio_binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								tests/component_tests/gpio/test_gpio_binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | """Tests for the GPIO binary sensor component.""" | ||||||
|  |  | ||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from collections.abc import Callable | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_gpio_binary_sensor_basic_setup( | ||||||
|  |     generate_main: Callable[[str | Path], str], | ||||||
|  | ) -> None: | ||||||
|  |     """ | ||||||
|  |     When the GPIO binary sensor is set in the yaml file, it should be registered in main | ||||||
|  |     """ | ||||||
|  |     main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml") | ||||||
|  |  | ||||||
|  |     assert "new gpio::GPIOBinarySensor();" in main_cpp | ||||||
|  |     assert "App.register_binary_sensor" in main_cpp | ||||||
|  |     assert "bs_gpio->set_use_interrupt(true);" in main_cpp | ||||||
|  |     assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_gpio_binary_sensor_esp8266_gpio16_disables_interrupt( | ||||||
|  |     generate_main: Callable[[str | Path], str], | ||||||
|  |     caplog: pytest.LogCaptureFixture, | ||||||
|  | ) -> None: | ||||||
|  |     """ | ||||||
|  |     Test that ESP8266 GPIO16 automatically disables interrupt mode with a warning | ||||||
|  |     """ | ||||||
|  |     main_cpp = generate_main( | ||||||
|  |         "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Check that interrupt is disabled for GPIO16 | ||||||
|  |     assert "bs_gpio16->set_use_interrupt(false);" in main_cpp | ||||||
|  |  | ||||||
|  |     # Check that the warning was logged | ||||||
|  |     assert "GPIO16 on ESP8266 doesn't support interrupts" in caplog.text | ||||||
|  |     assert "Falling back to polling mode" in caplog.text | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt( | ||||||
|  |     generate_main: Callable[[str | Path], str], | ||||||
|  | ) -> None: | ||||||
|  |     """ | ||||||
|  |     Test that ESP8266 pins other than GPIO16 still use interrupt mode | ||||||
|  |     """ | ||||||
|  |     main_cpp = generate_main( | ||||||
|  |         "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # GPIO5 should still use interrupts | ||||||
|  |     assert "bs_gpio5->set_use_interrupt(true);" in main_cpp | ||||||
|  |     assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_gpio_binary_sensor_explicit_polling_mode( | ||||||
|  |     generate_main: Callable[[str | Path], str], | ||||||
|  | ) -> None: | ||||||
|  |     """ | ||||||
|  |     Test that explicitly setting use_interrupt: false works | ||||||
|  |     """ | ||||||
|  |     main_cpp = generate_main( | ||||||
|  |         "tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     assert "bs_polling->set_use_interrupt(false);" in main_cpp | ||||||
							
								
								
									
										11
									
								
								tests/component_tests/gpio/test_gpio_binary_sensor.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tests/component_tests/gpio/test_gpio_binary_sensor.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | esphome: | ||||||
|  |   name: test | ||||||
|  |  | ||||||
|  | esp32: | ||||||
|  |   board: esp32dev | ||||||
|  |  | ||||||
|  | binary_sensor: | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: 5 | ||||||
|  |     name: "Test GPIO Binary Sensor" | ||||||
|  |     id: bs_gpio | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | esphome: | ||||||
|  |   name: test | ||||||
|  |  | ||||||
|  | esp8266: | ||||||
|  |   board: d1_mini | ||||||
|  |  | ||||||
|  | binary_sensor: | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: | ||||||
|  |       number: 16 | ||||||
|  |       mode: INPUT_PULLDOWN_16 | ||||||
|  |     name: "GPIO16 Touch Sensor" | ||||||
|  |     id: bs_gpio16 | ||||||
|  |  | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: | ||||||
|  |       number: 5 | ||||||
|  |       mode: INPUT_PULLUP | ||||||
|  |     name: "GPIO5 Button" | ||||||
|  |     id: bs_gpio5 | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | esphome: | ||||||
|  |   name: test | ||||||
|  |  | ||||||
|  | esp32: | ||||||
|  |   board: esp32dev | ||||||
|  |  | ||||||
|  | binary_sensor: | ||||||
|  |   - platform: gpio | ||||||
|  |     pin: 5 | ||||||
|  |     name: "Polling Mode Sensor" | ||||||
|  |     id: bs_polling | ||||||
|  |     use_interrupt: false | ||||||
| @@ -919,21 +919,21 @@ lvgl: | |||||||
|                       text_color: 0xFFFFFF |                       text_color: 0xFFFFFF | ||||||
|                       scales: |                       scales: | ||||||
|                         - ticks: |                         - ticks: | ||||||
|                             width: 1 |                             width: !lambda return 1; | ||||||
|                             count: 61 |                             count: 61 | ||||||
|                             length: 20 |                             length: 20% | ||||||
|                             color: 0xFFFFFF |                             color: 0xFFFFFF | ||||||
|                           range_from: 0 |                           range_from: 0 | ||||||
|                           range_to: 60 |                           range_to: 60 | ||||||
|                           angle_range: 360 |                           angle_range: 360 | ||||||
|                           rotation: 270 |                           rotation: !lambda return 2700; | ||||||
|                           indicators: |                           indicators: | ||||||
|                             - line: |                             - line: | ||||||
|                                 opa: 50% |                                 opa: 50% | ||||||
|                                 id: minute_hand |                                 id: minute_hand | ||||||
|                                 color: 0xFF0000 |                                 color: 0xFF0000 | ||||||
|                                 r_mod: -1 |                                 r_mod: !lambda return -1; | ||||||
|                                 width: 3 |                                 width: !lambda return 3; | ||||||
|                         - |                         - | ||||||
|                           angle_range: 330 |                           angle_range: 330 | ||||||
|                           rotation: 300 |                           rotation: 300 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user